reintroduce microsoft login. upgrade a bunch of stuff.
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -7,41 +7,46 @@ namespace Shogi.Api.Repositories;
|
|||||||
|
|
||||||
public class UserRepository : IUserRepository
|
public class UserRepository : IUserRepository
|
||||||
{
|
{
|
||||||
private readonly string connectionString;
|
private readonly string connectionString;
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
using var connection = new SqlConnection(connectionString);
|
using var connection = new SqlConnection(connectionString);
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
"user.CreateUser",
|
"user.CreateUser",
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Name = user.Id,
|
Name = user.Id,
|
||||||
DisplayName = user.DisplayName,
|
DisplayName = user.DisplayName,
|
||||||
Platform = user.LoginPlatform.ToString()
|
Platform = user.LoginPlatform.ToString()
|
||||||
},
|
},
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<User?> ReadUser(string id)
|
public async Task<User?> ReadUser(string id)
|
||||||
{
|
{
|
||||||
using var connection = new SqlConnection(connectionString);
|
using var connection = new SqlConnection(connectionString);
|
||||||
var results = await connection.QueryAsync<User>(
|
var results = await connection.QueryAsync<User>(
|
||||||
"user.ReadUser",
|
"user.ReadUser",
|
||||||
new { Name = id },
|
new { Name = id },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
return results.FirstOrDefault();
|
return results.FirstOrDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IUserRepository
|
public interface IUserRepository
|
||||||
{
|
{
|
||||||
Task CreateUser(User user);
|
Task CreateUser(User user);
|
||||||
Task<User?> ReadUser(string id);
|
Task<User?> ReadUser(string id);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -7,132 +8,144 @@ namespace Shogi.UI.Pages.Home.Account;
|
|||||||
|
|
||||||
public class AccountManager
|
public class AccountManager
|
||||||
{
|
{
|
||||||
private readonly AccountState accountState;
|
private readonly AccountState accountState;
|
||||||
private readonly IShogiApi shogiApi;
|
private readonly IShogiApi shogiApi;
|
||||||
private readonly IConfiguration configuration;
|
private readonly IConfiguration configuration;
|
||||||
private readonly ILocalStorage localStorage;
|
private readonly ILocalStorage localStorage;
|
||||||
private readonly AuthenticationStateProvider authState;
|
private readonly AuthenticationStateProvider authState;
|
||||||
private readonly NavigationManager navigation;
|
private readonly NavigationManager navigation;
|
||||||
private readonly ShogiSocket shogiSocket;
|
private readonly ShogiSocket shogiSocket;
|
||||||
|
|
||||||
public AccountManager(
|
public AccountManager(
|
||||||
AccountState accountState,
|
AccountState accountState,
|
||||||
IShogiApi unauthenticatedClient,
|
IShogiApi unauthenticatedClient,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
AuthenticationStateProvider authState,
|
AuthenticationStateProvider authState,
|
||||||
ILocalStorage localStorage,
|
ILocalStorage localStorage,
|
||||||
NavigationManager navigation,
|
NavigationManager navigation,
|
||||||
ShogiSocket shogiSocket)
|
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)
|
|
||||||
{
|
{
|
||||||
User = new User
|
this.accountState = accountState;
|
||||||
{
|
this.shogiApi = unauthenticatedClient;
|
||||||
DisplayName = response.DisplayName,
|
this.configuration = configuration;
|
||||||
Id = response.UserId,
|
this.authState = authState;
|
||||||
OneTimeSocketToken = response.OneTimeToken,
|
this.localStorage = localStorage;
|
||||||
WhichAccountPlatform = WhichAccountPlatform.Guest
|
this.navigation = navigation;
|
||||||
};
|
this.shogiSocket = shogiSocket;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await shogiApi.GetToken();
|
private User? MyUser { get => accountState.User; set => accountState.User = value; }
|
||||||
if (response != null)
|
|
||||||
{
|
|
||||||
User = new User
|
|
||||||
{
|
|
||||||
DisplayName = response.DisplayName,
|
|
||||||
Id = response.UserId,
|
|
||||||
OneTimeSocketToken = response.OneTimeToken,
|
|
||||||
WhichAccountPlatform = WhichAccountPlatform.Microsoft,
|
|
||||||
};
|
|
||||||
|
|
||||||
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString());
|
public async Task LoginWithGuestAccount()
|
||||||
await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Try to log in with the account used from the previous browser session.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<bool> TryLoginSilentAsync()
|
|
||||||
{
|
|
||||||
var platform = await localStorage.GetAccountPlatform();
|
|
||||||
if (platform == WhichAccountPlatform.Guest)
|
|
||||||
{
|
{
|
||||||
var response = await shogiApi.GetToken();
|
var response = await shogiApi.GetToken();
|
||||||
if (response != null)
|
if (response != null)
|
||||||
{
|
|
||||||
User = new User
|
|
||||||
{
|
{
|
||||||
DisplayName = response.DisplayName,
|
MyUser = new User
|
||||||
Id = response.UserId,
|
{
|
||||||
OneTimeSocketToken = response.OneTimeToken,
|
DisplayName = response.DisplayName,
|
||||||
WhichAccountPlatform = WhichAccountPlatform.Guest
|
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");
|
var state = await authState.GetAuthenticationStateAsync();
|
||||||
throw new NotImplementedException();
|
|
||||||
//var state = await authState.GetAuthenticationStateAsync();
|
if (state.User?.Identity?.Name == null || state.User?.Identity?.IsAuthenticated == false)
|
||||||
//if (state.User?.Identity?.Name != null)
|
{
|
||||||
//{
|
// Set the login platform so that we know to log in with microsoft after being redirected away from the UI.
|
||||||
// var id = state.User.Identity;
|
await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft);
|
||||||
// User = new User
|
navigation.NavigateToLogin("authentication/login");
|
||||||
// {
|
return;
|
||||||
// DisplayName = id.Name,
|
}
|
||||||
// Id = id.Name
|
|
||||||
// };
|
//var response = await shogiApi.GetToken();
|
||||||
// var token = await shogiApi.GetToken();
|
//if (response != null)
|
||||||
// if (token.HasValue)
|
//{
|
||||||
// {
|
// MyUser = new User
|
||||||
// User.OneTimeSocketToken = token.Value;
|
// {
|
||||||
// }
|
// DisplayName = response.DisplayName,
|
||||||
//}
|
// Id = response.UserId,
|
||||||
// TODO: If this fails then platform saved to localStorage should get cleared
|
// OneTimeSocketToken = response.OneTimeToken,
|
||||||
|
// WhichAccountPlatform = WhichAccountPlatform.Microsoft,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// await shogiSocket.OpenAsync(MyUser.Value.OneTimeSocketToken.ToString());
|
||||||
|
// await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft);
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (User != null)
|
/// <summary>
|
||||||
|
/// Try to log in with the account used from the previous browser session.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> TryLoginSilentAsync()
|
||||||
{
|
{
|
||||||
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString());
|
var platform = await localStorage.GetAccountPlatform();
|
||||||
return true;
|
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()
|
||||||
}
|
{
|
||||||
|
MyUser = null;
|
||||||
|
var platform = await localStorage.GetAccountPlatform();
|
||||||
|
await localStorage.DeleteAccountPlatform();
|
||||||
|
|
||||||
public async Task LogoutAsync()
|
if (platform == WhichAccountPlatform.Guest)
|
||||||
{
|
{
|
||||||
await Task.WhenAll(shogiApi.GuestLogout(), localStorage.DeleteAccountPlatform());
|
await shogiApi.GuestLogout();
|
||||||
User = null;
|
} else if (platform == WhichAccountPlatform.Microsoft)
|
||||||
}
|
{
|
||||||
|
navigation.NavigateToLogout("authentication/logout");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,27 @@
|
|||||||
|
|
||||||
public class AccountState
|
public class AccountState
|
||||||
{
|
{
|
||||||
public event EventHandler<LoginEventArgs>? LoginChangedEvent;
|
public event EventHandler<LoginEventArgs>? LoginChangedEvent;
|
||||||
|
|
||||||
private User? user;
|
private User? user;
|
||||||
public User? User
|
public User? User
|
||||||
{
|
{
|
||||||
get => user;
|
get => user;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (user != value)
|
if (user != value)
|
||||||
{
|
{
|
||||||
user = value;
|
user = value;
|
||||||
EmitLoginChangedEvent();
|
EmitLoginChangedEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EmitLoginChangedEvent()
|
private void EmitLoginChangedEvent()
|
||||||
{
|
{
|
||||||
LoginChangedEvent?.Invoke(this, new LoginEventArgs
|
LoginChangedEvent?.Invoke(this, new LoginEventArgs
|
||||||
{
|
{
|
||||||
User = User
|
User = User
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
public string Id { get; set; }
|
string DisplayName,
|
||||||
public string DisplayName { get; set; }
|
WhichAccountPlatform WhichAccountPlatform,
|
||||||
|
Guid OneTimeSocketToken)
|
||||||
public WhichAccountPlatform WhichAccountPlatform { get; set; }
|
{
|
||||||
|
}
|
||||||
public Guid OneTimeSocketToken { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,74 +7,75 @@ using System.Text.Json;
|
|||||||
|
|
||||||
namespace Shogi.UI.Pages.Home.Api
|
namespace Shogi.UI.Pages.Home.Api
|
||||||
{
|
{
|
||||||
public class ShogiApi : IShogiApi
|
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)
|
|
||||||
{
|
{
|
||||||
serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
public const string GuestClientName = "Guest";
|
||||||
this.clientFactory = clientFactory;
|
public const string MsalClientName = "Msal";
|
||||||
this.accountState = accountState;
|
//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<Session?> GetSession(string name)
|
||||||
|
{
|
||||||
|
var response = await HttpClient.GetAsync(new Uri($"Sessions/{name}", UriKind.Relative));
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return (await response.Content.ReadFromJsonAsync<ReadSessionResponse>(serializerOptions))?.Session;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ReadSessionsPlayerCountResponse?> GetSessionsPlayerCount()
|
||||||
|
{
|
||||||
|
var response = await HttpClient.GetAsync(new Uri("Sessions/PlayerCount", UriKind.Relative));
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return await response.Content.ReadFromJsonAsync<ReadSessionsPlayerCountResponse>(serializerOptions);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CreateTokenResponse?> GetToken()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"GetToken() - {accountState.User?.WhichAccountPlatform}");
|
||||||
|
var response = await HttpClient.GetFromJsonAsync<CreateTokenResponse>(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<HttpStatusCode> 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<Session?> GetSession(string name)
|
|
||||||
{
|
|
||||||
var response = await HttpClient.GetAsync(new Uri($"Sessions/{name}", UriKind.Relative));
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return (await response.Content.ReadFromJsonAsync<ReadSessionResponse>(serializerOptions))?.Session;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ReadSessionsPlayerCountResponse?> GetSessionsPlayerCount()
|
|
||||||
{
|
|
||||||
var response = await HttpClient.GetAsync(new Uri("Sessions/PlayerCount", UriKind.Relative));
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return await response.Content.ReadFromJsonAsync<ReadSessionsPlayerCountResponse>(serializerOptions);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CreateTokenResponse?> GetToken()
|
|
||||||
{
|
|
||||||
var response = await HttpClient.GetFromJsonAsync<CreateTokenResponse>(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<HttpStatusCode> PostSession(string name, bool isPrivate)
|
|
||||||
{
|
|
||||||
var response = await HttpClient.PostAsJsonAsync(new Uri("Sessions", UriKind.Relative), new CreateSessionCommand
|
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
});
|
|
||||||
return response.StatusCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
Shogi.UI/Pages/Home/GameBoard/EmptyGameBoard.razor
Normal file
11
Shogi.UI/Pages/Home/GameBoard/EmptyGameBoard.razor
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
@using Contracts.Types;
|
||||||
|
|
||||||
|
<GameBoardPresentation Perspective="WhichPlayer.Player1" />
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
base.OnInitialized();
|
||||||
|
Console.WriteLine("Empty Game Board.");
|
||||||
|
}
|
||||||
|
}
|
||||||
45
Shogi.UI/Pages/Home/GameBoard/GameBoard.razor
Normal file
45
Shogi.UI/Pages/Home/GameBoard/GameBoard.razor
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
116
Shogi.UI/Pages/Home/GameBoard/GameBoardPresentation.razor
Normal file
116
Shogi.UI/Pages/Home/GameBoard/GameBoardPresentation.razor
Normal 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);
|
||||||
|
}
|
||||||
71
Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor
Normal file
71
Shogi.UI/Pages/Home/GameBoard/SeatedGameBoard.razor
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor
Normal file
8
Shogi.UI/Pages/Home/GameBoard/SpectatorGameBoard.razor
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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'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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,14 @@
|
|||||||
}
|
}
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
<GameBrowser ActiveSessionChanged="OnChangeSession" />
|
<GameBrowser ActiveSessionChanged="OnChangeSession" />
|
||||||
<GameBoard SessionName="@activeSessionName" />
|
@if (Account.User == null || activeSessionName == null)
|
||||||
|
{
|
||||||
|
<EmptyGameBoard />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<GameBoard SessionName="@activeSessionName" />
|
||||||
|
}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "6.0.300",
|
"version": "7.0.2",
|
||||||
"rollForward": "latestFeature"
|
"rollForward": "latestFeature"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user