all the things
This commit is contained in:
@@ -2,17 +2,20 @@
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Shogi.BackEnd.Identity;
|
using Shogi.BackEnd.Identity;
|
||||||
|
using Shogi.BackEnd.Repositories;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Shogi.BackEnd.Controllers;
|
namespace Shogi.BackEnd.Controllers;
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[Route("[controller]")]
|
[Route("backend/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class AccountController(
|
public class AccountController(
|
||||||
SignInManager<ShogiUser> signInManager,
|
SignInManager<ShogiUser> signInManager,
|
||||||
UserManager<ShogiUser> UserManager,
|
UserManager<ShogiUser> UserManager,
|
||||||
IConfiguration configuration) : ControllerBase
|
IConfiguration configuration,
|
||||||
|
SessionRepository sessionRepository,
|
||||||
|
QueryRepository queryRepository) : ControllerBase
|
||||||
{
|
{
|
||||||
[Authorize("Admin")]
|
[Authorize("Admin")]
|
||||||
[HttpPost("TestAccount")]
|
[HttpPost("TestAccount")]
|
||||||
@@ -36,7 +39,31 @@ public class AccountController(
|
|||||||
return this.Created();
|
return this.Created();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("/logout")]
|
[Authorize("Admin")]
|
||||||
|
[HttpDelete("TestAccount")]
|
||||||
|
public async Task<IActionResult> DeleteTestAccounts()
|
||||||
|
{
|
||||||
|
var testUsers = new[] { "aat-account", "aat-account-2" };
|
||||||
|
|
||||||
|
foreach (var username in testUsers)
|
||||||
|
{
|
||||||
|
var user = await UserManager.FindByNameAsync(username);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
var sessions = await queryRepository.ReadSessionsMetadata(user.Id);
|
||||||
|
foreach (var session in sessions)
|
||||||
|
{
|
||||||
|
await sessionRepository.DeleteSession(session.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserManager.DeleteAsync(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("/backend/logout")]
|
||||||
public async Task<IActionResult> Logout([FromBody] object empty)
|
public async Task<IActionResult> Logout([FromBody] object empty)
|
||||||
{
|
{
|
||||||
// https://learn.microsoft.com/aspnet/core/blazor/security/webassembly/standalone-with-identity#antiforgery-support
|
// https://learn.microsoft.com/aspnet/core/blazor/security/webassembly/standalone-with-identity#antiforgery-support
|
||||||
@@ -50,7 +77,7 @@ public class AccountController(
|
|||||||
return this.Unauthorized();
|
return this.Unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("/roles")]
|
[HttpGet("/backend/roles")]
|
||||||
public IActionResult GetRoles()
|
public IActionResult GetRoles()
|
||||||
{
|
{
|
||||||
if (this.User.Identity is not null && this.User.Identity.IsAuthenticated)
|
if (this.User.Identity is not null && this.User.Identity.IsAuthenticated)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public static class MyIdentityApiEndpointRouteBuilderExtensions
|
|||||||
// We'll figure out a unique endpoint name based on the final route pattern during endpoint generation.
|
// We'll figure out a unique endpoint name based on the final route pattern during endpoint generation.
|
||||||
string? confirmEmailEndpointName = null;
|
string? confirmEmailEndpointName = null;
|
||||||
|
|
||||||
var routeGroup = endpoints.MapGroup("");
|
var routeGroup = endpoints.MapGroup("backend");
|
||||||
|
|
||||||
// NOTE: We cannot inject UserManager<TUser> directly because the TUser generic parameter is currently unsupported by RDG.
|
// NOTE: We cannot inject UserManager<TUser> directly because the TUser generic parameter is currently unsupported by RDG.
|
||||||
// https://github.com/dotnet/aspnetcore/issues/47338
|
// https://github.com/dotnet/aspnetcore/issues/47338
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace Shogi.BackEnd.Controllers;
|
|||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("[controller]")]
|
[Route("backend/[controller]")]
|
||||||
public class SessionsController(
|
public class SessionsController(
|
||||||
SessionRepository sessionRepository,
|
SessionRepository sessionRepository,
|
||||||
ShogiApplication application) : ControllerBase
|
ShogiApplication application) : ControllerBase
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ using System.Text.Json;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles state for cookie-based auth.
|
/// Handles state for cookie-based auth.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IAccountManagement
|
/// <remarks>
|
||||||
|
/// Create a new instance of the auth provider.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="httpClientFactory">Factory to retrieve auth client.</param>
|
||||||
|
public class CookieAuthenticationStateProvider(IHttpClientFactory httpClientFactory) : AuthenticationStateProvider, IAccountManagement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Map the JavaScript-formatted properties to C#-formatted classes.
|
/// Map the JavaScript-formatted properties to C#-formatted classes.
|
||||||
@@ -23,12 +27,12 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Special auth client.
|
/// Special auth client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient httpClient = httpClientFactory.CreateClient("Auth");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authentication state.
|
/// Authentication state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool _authenticated = false;
|
private bool authenticated = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default principal for anonymous (not authenticated) users.
|
/// Default principal for anonymous (not authenticated) users.
|
||||||
@@ -36,13 +40,6 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
private readonly ClaimsPrincipal Unauthenticated =
|
private readonly ClaimsPrincipal Unauthenticated =
|
||||||
new(new ClaimsIdentity());
|
new(new ClaimsIdentity());
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new instance of the auth provider.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpClientFactory">Factory to retrieve auth client.</param>
|
|
||||||
public CookieAuthenticationStateProvider(IHttpClientFactory httpClientFactory)
|
|
||||||
=> _httpClient = httpClientFactory.CreateClient("Auth");
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register a new user.
|
/// Register a new user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -57,7 +54,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// make the request
|
// make the request
|
||||||
var result = await _httpClient.PostAsJsonAsync("register", new
|
var result = await httpClient.PostAsJsonAsync("backend/register", new
|
||||||
{
|
{
|
||||||
email,
|
email,
|
||||||
password
|
password
|
||||||
@@ -118,7 +115,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// login with cookies
|
// login with cookies
|
||||||
var result = await _httpClient.PostAsJsonAsync("login?useCookies=true", new
|
var result = await httpClient.PostAsJsonAsync("backend/login?useCookies=true", new
|
||||||
{
|
{
|
||||||
email,
|
email,
|
||||||
password
|
password
|
||||||
@@ -154,7 +151,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
/// <returns>The authentication state asynchronous request.</returns>
|
/// <returns>The authentication state asynchronous request.</returns>
|
||||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||||
{
|
{
|
||||||
_authenticated = false;
|
authenticated = false;
|
||||||
|
|
||||||
// default to not authenticated
|
// default to not authenticated
|
||||||
var user = Unauthenticated;
|
var user = Unauthenticated;
|
||||||
@@ -162,7 +159,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// the user info endpoint is secured, so if the user isn't logged in this will fail
|
// the user info endpoint is secured, so if the user isn't logged in this will fail
|
||||||
var userResponse = await _httpClient.GetAsync("manage/info");
|
var userResponse = await httpClient.GetAsync("backend/manage/info");
|
||||||
|
|
||||||
// throw if user info wasn't retrieved
|
// throw if user info wasn't retrieved
|
||||||
userResponse.EnsureSuccessStatusCode();
|
userResponse.EnsureSuccessStatusCode();
|
||||||
@@ -187,7 +184,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
.Select(c => new Claim(c.Key, c.Value)));
|
.Select(c => new Claim(c.Key, c.Value)));
|
||||||
|
|
||||||
// tap the roles endpoint for the user's roles
|
// tap the roles endpoint for the user's roles
|
||||||
var rolesResponse = await _httpClient.GetAsync("roles");
|
var rolesResponse = await httpClient.GetAsync("backend/roles");
|
||||||
|
|
||||||
// throw if request fails
|
// throw if request fails
|
||||||
rolesResponse.EnsureSuccessStatusCode();
|
rolesResponse.EnsureSuccessStatusCode();
|
||||||
@@ -213,7 +210,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
// set the principal
|
// set the principal
|
||||||
var id = new ClaimsIdentity(claims, nameof(CookieAuthenticationStateProvider));
|
var id = new ClaimsIdentity(claims, nameof(CookieAuthenticationStateProvider));
|
||||||
user = new ClaimsPrincipal(id);
|
user = new ClaimsPrincipal(id);
|
||||||
_authenticated = true;
|
authenticated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
@@ -226,14 +223,14 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
{
|
{
|
||||||
const string Empty = "{}";
|
const string Empty = "{}";
|
||||||
var emptyContent = new StringContent(Empty, Encoding.UTF8, "application/json");
|
var emptyContent = new StringContent(Empty, Encoding.UTF8, "application/json");
|
||||||
await _httpClient.PostAsync("logout", emptyContent);
|
await httpClient.PostAsync("backend/logout", emptyContent);
|
||||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CheckAuthenticatedAsync()
|
public async Task<bool> CheckAuthenticatedAsync()
|
||||||
{
|
{
|
||||||
await GetAuthenticationStateAsync();
|
await GetAuthenticationStateAsync();
|
||||||
return _authenticated;
|
return authenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -242,7 +239,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
/// <remarks>Do not surface errors from this to users which may tell bad actors if emails do or do not exist in the system.</remarks>
|
/// <remarks>Do not surface errors from this to users which may tell bad actors if emails do or do not exist in the system.</remarks>
|
||||||
public async Task<HttpResponseMessage> RequestPasswordReset(string email)
|
public async Task<HttpResponseMessage> RequestPasswordReset(string email)
|
||||||
{
|
{
|
||||||
return await _httpClient.PostAsJsonAsync("forgotPassword", new { email });
|
return await httpClient.PostAsJsonAsync("backend/forgotPassword", new { email });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<FormResult> ChangePassword(string email, string resetCode, string newPassword)
|
public async Task<FormResult> ChangePassword(string email, string resetCode, string newPassword)
|
||||||
@@ -253,7 +250,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
resetCode,
|
resetCode,
|
||||||
newPassword
|
newPassword
|
||||||
};
|
};
|
||||||
var response = await _httpClient.PostAsJsonAsync("resetPassword", body);
|
var response = await httpClient.PostAsJsonAsync("backend/resetPassword", body);
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
return new FormResult { Succeeded = true };
|
return new FormResult { Succeeded = true };
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
@* Desktop view *@
|
@* Desktop view *@
|
||||||
<nav class="NavMenu PrimaryTheme ThemeVariant--Contrast">
|
<nav class="NavMenu PrimaryTheme ThemeVariant--Contrast">
|
||||||
<h1>Shogi</h1>
|
|
||||||
<a href="">Home</a>
|
<a href="">Home</a>
|
||||||
|
|
||||||
<a href="search">Search</a>
|
<a href="search">Search</a>
|
||||||
@@ -12,7 +11,8 @@
|
|||||||
<button class="href" @onclick="CreateSession">Create</button>
|
<button class="href" @onclick="CreateSession">Create</button>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
|
|
||||||
<div class="spacer" />
|
<h1>Shogi</h1>
|
||||||
|
@* <div class="spacer" /> *@
|
||||||
|
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
.NavMenu {
|
.NavMenu {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 0.75rem;
|
justify-content: center;
|
||||||
padding: 0 0.5rem;
|
gap: 2rem;
|
||||||
|
padding: 0 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.NavMenu .spacer {
|
.NavMenu .spacer {
|
||||||
|
|||||||
@@ -4,32 +4,66 @@
|
|||||||
@using System.Text
|
@using System.Text
|
||||||
|
|
||||||
<main class="shogi PrimaryTheme">
|
<main class="shogi PrimaryTheme">
|
||||||
<h2>What is Shogi?</h2>
|
<div class="shogi-toc">
|
||||||
<p>Shogi is a two-player strategy game where each player simultaneously protects their king while capturing their opponent's.</p>
|
<h3>Table of Contents</h3>
|
||||||
<p>Players take turns, moving one piece each turn until check-mate is achieved.</p>
|
<ul>
|
||||||
|
<li><a href="#what-is-shogi">What is Shogi?</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="#how-to-play">How to Play</a>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#setup">Setup</a></li>
|
||||||
|
<li><a href="#pieces-and-movement">Pieces and Movement</a></li>
|
||||||
|
<li><a href="#promotion">Promotion</a></li>
|
||||||
|
<li><a href="#capturing-and-the-hand">Capturing and the Hand</a></li>
|
||||||
|
<li><a href="#check">The King and "Check"</a></li>
|
||||||
|
<li><a href="#victory">Victory</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2>How to Play</h2>
|
<div class="shogi-content">
|
||||||
|
<h2 id="what-is-shogi">What is Shogi?</h2>
|
||||||
|
<p>Shogi is a two-player strategy game where each player simultaneously protects their king while capturing their opponent's.</p>
|
||||||
|
<p>Players take turns, moving one piece each turn until check-mate is achieved.</p>
|
||||||
|
|
||||||
<h3>Setup</h3>
|
<h2 id="how-to-play">How to Play</h2>
|
||||||
<p>Arrange the board so it looks like this. Take note of the Rook and Bishop positions for each player.</p>
|
|
||||||
<BoardSetupVisualAid />
|
|
||||||
|
|
||||||
<!-- Margin top is because chromium browsers do not render nested grids the same as Firefox -->
|
<h3 id="setup">Setup</h3>
|
||||||
<h3 style="margin-top: 2rem">Pieces and Movement</h3>
|
<p>Arrange the board so it looks like this. Take note of the Rook and Bishop positions for each player.</p>
|
||||||
<p>Each piece has a unique set of moves. Some pieces, like the Pawn, may move only one tile per turn. Other pieces, like the Bishop, may move multiple tiles per turn.</p>
|
<BoardSetupVisualAid />
|
||||||
<p>A tile may only hold one piece and, except for the Knight, pieces may never move through each other.</p>
|
|
||||||
<p>Should your piece enter the tile of an opponent's piece, you must stop there and capture the opponent's piece.</p>
|
|
||||||
<PieceMovesVisualAid />
|
|
||||||
|
|
||||||
<h3>Promotion</h3>
|
<!-- Margin top is because chromium browsers do not render nested grids the same as Firefox -->
|
||||||
<p>The furthest three ranks from your starting position is an area called the <b>promotion zone</b>. A piece may promote at the end of the turn when it moves in to, out of, or within the promotion zone.</p>
|
<h3 id="pieces-and-movement" style="margin-top: 2rem">Pieces and Movement</h3>
|
||||||
<p>Promoting changes the move-set available to the peice, and a piece <em>must promote</em> if it has no legal, future moves. An example of this is a Pawn moving the the furthest rank on the board such that it cannot go further. In this case, the Pawn must promote.</p>
|
<p>Each piece has a unique set of moves. Some pieces, like the Pawn, may move only one tile per turn. Other pieces, like the Bishop, may move multiple tiles per turn.</p>
|
||||||
<p>All pieces may promote <b>except for</b> the Gold General and King.</p>
|
<p>A tile may only hold one piece and, except for the Knight, pieces may never move through each other.</p>
|
||||||
<PromotedPieceVisualAid />
|
<p>Should your piece enter the tile of an opponent's piece, you must stop there and capture the opponent's piece.</p>
|
||||||
|
<PieceMovesVisualAid />
|
||||||
|
|
||||||
<h3>Capturing and the Hand</h3>
|
<h3 id="promotion">Promotion</h3>
|
||||||
<h3>The King and "Check"</h3>
|
<p>The furthest three ranks from your starting position is an area called the <b>promotion zone</b>. A piece may promote at the end of the turn when it moves in to, out of, or within the promotion zone.</p>
|
||||||
<h3>Victory</h3>
|
<p>Promoting changes the move-set available to the peice, and a piece <em>must promote</em> if it has no legal, future moves. An example of this is a Pawn moving the the furthest rank on the board such that it cannot go further. In this case, the Pawn must promote.</p>
|
||||||
|
<p>All pieces may promote <b>except for</b> the Gold General and King.</p>
|
||||||
|
<PromotedPieceVisualAid />
|
||||||
|
|
||||||
|
<h3 id="capturing-and-the-hand">Capturing and the Hand</h3>
|
||||||
|
<p>When you capture an opponent's piece, it becomes yours. It is placed in your "hand" and can be returned to the board later.</p>
|
||||||
|
<p>On any turn, instead of moving a piece, you may "drop" a captured piece onto any empty tile. This makes Shogi very dynamic as the board can change rapidly.</p>
|
||||||
|
<p>There are rules for drops:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Dropped pieces always start unpromoted.</li>
|
||||||
|
<li>You may not drop a piece where it has no future moves (like a Pawn on the last rank).</li>
|
||||||
|
<li>You cannot drop a Pawn into a column that already has another of your unpromoted Pawns.</li>
|
||||||
|
<li>You cannot drop a Pawn to deliver immediate checkmate.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 id="check">The King and "Check"</h3>
|
||||||
|
<p>If your King is under attack, it is in "check". You must make a move to protect the King immediately.</p>
|
||||||
|
<p>You can move the King to safety, capture the attacking piece, or block the attack with another piece.</p>
|
||||||
|
|
||||||
|
<h3 id="victory">Victory</h3>
|
||||||
|
<p>The goal is to capture the opponent's King. If the King is in check and cannot escape, it is "checkmate" and you win.</p>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -2,6 +2,73 @@
|
|||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 80ch;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
overflow: auto;
|
}
|
||||||
|
|
||||||
|
.shogi-toc {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shogi-content h2 {
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid white;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1025px) {
|
||||||
|
.shogi {
|
||||||
|
max-width: 140ch;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 250px 1fr;
|
||||||
|
gap: 4rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shogi-toc {
|
||||||
|
display: block;
|
||||||
|
position: sticky;
|
||||||
|
top: 2rem;
|
||||||
|
max-height: calc(100vh - 4rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shogi-toc h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.3);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shogi-toc ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shogi-toc > ul > li {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shogi-toc ul ul {
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shogi-toc a {
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
padding: 0.1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shogi-toc a:hover {
|
||||||
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,17 +22,21 @@
|
|||||||
<LanceMoves />
|
<LanceMoves />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
|
||||||
<div class="moves">
|
<div class="moves">
|
||||||
<h5>Knight</h5>
|
<h5>Knight</h5>
|
||||||
<KnightMoves />
|
<KnightMoves />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div class="moves">
|
<div class="moves">
|
||||||
<h5>Silver General</h5>
|
<h5>Silver General</h5>
|
||||||
<SilverMoves />
|
<SilverMoves />
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
|
||||||
<div class="moves">
|
<div class="moves">
|
||||||
<h5>Gold General</h5>
|
<h5>Gold General</h5>
|
||||||
|
|||||||
@@ -21,14 +21,14 @@
|
|||||||
<h5>Promoted Lance</h5>
|
<h5>Promoted Lance</h5>
|
||||||
<PromotedLanceMoves />
|
<PromotedLanceMoves />
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
|
||||||
<div class="moves">
|
<div class="moves">
|
||||||
<h5>Promoted Knight</h5>
|
<h5>Promoted Knight</h5>
|
||||||
<PromotedKnightMoves />
|
<PromotedKnightMoves />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div class="moves">
|
<div class="moves">
|
||||||
<h5>Promoted Silver General</h5>
|
<h5>Promoted Silver General</h5>
|
||||||
<PromotedSilverMoves />
|
<PromotedSilverMoves />
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<label for="email" style="grid-area: emailLabel">Email</label>
|
<label for="email" style="grid-area: emailLabel">Email</label>
|
||||||
<input required id="email" name="emailInput" type="email" style="grid-area: emailControl" @bind-value="email" readonly="@isEmailSubmitted" />
|
<input required id="email" name="emailInput" type="text" style="grid-area: emailControl" @bind-value="email" readonly="@isEmailSubmitted" />
|
||||||
|
|
||||||
@if (isEmailSubmitted)
|
@if (isEmailSubmitted)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,25 +4,25 @@ using Microsoft.AspNetCore.ResponseCompression;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Shogi;
|
using Shogi;
|
||||||
using Shogi.BackEnd.Application;
|
using Shogi.BackEnd.Application;
|
||||||
using Shogi.FrontEnd.Components;
|
|
||||||
using Shogi.BackEnd.Controllers;
|
using Shogi.BackEnd.Controllers;
|
||||||
using Shogi.BackEnd.Identity;
|
using Shogi.BackEnd.Identity;
|
||||||
using Shogi.BackEnd.Repositories;
|
using Shogi.BackEnd.Repositories;
|
||||||
using Shogi.FrontEnd.Client;
|
using Shogi.FrontEnd.Client;
|
||||||
|
using Shogi.FrontEnd.Components;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add Blazor components
|
// Add Blazor components
|
||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
|
|
||||||
// Add API controllers
|
// Add API controllers
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddControllers()
|
.AddControllers()
|
||||||
.AddJsonOptions(options =>
|
.AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
options.JsonSerializerOptions.WriteIndented = true;
|
options.JsonSerializerOptions.WriteIndented = true;
|
||||||
});
|
});
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ AddIdentity(builder, builder.Configuration);
|
|||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
builder.Services.AddResponseCompression(opts =>
|
builder.Services.AddResponseCompression(opts =>
|
||||||
{
|
{
|
||||||
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]);
|
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
@@ -55,13 +55,13 @@ app.MyMapIdentityApi<ShogiUser>(builder.Environment);
|
|||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Error");
|
app.UseExceptionHandler("/Error");
|
||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
@@ -74,43 +74,43 @@ app.UseAuthorization();
|
|||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.MapHub<GameHub>("/gamehub");
|
app.MapHub<GameHub>("/gamehub");
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode();
|
.AddInteractiveServerRenderMode();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
static void AddIdentity(WebApplicationBuilder builder, ConfigurationManager configuration)
|
static void AddIdentity(WebApplicationBuilder builder, ConfigurationManager configuration)
|
||||||
{
|
{
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddAuthorizationBuilder()
|
.AddAuthorizationBuilder()
|
||||||
.AddPolicy("Admin", policy =>
|
.AddPolicy("Admin", policy =>
|
||||||
{
|
{
|
||||||
policy.RequireAuthenticatedUser();
|
policy.RequireAuthenticatedUser();
|
||||||
policy.RequireAssertion(context => context.User?.Identity?.Name switch
|
policy.RequireAssertion(context => context.User?.Identity?.Name switch
|
||||||
{
|
{
|
||||||
"Hauth@live.com" => true,
|
"Hauth@live.com" => true,
|
||||||
"aat-account" => true,
|
"aat-account" => true,
|
||||||
_ => false
|
_ => false
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddDbContext<ApplicationDbContext>(options =>
|
.AddDbContext<ApplicationDbContext>(options =>
|
||||||
{
|
{
|
||||||
var cs = configuration.GetConnectionString("ShogiDatabase") ?? throw new InvalidOperationException("Database not configured.");
|
var cs = configuration.GetConnectionString("ShogiDatabase") ?? throw new InvalidOperationException("Database not configured.");
|
||||||
options.UseSqlServer(cs);
|
options.UseSqlServer(cs);
|
||||||
})
|
})
|
||||||
.AddIdentityApiEndpoints<ShogiUser>(options =>
|
.AddIdentityApiEndpoints<ShogiUser>(options =>
|
||||||
{
|
{
|
||||||
options.SignIn.RequireConfirmedEmail = true;
|
options.SignIn.RequireConfirmedEmail = true;
|
||||||
options.User.RequireUniqueEmail = true;
|
options.User.RequireUniqueEmail = true;
|
||||||
})
|
})
|
||||||
.AddEntityFrameworkStores<ApplicationDbContext>();
|
.AddEntityFrameworkStores<ApplicationDbContext>();
|
||||||
|
|
||||||
builder.Services.ConfigureApplicationCookie(options =>
|
builder.Services.ConfigureApplicationCookie(options =>
|
||||||
{
|
{
|
||||||
options.SlidingExpiration = true;
|
options.SlidingExpiration = true;
|
||||||
options.ExpireTimeSpan = TimeSpan.FromDays(3);
|
options.ExpireTimeSpan = TimeSpan.FromDays(3);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make Program accessible for WebApplicationFactory in integration tests
|
// Make Program accessible for WebApplicationFactory in integration tests
|
||||||
|
|||||||
@@ -82,6 +82,10 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.1" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
|
||||||
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
|
<PackageReference Include="System.Data.SqlClient" Version="4.9.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user