reintroduce microsoft login. upgrade a bunch of stuff.

This commit is contained in:
2023-01-19 16:20:41 -06:00
parent 2a423bcb93
commit 1d0beaf69f
29 changed files with 601 additions and 483 deletions

View File

@@ -38,23 +38,8 @@ public class UserController : ControllerBase
}; };
} }
[HttpPut("GuestLogout")]
public async Task<IActionResult> GuestLogout()
{
var signoutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var userId = User?.GetGuestUserId();
if (!string.IsNullOrEmpty(userId))
{
connectionManager.Unsubscribe(userId);
}
await signoutTask;
return Ok();
}
[HttpGet("Token")] [HttpGet("Token")]
public ActionResult<CreateTokenResponse> GetToken() public ActionResult<CreateTokenResponse> GetWebSocketToken()
{ {
var userId = User.GetShogiUserId(); var userId = User.GetShogiUserId();
var displayName = User.DisplayName(); var displayName = User.DisplayName();
@@ -83,4 +68,19 @@ public class UserController : ControllerBase
} }
return Ok(); return Ok();
} }
[HttpPut("GuestLogout")]
public async Task<IActionResult> GuestLogout()
{
var signOutTask = HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var userId = User?.GetGuestUserId();
if (!string.IsNullOrEmpty(userId))
{
connectionManager.Unsubscribe(userId);
}
await signOutTask;
return Ok();
}
} }

View File

@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.HttpLogging; using Microsoft.AspNetCore.HttpLogging;
using Microsoft.Identity.Web;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Shogi.Api.Managers; using Shogi.Api.Managers;
using Shogi.Api.Repositories; using Shogi.Api.Repositories;
@@ -119,9 +120,9 @@ namespace Shogi.Api
static void AddJwtAuth(WebApplicationBuilder builder) static void AddJwtAuth(WebApplicationBuilder builder)
{ {
//builder.Services builder.Services
// .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
//.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
} }
static void AddCookieAuth(WebApplicationBuilder builder) static void AddCookieAuth(WebApplicationBuilder builder)

View File

@@ -11,7 +11,12 @@ public class UserRepository : IUserRepository
public UserRepository(IConfiguration configuration) public UserRepository(IConfiguration configuration)
{ {
connectionString = configuration.GetConnectionString("ShogiDatabase"); 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) public async Task CreateUser(User user)

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers> <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>5</AnalysisLevel> <AnalysisLevel>5</AnalysisLevel>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@@ -24,12 +24,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" /> <PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
<PackageReference Include="Azure.Identity" Version="1.6.1" /> <PackageReference Include="Azure.Identity" Version="1.8.1" />
<PackageReference Include="Dapper" Version="2.0.123" /> <PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="FluentValidation" Version="11.2.0" /> <PackageReference Include="FluentValidation" Version="11.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> <PackageReference Include="Microsoft.Identity.Web" Version="1.25.10" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.5" /> <PackageReference Include="System.Data.SqlClient" Version="4.8.5" />
</ItemGroup> </ItemGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers> <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>5</AnalysisLevel> <AnalysisLevel>5</AnalysisLevel>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings> <ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@@ -3,7 +3,7 @@
<Router AppAssembly="@typeof(App).Assembly"> <Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData"> <Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> @*<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<Authorizing> <Authorizing>
Authorizing!! Authorizing!!
</Authorizing> </Authorizing>
@@ -17,7 +17,7 @@
<p role="alert">You are not authorized to access this resource.</p> <p role="alert">You are not authorized to access this resource.</p>
} }
</NotAuthorized> </NotAuthorized>
</AuthorizeRouteView> </AuthorizeRouteView>*@
<FocusOnNavigate RouteData="@routeData" Selector="h1" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found> </Found>
<NotFound> <NotFound>

View File

@@ -1,7 +1,10 @@
@page "/authentication/{action}" @page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication @using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@*<RemoteAuthenticatorView Action="@Action" />*@
<RemoteAuthenticatorView Action="@Action" />
@code{ @code{
[Parameter] public string? Action { get; set; } [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
} }

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Shogi.UI.Pages.Home.Api; using Shogi.UI.Pages.Home.Api;
using Shogi.UI.Shared; using Shogi.UI.Shared;
@@ -33,21 +34,21 @@ public class AccountManager
this.shogiSocket = shogiSocket; this.shogiSocket = shogiSocket;
} }
private User? User { get => accountState.User; set => accountState.User = value; } private User? MyUser { get => accountState.User; set => accountState.User = value; }
public async Task LoginWithGuestAccount() public async Task LoginWithGuestAccount()
{ {
var response = await shogiApi.GetToken(); var response = await shogiApi.GetToken();
if (response != null) if (response != null)
{ {
User = new User MyUser = new User
{ {
DisplayName = response.DisplayName, DisplayName = response.DisplayName,
Id = response.UserId, Id = response.UserId,
OneTimeSocketToken = response.OneTimeToken, OneTimeSocketToken = response.OneTimeToken,
WhichAccountPlatform = WhichAccountPlatform.Guest WhichAccountPlatform = WhichAccountPlatform.Guest
}; };
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString()); await shogiSocket.OpenAsync(MyUser.Value.OneTimeSocketToken.ToString());
await localStorage.SetAccountPlatform(WhichAccountPlatform.Guest); await localStorage.SetAccountPlatform(WhichAccountPlatform.Guest);
} }
} }
@@ -56,26 +57,28 @@ public class AccountManager
{ {
var state = await authState.GetAuthenticationStateAsync(); var state = await authState.GetAuthenticationStateAsync();
if (state.User?.Identity?.Name == null || state.User?.Identity?.IsAuthenticated != true) if (state.User?.Identity?.Name == null || state.User?.Identity?.IsAuthenticated == false)
{ {
navigation.NavigateTo("authentication/login"); // 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; return;
} }
var response = await shogiApi.GetToken(); //var response = await shogiApi.GetToken();
if (response != null) //if (response != null)
{ //{
User = new User // MyUser = new User
{ // {
DisplayName = response.DisplayName, // DisplayName = response.DisplayName,
Id = response.UserId, // Id = response.UserId,
OneTimeSocketToken = response.OneTimeToken, // OneTimeSocketToken = response.OneTimeToken,
WhichAccountPlatform = WhichAccountPlatform.Microsoft, // WhichAccountPlatform = WhichAccountPlatform.Microsoft,
}; // };
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString()); // await shogiSocket.OpenAsync(MyUser.Value.OneTimeSocketToken.ToString());
await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft); // await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft);
} //}
} }
/// <summary> /// <summary>
@@ -85,12 +88,13 @@ public class AccountManager
public async Task<bool> TryLoginSilentAsync() public async Task<bool> TryLoginSilentAsync()
{ {
var platform = await localStorage.GetAccountPlatform(); var platform = await localStorage.GetAccountPlatform();
Console.WriteLine($"Try Login Silent - {platform}");
if (platform == WhichAccountPlatform.Guest) if (platform == WhichAccountPlatform.Guest)
{ {
var response = await shogiApi.GetToken(); var response = await shogiApi.GetToken();
if (response != null) if (response != null)
{ {
User = new User MyUser = new User
{ {
DisplayName = response.DisplayName, DisplayName = response.DisplayName,
Id = response.UserId, Id = response.UserId,
@@ -101,29 +105,29 @@ public class AccountManager
} }
else if (platform == WhichAccountPlatform.Microsoft) else if (platform == WhichAccountPlatform.Microsoft)
{ {
Console.WriteLine("Login Microsoft"); var state = await authState.GetAuthenticationStateAsync();
throw new NotImplementedException(); if (state.User?.Identity?.Name != null)
//var state = await authState.GetAuthenticationStateAsync(); {
//if (state.User?.Identity?.Name != null) var response = await shogiApi.GetToken();
//{ if (response == null)
// var id = state.User.Identity; {
// User = new User // Login failed, so reset local storage to avoid putting the user in a broken state.
// { await localStorage.DeleteAccountPlatform();
// DisplayName = id.Name, return false;
// Id = id.Name }
// }; var id = state.User.Identity;
// var token = await shogiApi.GetToken(); MyUser = new User
// if (token.HasValue) {
// { DisplayName = id.Name,
// User.OneTimeSocketToken = token.Value; Id = id.Name,
// } OneTimeSocketToken = response.OneTimeToken
//} };
// TODO: If this fails then platform saved to localStorage should get cleared }
} }
if (User != null) if (MyUser != null)
{ {
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString()); await shogiSocket.OpenAsync(MyUser.Value.OneTimeSocketToken.ToString());
return true; return true;
} }
@@ -132,7 +136,16 @@ public class AccountManager
public async Task LogoutAsync() public async Task LogoutAsync()
{ {
await Task.WhenAll(shogiApi.GuestLogout(), localStorage.DeleteAccountPlatform()); MyUser = null;
User = 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");
}
} }
} }

View File

@@ -1,12 +1,10 @@
namespace Shogi.UI.Pages.Home.Account namespace Shogi.UI.Pages.Home.Account
{ {
public class User public readonly record struct User(
string Id,
string DisplayName,
WhichAccountPlatform WhichAccountPlatform,
Guid OneTimeSocketToken)
{ {
public string Id { get; set; }
public string DisplayName { get; set; }
public WhichAccountPlatform WhichAccountPlatform { get; set; }
public Guid OneTimeSocketToken { get; set; }
} }
} }

View File

@@ -11,7 +11,7 @@ namespace Shogi.UI.Pages.Home.Api
{ {
public const string GuestClientName = "Guest"; public const string GuestClientName = "Guest";
public const string MsalClientName = "Msal"; public const string MsalClientName = "Msal";
public const string AnonymouseClientName = "Anonymous"; //public const string AnonymousClientName = "Anonymous";
private readonly JsonSerializerOptions serializerOptions; private readonly JsonSerializerOptions serializerOptions;
private readonly IHttpClientFactory clientFactory; private readonly IHttpClientFactory clientFactory;
@@ -28,7 +28,7 @@ namespace Shogi.UI.Pages.Home.Api
{ {
WhichAccountPlatform.Guest => clientFactory.CreateClient(GuestClientName), WhichAccountPlatform.Guest => clientFactory.CreateClient(GuestClientName),
WhichAccountPlatform.Microsoft => clientFactory.CreateClient(MsalClientName), WhichAccountPlatform.Microsoft => clientFactory.CreateClient(MsalClientName),
_ => clientFactory.CreateClient(AnonymouseClientName) _ => clientFactory.CreateClient(GuestClientName)
}; };
public async Task GuestLogout() public async Task GuestLogout()
@@ -59,6 +59,7 @@ namespace Shogi.UI.Pages.Home.Api
public async Task<CreateTokenResponse?> GetToken() public async Task<CreateTokenResponse?> GetToken()
{ {
Console.WriteLine($"GetToken() - {accountState.User?.WhichAccountPlatform}");
var response = await HttpClient.GetFromJsonAsync<CreateTokenResponse>(new Uri("User/Token", UriKind.Relative), serializerOptions); var response = await HttpClient.GetFromJsonAsync<CreateTokenResponse>(new Uri("User/Token", UriKind.Relative), serializerOptions);
return response; return response;
} }

View File

@@ -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;
<article class="game-board">
<!-- Game board -->
<section class="board" data-perspective="@Perspective">
@for (var rank = 1; rank < 10; rank++)
{
foreach (var file in Files)
{
var position = $"{file}{rank}";
var piece = session?.BoardState.Board[position];
<div class="tile"
data-position="@(position)"
data-selected="@(piece != null && selectedPosition == position)"
style="grid-area: @(position)"
@onclick="() => OnClickTile(piece, position)">
<GamePiece Piece="piece" Perspective="Perspective" />
</div>
}
}
<div class="ruler vertical" style="grid-area: rank">
<span>9</span>
<span>8</span>
<span>7</span>
<span>6</span>
<span>5</span>
<span>4</span>
<span>3</span>
<span>2</span>
<span>1</span>
</div>
<div class="ruler" style="grid-area: file">
<span>A</span>
<span>B</span>
<span>C</span>
<span>D</span>
<span>E</span>
<span>F</span>
<span>G</span>
<span>H</span>
<span>I</span>
</div>
<!-- Promote prompt -->
<div class="promote-prompt" data-visible="@PromotePrompt.IsVisible">
<p>Do you wish to promote?</p>
<div>
<button type="button">Yes</button>
<button type="button">No</button>
<button type="button">Cancel</button>
</div>
</div>
</section>
<!-- Side board -->
@if (session != null)
{
<aside class="side-board">
<div class="hand">
@foreach (var piece in OpponentHand)
{
<div class="tile">
<GamePiece Piece="piece" Perspective="Perspective" />
</div>
}
</div>
<div class="spacer" />
<div class="hand">
@foreach (var piece in UserHand)
{
<div class="title" @onclick="() => OnClickHand(piece)">
<GamePiece Piece="piece" Perspective="Perspective" />
</div>
}
</div>
</aside>
}
</article>
@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<Piece> OpponentHand
{
get
{
if (this.session == null) return Array.Empty<Piece>();
return Perspective == WhichPlayer.Player1
? this.session.BoardState.Player1Hand
: this.session.BoardState.Player2Hand;
}
}
IReadOnlyCollection<Piece> UserHand
{
get
{
if (this.session == null) return Array.Empty<Piece>();
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;
}
}

View File

@@ -0,0 +1,11 @@
@using Contracts.Types;
<GameBoardPresentation Perspective="WhichPlayer.Player1" />
@code {
protected override void OnInitialized()
{
base.OnInitialized();
Console.WriteLine("Empty Game Board.");
}
}

View File

@@ -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)
{
<EmptyGameBoard />
}
else if (isSpectating)
{
<SpectatorGameBoard Session="session" />
}
else
{
<SeatedGameBoard Perspective="perspective" Session="session" />
}
@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}");
}
}
}
}

View File

@@ -0,0 +1,116 @@
@using Shogi.Contracts.Types;
@inject PromotePrompt PromotePrompt;
<article class="game-board">
<!-- Game board -->
<section class="board" data-perspective="@Perspective">
@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;
<div class="tile" @onclick="OnClickTileInternal(piece, position)"
data-position="@(position)"
data-selected="@(isSelected)"
style="grid-area: @position">
<GamePiece Piece="piece" Perspective="Perspective" />
</div>
}
}
<div class="ruler vertical" style="grid-area: rank">
<span>9</span>
<span>8</span>
<span>7</span>
<span>6</span>
<span>5</span>
<span>4</span>
<span>3</span>
<span>2</span>
<span>1</span>
</div>
<div class="ruler" style="grid-area: file">
<span>A</span>
<span>B</span>
<span>C</span>
<span>D</span>
<span>E</span>
<span>F</span>
<span>G</span>
<span>H</span>
<span>I</span>
</div>
<!-- Promote prompt -->
<div class="promote-prompt" data-visible="@PromotePrompt.IsVisible">
<p>Do you wish to promote?</p>
<div>
<button type="button">Yes</button>
<button type="button">No</button>
<button type="button">Cancel</button>
</div>
</div>
</section>
<!-- Side board -->
@if (Session != null)
{
<aside class="side-board">
<div class="hand">
@foreach (var piece in OpponentHand)
{
<div class="tile">
<GamePiece Piece="piece" Perspective="Perspective" />
</div>
}
</div>
<div class="spacer" />
<div class="hand">
@foreach (var piece in UserHand)
{
<div class="title" @onclick="OnClickHandInternal(piece)">
<GamePiece Piece="piece" Perspective="Perspective" />
</div>
}
</div>
</aside>
}
</article>
@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<Piece?, string>? OnClickTile { get; set; }
[Parameter] public Action<Piece>? OnClickHand { get; set; }
static readonly string[] Files = new[] { "A", "B", "C", "D", "E", "F", "G", "H", "I" };
private IReadOnlyCollection<Piece> OpponentHand
{
get
{
if (this.Session == null) return Array.Empty<Piece>();
return Perspective == WhichPlayer.Player1
? this.Session.BoardState.Player1Hand
: this.Session.BoardState.Player2Hand;
}
}
IReadOnlyCollection<Piece> UserHand
{
get
{
if (this.Session == null) return Array.Empty<Piece>();
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);
}

View File

@@ -0,0 +1,71 @@
@using Shogi.Contracts.Api;
@using Shogi.Contracts.Types;
@using System.Text.RegularExpressions;
@inject PromotePrompt PromotePrompt;
@inject IShogiApi ShogiApi;
<GameBoardPresentation OnClickHand="OnClickHand" OnClickTile="OnClickTile" Session="Session" Perspective="Perspective" />
@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;
}
}

View File

@@ -0,0 +1,8 @@
@using Contracts.Types;
<p>You are spectating</p>
<GameBoardPresentation Perspective="WhichPlayer.Player1" Session="Session" />
@code {
[Parameter] public Session Session { get; set; }
}

View File

@@ -1,6 +1,7 @@
@using Shogi.Contracts.Types @using Shogi.Contracts.Types;
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations;
@using System.Net @using System.Net;
@using System.Text.Json;
@inject IShogiApi ShogiApi; @inject IShogiApi ShogiApi;
@inject ShogiSocket ShogiSocket; @inject ShogiSocket ShogiSocket;
@inject AccountState Account; @inject AccountState Account;
@@ -17,12 +18,27 @@
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane fade show active" id="search-pane"> <div class="tab-pane fade show active" id="search-pane">
<h3>Games you&apos;re seated at</h3>
<div class="list-group"> <div class="list-group">
@if (!sessions.Any()) @if (!joinedSessions.Any())
{ {
<p>No games exist</p> <p>You have not joined any games.</p>
} }
@foreach (var session in sessions) @foreach (var session in joinedSessions)
{
<button class="list-group-item list-group-item-action @ActiveCss(session)" @onclick="() => OnClickSession(session)">
<span>@session.Name</span>
<span>(@session.PlayerCount/2)</span>
</button>
}
</div>
<h3>Other games</h3>
<div class="list-group">
@if (!otherSessions.Any())
{
<p>You have not joined any games.</p>
}
@foreach (var session in otherSessions)
{ {
<button class="list-group-item list-group-item-action @ActiveCss(session)" @onclick="() => OnClickSession(session)"> <button class="list-group-item list-group-item-action @ActiveCss(session)" @onclick="() => OnClickSession(session)">
<span>@session.Name</span> <span>@session.Name</span>
@@ -69,16 +85,19 @@
@code { @code {
[Parameter] [Parameter]
public Action<SessionMetadata>? ActiveSessionChanged { get; set; } public Action<SessionMetadata>? ActiveSessionChanged { get; set; }
CreateForm createForm = new();
SessionMetadata[] sessions = Array.Empty<SessionMetadata>(); private CreateForm createForm = new();
SessionMetadata? activeSession; private SessionMetadata[] joinedSessions = Array.Empty<SessionMetadata>();
HttpStatusCode? createSessionStatusCode; private SessionMetadata[] otherSessions = Array.Empty<SessionMetadata>();
private SessionMetadata? activeSession;
private HttpStatusCode? createSessionStatusCode;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
ShogiSocket.OnCreateGameMessage += async (sender, message) => await FetchSessions(); ShogiSocket.OnCreateGameMessage += async (sender, message) => await FetchSessions();
Account.LoginChangedEvent += async (sender, message) => Account.LoginChangedEvent += async (sender, message) =>
{ {
Console.WriteLine($"LoginEvent. Message={JsonSerializer.Serialize(message)}.");
if (message.User != null) if (message.User != null)
{ {
await FetchSessions(); await FetchSessions();
@@ -99,7 +118,8 @@
var sessions = await ShogiApi.GetSessionsPlayerCount(); var sessions = await ShogiApi.GetSessionsPlayerCount();
if (sessions != null) if (sessions != null)
{ {
this.sessions = sessions.PlayerHasJoinedSessions.Concat(sessions.AllOtherSessions).ToArray(); this.joinedSessions = sessions.PlayerHasJoinedSessions.ToArray();
this.otherSessions = sessions.AllOtherSessions.ToArray();
StateHasChanged(); StateHasChanged();
} }
} }

View File

@@ -15,7 +15,14 @@
} }
<PageHeader /> <PageHeader />
<GameBrowser ActiveSessionChanged="OnChangeSession" /> <GameBrowser ActiveSessionChanged="OnChangeSession" />
@if (Account.User == null || activeSessionName == null)
{
<EmptyGameBoard />
}
else
{
<GameBoard SessionName="@activeSessionName" /> <GameBoard SessionName="@activeSessionName" />
}
</main> </main>
@code { @code {

View File

@@ -6,7 +6,7 @@
@if (user != null) @if (user != null)
{ {
<div class="user"> <div class="user">
<div>@user.DisplayName</div> <div>@user.Value.DisplayName</div>
<button type="button" class="logout" @onclick="AccountManager.LogoutAsync">Logout</button> <button type="button" class="logout" @onclick="AccountManager.LogoutAsync">Logout</button>
</div> </div>
} }

View File

@@ -28,14 +28,15 @@ static void ConfigureDependencies(IServiceCollection services, IConfiguration co
services services
.AddHttpClient(ShogiApi.GuestClientName, client => client.BaseAddress = shogiApiUrl) .AddHttpClient(ShogiApi.GuestClientName, client => client.BaseAddress = shogiApiUrl)
.AddHttpMessageHandler<CookieCredentialsMessageHandler>(); .AddHttpMessageHandler<CookieCredentialsMessageHandler>();
services //services
.AddHttpClient(ShogiApi.AnonymouseClientName, client => client.BaseAddress = shogiApiUrl) // .AddHttpClient(ShogiApi.AnonymousClientName, client => client.BaseAddress = shogiApiUrl)
.AddHttpMessageHandler<CookieCredentialsMessageHandler>(); // .AddHttpMessageHandler<CookieCredentialsMessageHandler>();
// Authorization // Authorization
services.AddMsalAuthentication(options => services.AddMsalAuthentication(options =>
{ {
configuration.Bind("AzureAd", options.ProviderOptions.Authentication); configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.LoginMode = "redirect";
}); });
services.AddOidcAuthentication(options => services.AddOidcAuthentication(options =>
{ {

View File

@@ -54,6 +54,7 @@ public class ShogiSocket : IDisposable
switch (action) switch (action)
{ {
case SocketAction.SessionCreated: case SocketAction.SessionCreated:
Console.WriteLine("Session created event.");
this.OnCreateGameMessage?.Invoke(this, JsonSerializer.Deserialize<SessionCreatedSocketMessage>(memory, this.serializerOptions)!); this.OnCreateGameMessage?.Invoke(this, JsonSerializer.Deserialize<SessionCreatedSocketMessage>(memory, this.serializerOptions)!);
break; break;
default: default:

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
@@ -14,12 +14,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.8" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="6.0.8" /> <PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="7.0.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -10,6 +10,7 @@
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Shogi.UI.Pages.Home.Account @using Shogi.UI.Pages.Home.Account
@using Shogi.UI.Pages.Home.Api @using Shogi.UI.Pages.Home.Api
@using Shogi.UI.Pages.Home.GameBoard
@using Shogi.UI.Pages.Home.Pieces @using Shogi.UI.Pages.Home.Pieces
@using Shogi.UI.Shared.Modal @using Shogi.UI.Shared.Modal
@using Shogi.UI.Shared @using Shogi.UI.Shared

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@@ -23,11 +23,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.9.0" /> <PackageReference Include="FluentAssertions" Version="6.9.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.48.0" /> <PackageReference Include="Microsoft.Identity.Client" Version="4.49.1" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="xunit" Version="2.4.2" />

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>

View File

@@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "6.0.300", "version": "7.0.2",
"rollForward": "latestFeature" "rollForward": "latestFeature"
} }
} }