all the things

This commit is contained in:
2026-01-14 22:04:37 -06:00
parent a3f23b199a
commit 114025fcfb
13 changed files with 233 additions and 99 deletions

View File

@@ -2,17 +2,20 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Shogi.BackEnd.Identity;
using Shogi.BackEnd.Repositories;
using System.Security.Claims;
namespace Shogi.BackEnd.Controllers;
[Authorize]
[Route("[controller]")]
[Route("backend/[controller]")]
[ApiController]
public class AccountController(
SignInManager<ShogiUser> signInManager,
UserManager<ShogiUser> UserManager,
IConfiguration configuration) : ControllerBase
IConfiguration configuration,
SessionRepository sessionRepository,
QueryRepository queryRepository) : ControllerBase
{
[Authorize("Admin")]
[HttpPost("TestAccount")]
@@ -36,7 +39,31 @@ public class AccountController(
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)
{
// https://learn.microsoft.com/aspnet/core/blazor/security/webassembly/standalone-with-identity#antiforgery-support
@@ -50,7 +77,7 @@ public class AccountController(
return this.Unauthorized();
}
[HttpGet("/roles")]
[HttpGet("/backend/roles")]
public IActionResult GetRoles()
{
if (this.User.Identity is not null && this.User.Identity.IsAuthenticated)

View File

@@ -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.
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.
// https://github.com/dotnet/aspnetcore/issues/47338

View File

@@ -9,7 +9,7 @@ namespace Shogi.BackEnd.Controllers;
[Authorize]
[ApiController]
[Route("[controller]")]
[Route("backend/[controller]")]
public class SessionsController(
SessionRepository sessionRepository,
ShogiApplication application) : ControllerBase

View File

@@ -10,7 +10,11 @@ using System.Text.Json;
/// <summary>
/// Handles state for cookie-based auth.
/// </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>
/// Map the JavaScript-formatted properties to C#-formatted classes.
@@ -23,12 +27,12 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
/// <summary>
/// Special auth client.
/// </summary>
private readonly HttpClient _httpClient;
private readonly HttpClient httpClient = httpClientFactory.CreateClient("Auth");
/// <summary>
/// Authentication state.
/// </summary>
private bool _authenticated = false;
private bool authenticated = false;
/// <summary>
/// Default principal for anonymous (not authenticated) users.
@@ -36,13 +40,6 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
private readonly ClaimsPrincipal Unauthenticated =
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>
/// Register a new user.
/// </summary>
@@ -57,7 +54,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
try
{
// make the request
var result = await _httpClient.PostAsJsonAsync("register", new
var result = await httpClient.PostAsJsonAsync("backend/register", new
{
email,
password
@@ -118,7 +115,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
try
{
// login with cookies
var result = await _httpClient.PostAsJsonAsync("login?useCookies=true", new
var result = await httpClient.PostAsJsonAsync("backend/login?useCookies=true", new
{
email,
password
@@ -154,7 +151,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
/// <returns>The authentication state asynchronous request.</returns>
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
_authenticated = false;
authenticated = false;
// default to not authenticated
var user = Unauthenticated;
@@ -162,7 +159,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
try
{
// 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
userResponse.EnsureSuccessStatusCode();
@@ -187,7 +184,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
.Select(c => new Claim(c.Key, c.Value)));
// 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
rolesResponse.EnsureSuccessStatusCode();
@@ -213,7 +210,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
// set the principal
var id = new ClaimsIdentity(claims, nameof(CookieAuthenticationStateProvider));
user = new ClaimsPrincipal(id);
_authenticated = true;
authenticated = true;
}
}
catch { }
@@ -226,14 +223,14 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
{
const string Empty = "{}";
var emptyContent = new StringContent(Empty, Encoding.UTF8, "application/json");
await _httpClient.PostAsync("logout", emptyContent);
await httpClient.PostAsync("backend/logout", emptyContent);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task<bool> CheckAuthenticatedAsync()
{
await GetAuthenticationStateAsync();
return _authenticated;
return authenticated;
}
/// <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>
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)
@@ -253,7 +250,7 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
resetCode,
newPassword
};
var response = await _httpClient.PostAsJsonAsync("resetPassword", body);
var response = await httpClient.PostAsJsonAsync("backend/resetPassword", body);
if (response.IsSuccessStatusCode)
{
return new FormResult { Succeeded = true };

View File

@@ -3,7 +3,6 @@
@* Desktop view *@
<nav class="NavMenu PrimaryTheme ThemeVariant--Contrast">
<h1>Shogi</h1>
<a href="">Home</a>
<a href="search">Search</a>
@@ -12,7 +11,8 @@
<button class="href" @onclick="CreateSession">Create</button>
</AuthorizeView>
<div class="spacer" />
<h1>Shogi</h1>
@* <div class="spacer" /> *@
<AuthorizeView>
<Authorized>

View File

@@ -1,8 +1,9 @@
.NavMenu {
display: flex;
align-items: baseline;
gap: 0.75rem;
padding: 0 0.5rem;
justify-content: center;
gap: 2rem;
padding: 0 2rem;
}
.NavMenu .spacer {

View File

@@ -4,32 +4,66 @@
@using System.Text
<main class="shogi PrimaryTheme">
<h2>What is Shogi?</h2>
<div class="shogi-toc">
<h3>Table of Contents</h3>
<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>
<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>
<h2>How to Play</h2>
<h2 id="how-to-play">How to Play</h2>
<h3>Setup</h3>
<h3 id="setup">Setup</h3>
<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 style="margin-top: 2rem">Pieces and Movement</h3>
<h3 id="pieces-and-movement" style="margin-top: 2rem">Pieces and Movement</h3>
<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>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>
<h3 id="promotion">Promotion</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>
<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>Capturing and the Hand</h3>
<h3>The King and "Check"</h3>
<h3>Victory</h3>
<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>
@code {

View File

@@ -2,6 +2,73 @@
background-color: var(--primary-color);
color: white;
padding: 1rem;
margin: auto;
width: 100%;
max-width: 80ch;
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;
}
}

View File

@@ -22,17 +22,21 @@
<LanceMoves />
</div>
</section>
<section>
<div class="moves">
<h5>Knight</h5>
<KnightMoves />
</div>
</section>
<section>
<div class="moves">
<h5>Silver General</h5>
<SilverMoves />
</div>
</section>
<section>
<div class="moves">
<h5>Gold General</h5>

View File

@@ -21,14 +21,14 @@
<h5>Promoted Lance</h5>
<PromotedLanceMoves />
</div>
</section>
<section>
<div class="moves">
<h5>Promoted Knight</h5>
<PromotedKnightMoves />
</div>
</section>
<section>
<div class="moves">
<h5>Promoted Silver General</h5>
<PromotedSilverMoves />

View File

@@ -25,7 +25,7 @@
}
<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)
{

View File

@@ -4,11 +4,11 @@ using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.EntityFrameworkCore;
using Shogi;
using Shogi.BackEnd.Application;
using Shogi.FrontEnd.Components;
using Shogi.BackEnd.Controllers;
using Shogi.BackEnd.Identity;
using Shogi.BackEnd.Repositories;
using Shogi.FrontEnd.Client;
using Shogi.FrontEnd.Components;
var builder = WebApplication.CreateBuilder(args);

View File

@@ -82,6 +82,10 @@
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" 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="System.Data.SqlClient" Version="4.9.0" />
</ItemGroup>