Merge branch 'master' of https://bitbucket.org/Hauth/shogi
This commit is contained in:
@@ -9,85 +9,89 @@ namespace Shogi.Api.Repositories;
|
|||||||
|
|
||||||
public class SessionRepository : ISessionRepository
|
public class SessionRepository : ISessionRepository
|
||||||
{
|
{
|
||||||
private readonly string connectionString;
|
private readonly string connectionString;
|
||||||
|
|
||||||
public SessionRepository(IConfiguration configuration)
|
public SessionRepository(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
connectionString = configuration.GetConnectionString("ShogiDatabase");
|
connectionString = configuration.GetConnectionString("ShogiDatabase");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateSession(Session session)
|
public async Task CreateSession(Session session)
|
||||||
{
|
{
|
||||||
using var connection = new SqlConnection(connectionString);
|
using var connection = new SqlConnection(connectionString);
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
"session.CreateSession",
|
"session.CreateSession",
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
session.Name,
|
session.Name,
|
||||||
Player1Name = session.Player1,
|
Player1Name = session.Player1,
|
||||||
},
|
},
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteSession(string name)
|
public async Task DeleteSession(string name)
|
||||||
{
|
{
|
||||||
using var connection = new SqlConnection(connectionString);
|
using var connection = new SqlConnection(connectionString);
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
"session.DeleteSession",
|
"session.DeleteSession",
|
||||||
new { Name = name },
|
new { Name = name },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Session?> ReadSession(string name)
|
public async Task<Session?> ReadSession(string name)
|
||||||
{
|
{
|
||||||
using var connection = new SqlConnection(connectionString);
|
using var connection = new SqlConnection(connectionString);
|
||||||
var results = await connection.QueryMultipleAsync(
|
var results = await connection.QueryMultipleAsync(
|
||||||
"session.ReadSession",
|
"session.ReadSession",
|
||||||
new { Name = name },
|
new { Name = name },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
var sessionDtos = await results.ReadAsync<SessionDto>();
|
var sessionDtos = await results.ReadAsync<SessionDto>();
|
||||||
if (!sessionDtos.Any()) return null;
|
if (!sessionDtos.Any()) return null;
|
||||||
var dto = sessionDtos.First();
|
var dto = sessionDtos.First();
|
||||||
var session = new Session(dto.Name, dto.Player1);
|
var session = new Session(dto.Name, dto.Player1);
|
||||||
if (!string.IsNullOrWhiteSpace(dto.Player2)) session.AddPlayer2(dto.Player2);
|
if (!string.IsNullOrWhiteSpace(dto.Player2)) session.AddPlayer2(dto.Player2);
|
||||||
|
|
||||||
var moveDtos = await results.ReadAsync<MoveDto>();
|
var moveDtos = await results.ReadAsync<MoveDto>();
|
||||||
foreach (var move in moveDtos)
|
foreach (var move in moveDtos)
|
||||||
{
|
{
|
||||||
if (move.PieceFromHand.HasValue)
|
if (move.PieceFromHand.HasValue)
|
||||||
{
|
{
|
||||||
session.Board.Move(move.PieceFromHand.Value, move.To);
|
session.Board.Move(move.PieceFromHand.Value, move.To);
|
||||||
}
|
}
|
||||||
else
|
else if (move.From != null)
|
||||||
{
|
{
|
||||||
session.Board.Move(move.From, move.To, false);
|
session.Board.Move(move.From, move.To, false);
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
return session;
|
{
|
||||||
}
|
throw new InvalidOperationException($"Corrupt data during {nameof(ReadSession)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task CreateMove(string sessionName, Contracts.Api.MovePieceCommand command)
|
public async Task CreateMove(string sessionName, Contracts.Api.MovePieceCommand command)
|
||||||
{
|
{
|
||||||
using var connection = new SqlConnection(connectionString);
|
using var connection = new SqlConnection(connectionString);
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
"session.CreateMove",
|
"session.CreateMove",
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
command.To,
|
command.To,
|
||||||
command.From,
|
command.From,
|
||||||
command.IsPromotion,
|
command.IsPromotion,
|
||||||
command.PieceFromHand,
|
command.PieceFromHand,
|
||||||
SessionName = sessionName
|
SessionName = sessionName
|
||||||
},
|
},
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ISessionRepository
|
public interface ISessionRepository
|
||||||
{
|
{
|
||||||
Task CreateMove(string sessionName, MovePieceCommand command);
|
Task CreateMove(string sessionName, MovePieceCommand command);
|
||||||
Task CreateSession(Session session);
|
Task CreateSession(Session session);
|
||||||
Task DeleteSession(string name);
|
Task DeleteSession(string name);
|
||||||
Task<Session?> ReadSession(string name);
|
Task<Session?> ReadSession(string name);
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ public class ShogiUserClaimsTransformer : IShogiUserClaimsTransformer
|
|||||||
|
|
||||||
private async Task<ClaimsPrincipal> CreateClaimsFromMicrosoftPrincipal(ClaimsPrincipal principal)
|
private async Task<ClaimsPrincipal> CreateClaimsFromMicrosoftPrincipal(ClaimsPrincipal principal)
|
||||||
{
|
{
|
||||||
var id = principal.GetMsalAccountId();
|
var id = principal.GetMicrosoftUserId();
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
{
|
{
|
||||||
throw new UnauthorizedAccessException("Found MSAL claims but no preferred_username.");
|
throw new UnauthorizedAccessException("Found MSAL claims but no preferred_username.");
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ BEGIN
|
|||||||
piece.[Name] as PieceFromHand
|
piece.[Name] as PieceFromHand
|
||||||
FROM [session].[Move] mv
|
FROM [session].[Move] mv
|
||||||
INNER JOIN [session].[Session] sess ON sess.Id = mv.SessionId
|
INNER JOIN [session].[Session] sess ON sess.Id = mv.SessionId
|
||||||
RIGHT JOIN [session].Piece piece on piece.Id = mv.PieceIdFromHand
|
LEFT JOIN [session].Piece piece on piece.Id = mv.PieceIdFromHand
|
||||||
WHERE sess.[Name] = @Name;
|
WHERE sess.[Name] = @Name;
|
||||||
|
|
||||||
COMMIT
|
COMMIT
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
@*<CascadingAuthenticationState>*@
|
<CascadingAuthenticationState>
|
||||||
|
|
||||||
<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>
|
||||||
@@ -28,4 +28,4 @@
|
|||||||
</NotFound>
|
</NotFound>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
||||||
@*</CascadingAuthenticationState>*@
|
</CascadingAuthenticationState>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Shogi.UI.Pages.Home.Api;
|
using Shogi.UI.Pages.Home.Api;
|
||||||
using Shogi.UI.Shared;
|
using Shogi.UI.Shared;
|
||||||
|
|
||||||
@@ -6,133 +7,132 @@ 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.accountState = accountState;
|
||||||
this.shogiApi = unauthenticatedClient;
|
this.shogiApi = unauthenticatedClient;
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
//this.authState = authState;
|
this.authState = authState;
|
||||||
this.localStorage = localStorage;
|
this.localStorage = localStorage;
|
||||||
this.navigation = navigation;
|
this.navigation = navigation;
|
||||||
this.shogiSocket = shogiSocket;
|
this.shogiSocket = shogiSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
private User? User { get => accountState.User; set => accountState.User = value; }
|
private User? User { 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
|
User = 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(User.OneTimeSocketToken.ToString());
|
||||||
await localStorage.SetAccountPlatform(WhichAccountPlatform.Guest);
|
await localStorage.SetAccountPlatform(WhichAccountPlatform.Guest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoginWithMicrosoftAccount()
|
public async Task LoginWithMicrosoftAccount()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
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 != true)
|
||||||
//{
|
{
|
||||||
// navigation.NavigateTo("authentication/login");
|
navigation.NavigateTo("authentication/login");
|
||||||
// return;
|
return;
|
||||||
//}
|
}
|
||||||
|
|
||||||
//var id = state.User.Identity.Name;
|
var response = await shogiApi.GetToken();
|
||||||
//var socketToken = await shogiApi.GetToken();
|
if (response != null)
|
||||||
//if (socketToken.HasValue)
|
{
|
||||||
//{
|
User = new User
|
||||||
// User = new User
|
{
|
||||||
// {
|
DisplayName = response.DisplayName,
|
||||||
// DisplayName = id,
|
Id = response.UserId,
|
||||||
// Id = id,
|
OneTimeSocketToken = response.OneTimeToken,
|
||||||
// OneTimeSocketToken = socketToken.Value
|
WhichAccountPlatform = WhichAccountPlatform.Microsoft,
|
||||||
// };
|
};
|
||||||
|
|
||||||
// await ConnectToSocketAsync();
|
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString());
|
||||||
// await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft);
|
await localStorage.SetAccountPlatform(WhichAccountPlatform.Microsoft);
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try to log in with the account used from the previous browser session.
|
/// Try to log in with the account used from the previous browser session.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<bool> TryLoginSilentAsync()
|
public async Task<bool> TryLoginSilentAsync()
|
||||||
{
|
{
|
||||||
var platform = await localStorage.GetAccountPlatform();
|
var platform = await localStorage.GetAccountPlatform();
|
||||||
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
|
User = 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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (platform == WhichAccountPlatform.Microsoft)
|
else if (platform == WhichAccountPlatform.Microsoft)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Login Microsoft");
|
Console.WriteLine("Login Microsoft");
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
//var state = await authState.GetAuthenticationStateAsync();
|
//var state = await authState.GetAuthenticationStateAsync();
|
||||||
//if (state.User?.Identity?.Name != null)
|
//if (state.User?.Identity?.Name != null)
|
||||||
//{
|
//{
|
||||||
// var id = state.User.Identity;
|
// var id = state.User.Identity;
|
||||||
// User = new User
|
// User = new User
|
||||||
// {
|
// {
|
||||||
// DisplayName = id.Name,
|
// DisplayName = id.Name,
|
||||||
// Id = id.Name
|
// Id = id.Name
|
||||||
// };
|
// };
|
||||||
// var token = await shogiApi.GetToken();
|
// var token = await shogiApi.GetToken();
|
||||||
// if (token.HasValue)
|
// if (token.HasValue)
|
||||||
// {
|
// {
|
||||||
// User.OneTimeSocketToken = token.Value;
|
// User.OneTimeSocketToken = token.Value;
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
// TODO: If this fails then platform saved to localStorage should get cleared
|
// TODO: If this fails then platform saved to localStorage should get cleared
|
||||||
}
|
}
|
||||||
|
|
||||||
if (User != null)
|
if (User != null)
|
||||||
{
|
{
|
||||||
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString());
|
await shogiSocket.OpenAsync(User.OneTimeSocketToken.ToString());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LogoutAsync()
|
public async Task LogoutAsync()
|
||||||
{
|
{
|
||||||
await Task.WhenAll(shogiApi.GuestLogout(), localStorage.DeleteAccountPlatform());
|
await Task.WhenAll(shogiApi.GuestLogout(), localStorage.DeleteAccountPlatform());
|
||||||
User = null;
|
User = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
using Shogi.UI.Shared;
|
using Shogi.UI.Shared;
|
||||||
|
|
||||||
namespace Shogi.UI.Pages.Home.Account
|
namespace Shogi.UI.Pages.Home.Account;
|
||||||
|
|
||||||
|
public static class LocalStorageExtensions
|
||||||
{
|
{
|
||||||
public static class LocalStorageExtensions
|
private const string AccountPlatform = "AccountPlatform";
|
||||||
|
|
||||||
|
public static Task<WhichAccountPlatform?> GetAccountPlatform(this ILocalStorage self)
|
||||||
{
|
{
|
||||||
private const string AccountPlatform = "AccountPlatform";
|
return self.Get<WhichAccountPlatform>(AccountPlatform).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
public static Task<WhichAccountPlatform?> GetAccountPlatform(this ILocalStorage self)
|
public static Task SetAccountPlatform(this ILocalStorage self, WhichAccountPlatform platform)
|
||||||
{
|
{
|
||||||
return self.Get<WhichAccountPlatform>(AccountPlatform).AsTask();
|
return self.Set(AccountPlatform, platform.ToString()).AsTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task SetAccountPlatform(this ILocalStorage self, WhichAccountPlatform platform)
|
public static Task DeleteAccountPlatform(this ILocalStorage self)
|
||||||
{
|
{
|
||||||
return self.Set(AccountPlatform, platform.ToString()).AsTask();
|
return self.Delete(AccountPlatform).AsTask();
|
||||||
}
|
|
||||||
|
|
||||||
public static Task DeleteAccountPlatform(this ILocalStorage self)
|
|
||||||
{
|
|
||||||
return self.Delete(AccountPlatform).AsTask();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
namespace Shogi.UI.Pages.Home.Account
|
namespace Shogi.UI.Pages.Home.Account;
|
||||||
|
|
||||||
|
public class LoginEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public class LoginEventArgs : EventArgs
|
public User? User { get; set; }
|
||||||
{
|
|
||||||
public User? User { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ public interface IShogiApi
|
|||||||
Task<ReadSessionsPlayerCountResponse?> GetSessionsPlayerCount();
|
Task<ReadSessionsPlayerCountResponse?> GetSessionsPlayerCount();
|
||||||
Task<CreateTokenResponse?> GetToken();
|
Task<CreateTokenResponse?> GetToken();
|
||||||
Task GuestLogout();
|
Task GuestLogout();
|
||||||
Task PostMove(string sessionName, MovePieceCommand move);
|
Task Move(string sessionName, MovePieceCommand move);
|
||||||
Task<HttpStatusCode> PostSession(string name, bool isPrivate);
|
Task<HttpStatusCode> PostSession(string name, bool isPrivate);
|
||||||
}
|
}
|
||||||
@@ -7,74 +7,74 @@ 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 GuestClientName = "Guest";
|
||||||
public const string MsalClientName = "Msal";
|
public const string MsalClientName = "Msal";
|
||||||
public const string AnonymouseClientName = "Anonymous";
|
public const string AnonymouseClientName = "Anonymous";
|
||||||
|
|
||||||
private readonly JsonSerializerOptions serializerOptions;
|
private readonly JsonSerializerOptions serializerOptions;
|
||||||
private readonly IHttpClientFactory clientFactory;
|
private readonly IHttpClientFactory clientFactory;
|
||||||
private readonly AccountState accountState;
|
private readonly AccountState accountState;
|
||||||
|
|
||||||
public ShogiApi(IHttpClientFactory clientFactory, AccountState accountState)
|
public ShogiApi(IHttpClientFactory clientFactory, AccountState accountState)
|
||||||
{
|
{
|
||||||
serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||||
this.clientFactory = clientFactory;
|
this.clientFactory = clientFactory;
|
||||||
this.accountState = accountState;
|
this.accountState = accountState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpClient HttpClient => accountState.User?.WhichAccountPlatform switch
|
private HttpClient HttpClient => accountState.User?.WhichAccountPlatform switch
|
||||||
{
|
{
|
||||||
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(AnonymouseClientName)
|
||||||
};
|
};
|
||||||
|
|
||||||
public async Task GuestLogout()
|
public async Task GuestLogout()
|
||||||
{
|
{
|
||||||
var response = await HttpClient.PutAsync(new Uri("User/GuestLogout", UriKind.Relative), null);
|
var response = await HttpClient.PutAsync(new Uri("User/GuestLogout", UriKind.Relative), null);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Session?> GetSession(string name)
|
public async Task<Session?> GetSession(string name)
|
||||||
{
|
{
|
||||||
var response = await HttpClient.GetAsync(new Uri($"Sessions/{name}", UriKind.Relative));
|
var response = await HttpClient.GetAsync(new Uri($"Sessions/{name}", UriKind.Relative));
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
return (await response.Content.ReadFromJsonAsync<ReadSessionResponse>(serializerOptions))?.Session;
|
return (await response.Content.ReadFromJsonAsync<ReadSessionResponse>(serializerOptions))?.Session;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ReadSessionsPlayerCountResponse?> GetSessionsPlayerCount()
|
public async Task<ReadSessionsPlayerCountResponse?> GetSessionsPlayerCount()
|
||||||
{
|
{
|
||||||
var response = await HttpClient.GetAsync(new Uri("Sessions/PlayerCount", UriKind.Relative));
|
var response = await HttpClient.GetAsync(new Uri("Sessions/PlayerCount", UriKind.Relative));
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
return await response.Content.ReadFromJsonAsync<ReadSessionsPlayerCountResponse>(serializerOptions);
|
return await response.Content.ReadFromJsonAsync<ReadSessionsPlayerCountResponse>(serializerOptions);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CreateTokenResponse?> GetToken()
|
public async Task<CreateTokenResponse?> GetToken()
|
||||||
{
|
{
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PostMove(string sessionName, MovePieceCommand command)
|
public async Task Move(string sessionName, MovePieceCommand command)
|
||||||
{
|
{
|
||||||
await this.HttpClient.PostAsJsonAsync($"Sessions{sessionName}/Move", command);
|
await this.HttpClient.PatchAsync($"Sessions/{sessionName}/Move", JsonContent.Create(command));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpStatusCode> PostSession(string name, bool isPrivate)
|
public async Task<HttpStatusCode> PostSession(string name, bool isPrivate)
|
||||||
{
|
{
|
||||||
var response = await HttpClient.PostAsJsonAsync(new Uri("Sessions", UriKind.Relative), new CreateSessionCommand
|
var response = await HttpClient.PostAsJsonAsync(new Uri("Sessions", UriKind.Relative), new CreateSessionCommand
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
});
|
});
|
||||||
return response.StatusCode;
|
return response.StatusCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
var position = $"{file}{rank}";
|
var position = $"{file}{rank}";
|
||||||
var piece = session?.BoardState.Board[position];
|
var piece = session?.BoardState.Board[position];
|
||||||
<div class="tile"
|
<div class="tile"
|
||||||
data-position="@(position)"
|
data-position="@(position)"
|
||||||
data-selected="@(piece != null && selectedPosition == position)"
|
data-selected="@(piece != null && selectedPosition == position)"
|
||||||
style="grid-area: @(position)"
|
style="grid-area: @(position)"
|
||||||
@onclick="() => OnClickTile(piece, position)">
|
@onclick="() => OnClickTile(piece, position)">
|
||||||
<GamePiece Piece="piece" Perspective="Perspective" />
|
<GamePiece Piece="piece" Perspective="Perspective" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<span>I</span>
|
<span>I</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Promote prompt -->
|
<!-- Promote prompt -->
|
||||||
<div class="promote-prompt">
|
<div class="promote-prompt" data-visible="@PromotePrompt.IsVisible">
|
||||||
<p>Do you wish to promote?</p>
|
<p>Do you wish to promote?</p>
|
||||||
<div>
|
<div>
|
||||||
<button type="button">Yes</button>
|
<button type="button">Yes</button>
|
||||||
@@ -114,6 +114,7 @@
|
|||||||
: this.session.BoardState.Player2Hand;
|
: this.session.BoardState.Player2Hand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bool IsMyTurn => session?.BoardState.WhoseTurn == Perspective;
|
||||||
|
|
||||||
string? selectedPosition;
|
string? selectedPosition;
|
||||||
WhichPiece? selectedPiece;
|
WhichPiece? selectedPiece;
|
||||||
@@ -138,21 +139,24 @@
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async void OnClickTile(Piece? piece, string position)
|
async void OnClickTile(Piece? piece, string position)
|
||||||
{
|
{
|
||||||
if (SessionName == null) return;
|
if (SessionName == null || !IsMyTurn) return;
|
||||||
|
|
||||||
if (selectedPosition == null)
|
if (selectedPosition == null || piece?.Owner == Perspective)
|
||||||
{
|
{
|
||||||
|
// Select a position.
|
||||||
selectedPosition = position;
|
selectedPosition = position;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (selectedPosition == position)
|
if (selectedPosition == position)
|
||||||
{
|
{
|
||||||
|
// Deselect the selected position.
|
||||||
selectedPosition = null;
|
selectedPosition = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (piece != null)
|
if (piece == null)
|
||||||
{
|
{
|
||||||
if (ShouldPromptForPromotion(position) || ShouldPromptForPromotion(selectedPosition))
|
if (ShouldPromptForPromotion(position) || ShouldPromptForPromotion(selectedPosition))
|
||||||
{
|
{
|
||||||
@@ -164,7 +168,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await ShogiApi.PostMove(SessionName, new MovePieceCommand
|
await ShogiApi.Move(SessionName, new MovePieceCommand
|
||||||
{
|
{
|
||||||
From = selectedPosition,
|
From = selectedPosition,
|
||||||
IsPromotion = false,
|
IsPromotion = false,
|
||||||
@@ -173,6 +177,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnClickHand(Piece piece)
|
void OnClickHand(Piece piece)
|
||||||
{
|
{
|
||||||
selectedPiece = piece.WhichPiece;
|
selectedPiece = piece.WhichPiece;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.board {
|
.board {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"rank A9 B9 C9 D9 E9 F9 G9 H9 I9"
|
"rank A9 B9 C9 D9 E9 F9 G9 H9 I9"
|
||||||
@@ -59,9 +60,10 @@
|
|||||||
overflow: hidden; /* Because SVGs are shaped weird */
|
overflow: hidden; /* Because SVGs are shaped weird */
|
||||||
transition: filter linear 0.25s;
|
transition: filter linear 0.25s;
|
||||||
}
|
}
|
||||||
.tile[data-selected] {
|
|
||||||
filter: invert(0.8);
|
.tile[data-selected] {
|
||||||
}
|
filter: invert(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
.ruler {
|
.ruler {
|
||||||
color: beige;
|
color: beige;
|
||||||
@@ -91,3 +93,20 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.promote-prompt {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border: 2px solid #444;
|
||||||
|
background-color: #eaeaea;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 1px 1px 1px #444;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promote-prompt[data-visible="true"] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class PromotePrompt
|
|||||||
if (command != null && sessionName != null)
|
if (command != null && sessionName != null)
|
||||||
{
|
{
|
||||||
command.IsPromotion = false;
|
command.IsPromotion = false;
|
||||||
return shogiApi.PostMove(sessionName, command);
|
return shogiApi.Move(sessionName, command);
|
||||||
}
|
}
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ public class PromotePrompt
|
|||||||
if (command != null && sessionName != null)
|
if (command != null && sessionName != null)
|
||||||
{
|
{
|
||||||
command.IsPromotion = true;
|
command.IsPromotion = true;
|
||||||
return shogiApi.PostMove(sessionName, command);
|
return shogiApi.Move(sessionName, command);
|
||||||
}
|
}
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@
|
|||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
//ModalService.ShowLoginModal();
|
//ModalService.ShowLoginModal();
|
||||||
//Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
|
Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,11 +277,11 @@ public class AcceptanceTests : IClassFixture<GuestTestFixture>
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var session = (await ReadTestSession()).Session;
|
var session = (await ReadTestSession()).Session;
|
||||||
session.BoardState.Board["A2"].Should().BeNull();
|
session.BoardState.Board["A3"].Should().BeNull();
|
||||||
session.BoardState.Board["A3"].Should().NotBeNull();
|
session.BoardState.Board["A4"].Should().NotBeNull();
|
||||||
session.BoardState.Board["A3"]!.IsPromoted.Should().BeFalse();
|
session.BoardState.Board["A4"]!.IsPromoted.Should().BeFalse();
|
||||||
session.BoardState.Board["A3"]!.Owner.Should().Be(WhichPlayer.Player1);
|
session.BoardState.Board["A4"]!.Owner.Should().Be(WhichPlayer.Player1);
|
||||||
session.BoardState.Board["A3"]!.WhichPiece.Should().Be(WhichPiece.Pawn);
|
session.BoardState.Board["A4"]!.WhichPiece.Should().Be(WhichPiece.Pawn);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,92 +1,64 @@
|
|||||||
# ASP.NET Core
|
# ASP.NET
|
||||||
# Build and test ASP.NET Core projects targeting .NET Core.
|
# Build and test ASP.NET projects.
|
||||||
# Add steps that run tests, create a NuGet package, deploy, and more:
|
# Add steps that publish symbols, save build artifacts, deploy, and more:
|
||||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core
|
# https://docs.microsoft.com/azure/devops/pipelines/apps/aspnet/build-aspnet-4
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
pr:
|
|
||||||
- none
|
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'windows-latest'
|
vmImage: 'windows-latest'
|
||||||
|
|
||||||
name: 'This is changed in a task, below'
|
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
projectName: 'Gameboard.ShogiUI.Sockets'
|
solution: '**/*.sln'
|
||||||
version.MajorMinor: '1.0' # Manually change this when needed to follow semver.
|
buildPlatform: 'Any CPU'
|
||||||
version.Patch: $[counter(variables['version.MajorMinor'], 0)]
|
buildConfiguration: 'Release'
|
||||||
versionNumber: '$(version.MajorMinor).$(version.Patch)'
|
apiProjectName: 'Shogi.Api'
|
||||||
|
uiProjectName: 'Shogi.UI'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
# https://github.com/microsoft/azure-pipelines-tasks/blob/master/docs/authoring/commands.md#build-logging-commands
|
|
||||||
- task: PowerShell@2
|
|
||||||
displayName: Set the name of the build (i.e. the Build.BuildNumber)
|
|
||||||
inputs:
|
|
||||||
targetType: 'inline'
|
|
||||||
script: |
|
|
||||||
[string] $buildName = "$(versionNumber)"
|
|
||||||
Write-Host "Setting the name of the build to '$buildName'."
|
|
||||||
Write-Host "##vso[build.updatebuildnumber]$buildName"
|
|
||||||
|
|
||||||
- task: NuGetToolInstaller@1
|
- task: NuGetToolInstaller@1
|
||||||
|
|
||||||
- task: NuGetCommand@2
|
- task: NuGetCommand@2
|
||||||
inputs:
|
inputs:
|
||||||
command: 'restore'
|
restoreSolution: '$(solution)'
|
||||||
restoreSolution: '**/*.sln'
|
|
||||||
feedsToUse: 'config'
|
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
|
||||||
displayName: Publish
|
|
||||||
env :
|
|
||||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
|
||||||
inputs:
|
|
||||||
command: 'publish'
|
|
||||||
publishWebProjects: false
|
|
||||||
projects: '**/*.csproj'
|
|
||||||
zipAfterPublish: false
|
|
||||||
arguments: '-c Release'
|
|
||||||
|
|
||||||
- task: FileTransform@1
|
- task: FileTransform@1
|
||||||
inputs:
|
inputs:
|
||||||
folderPath: '$(System.DefaultWorkingDirectory)'
|
folderPath: '$(System.DefaultWorkingDirectory)\$(uiProjectName)'
|
||||||
fileType: 'json'
|
fileType: 'json'
|
||||||
targetFiles: '**/appsettings.json'
|
targetFiles: 'wwwroot/appsettings.json'
|
||||||
|
|
||||||
|
- task: DotNetCoreCLI@2
|
||||||
|
inputs:
|
||||||
|
command: 'publish'
|
||||||
|
publishWebProjects: false
|
||||||
|
arguments: '-c Release'
|
||||||
|
zipAfterPublish: false
|
||||||
|
|
||||||
- task: CopyFilesOverSSH@0
|
- task: CopyFilesOverSSH@0
|
||||||
displayName: SSH Copy to 1UB
|
displayName: "Copy API files."
|
||||||
inputs:
|
inputs:
|
||||||
sshEndpoint: 'LucaServer'
|
sshEndpoint: 'LucaServer'
|
||||||
sourceFolder: '$(System.DefaultWorkingDirectory)\$(projectName)\bin\Release\net6.0\publish'
|
sourceFolder: '$(System.DefaultWorkingDirectory)/$(apiProjectName)/bin/Release/net6.0/publish'
|
||||||
targetFolder: '/var/www/apps/$(projectName)'
|
|
||||||
contents: '**'
|
contents: '**'
|
||||||
failOnEmptySource: true
|
targetFolder: '/var/www/apps/$(apiProjectName)'
|
||||||
cleanTargetFolder: true
|
readyTimeout: '20000'
|
||||||
|
|
||||||
|
- task: CopyFilesOverSSH@0
|
||||||
|
displayName: "Copy UI files."
|
||||||
|
inputs:
|
||||||
|
sshEndpoint: 'LucaServer'
|
||||||
|
sourceFolder: '$(System.DefaultWorkingDirectory)/$(uiProjectName)/bin/Release/net6.0/publish'
|
||||||
|
contents: '**'
|
||||||
|
targetFolder: '/var/www/apps/$(uiProjectName)'
|
||||||
|
readyTimeout: '20000'
|
||||||
|
|
||||||
- task: SSH@0
|
- task: SSH@0
|
||||||
displayName: Restart Kestrel
|
displayName: "Restart Kestrel"
|
||||||
inputs:
|
inputs:
|
||||||
sshEndpoint: 'LucaServer'
|
sshEndpoint: 'LucaServer'
|
||||||
runOptions: 'commands'
|
runOptions: 'commands'
|
||||||
commands: 'sudo systemctl restart kestrel-gameboard.shogiui.sockets.service'
|
commands: 'sudo systemctl restart kestrel-shogi.api.service'
|
||||||
readyTimeout: '20000'
|
readyTimeout: '20000'
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
|
||||||
displayName: 'Pack ServiceModels'
|
|
||||||
inputs:
|
|
||||||
command: 'pack'
|
|
||||||
packagesToPack: '**/*ServiceModels*.csproj'
|
|
||||||
versioningScheme: byBuildNumber
|
|
||||||
arguments: '-c Release'
|
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
|
||||||
displayName: 'Push NuGet packages to Feed'
|
|
||||||
inputs:
|
|
||||||
command: 'push'
|
|
||||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg'
|
|
||||||
nuGetFeedType: 'internal'
|
|
||||||
publishVstsFeed: '5622b603-b328-44aa-a6e8-8ca56ff54f88/22948853-baa7-4774-b19d-3aed351711c7'
|
|
||||||
Reference in New Issue
Block a user