yep
This commit is contained in:
@@ -60,21 +60,29 @@ public class AccountController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.Ok();
|
return this.NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("/backend/logout")]
|
[HttpPost("Login")]
|
||||||
public async Task<IActionResult> Logout([FromBody] object empty)
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> Login([FromForm] string email, [FromForm] string password)
|
||||||
{
|
{
|
||||||
// https://learn.microsoft.com/aspnet/core/blazor/security/webassembly/standalone-with-identity#antiforgery-support
|
var result = await signInManager.PasswordSignInAsync(email, password, isPersistent: true, lockoutOnFailure: false);
|
||||||
if (empty is not null)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
await signInManager.SignOutAsync();
|
return Redirect("/");
|
||||||
|
|
||||||
return this.Ok();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.Unauthorized();
|
return Redirect("/login?error=Invalid login attempt.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("Logout")]
|
||||||
|
[HttpPost("Logout")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> Logout()
|
||||||
|
{
|
||||||
|
await signInManager.SignOutAsync();
|
||||||
|
return Redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("/backend/roles")]
|
[HttpGet("/backend/roles")]
|
||||||
|
|||||||
72
Shogi/BackEnd/Identity/ServerAccountManager.cs
Normal file
72
Shogi/BackEnd/Identity/ServerAccountManager.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
namespace Shogi.BackEnd.Identity;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Identity.UI.Services;
|
||||||
|
using Microsoft.AspNetCore.WebUtilities;
|
||||||
|
using Shogi.FrontEnd.Client;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
|
public class ServerAccountManager(
|
||||||
|
UserManager<ShogiUser> userManager,
|
||||||
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
IEmailSender emailSender) : IAccountManagement
|
||||||
|
{
|
||||||
|
public async Task<FormResult> RegisterAsync(string email, string password)
|
||||||
|
{
|
||||||
|
var user = new ShogiUser { UserName = email, Email = email };
|
||||||
|
var result = await userManager.CreateAsync(user, password);
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
var userId = await userManager.GetUserIdAsync(user);
|
||||||
|
var code = await userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||||
|
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||||
|
|
||||||
|
var request = httpContextAccessor.HttpContext?.Request;
|
||||||
|
var host = request?.Host.Value ?? "localhost";
|
||||||
|
var scheme = request?.Scheme ?? "https";
|
||||||
|
var callbackUrl = $"{scheme}://{host}/backend/confirmEmail?userId={userId}&code={code}";
|
||||||
|
|
||||||
|
await emailSender.SendEmailAsync(email, "Confirm your email",
|
||||||
|
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
|
||||||
|
|
||||||
|
return new FormResult { Succeeded = true };
|
||||||
|
}
|
||||||
|
return new FormResult { Succeeded = false, ErrorList = result.Errors.Select(e => e.Description).ToArray() };
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<FormResult> LoginAsync(string email, string password)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Login must be performed via Form POST to /backend/Account/Login in Server-Side Rendering.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task LogoutAsync()
|
||||||
|
{
|
||||||
|
// Logout should be performed via Form POST or Link to /backend/Account/Logout.
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> CheckAuthenticatedAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult(httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HttpResponseMessage> RequestPasswordReset(string email)
|
||||||
|
{
|
||||||
|
var user = await userManager.FindByEmailAsync(email);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
// Generate token and send email logic would go here.
|
||||||
|
}
|
||||||
|
return new HttpResponseMessage(System.Net.HttpStatusCode.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FormResult> ChangePassword(string email, string resetCode, string newPassword)
|
||||||
|
{
|
||||||
|
var user = await userManager.FindByEmailAsync(email);
|
||||||
|
if (user == null) return new FormResult { Succeeded = false, ErrorList = ["User not found"] };
|
||||||
|
var result = await userManager.ResetPasswordAsync(user, resetCode, newPassword);
|
||||||
|
return new FormResult { Succeeded = result.Succeeded, ErrorList = result.Errors.Select(e => e.Description).ToArray() };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
namespace Shogi.FrontEnd.Client;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles state for cookie-based auth.
|
|
||||||
/// </summary>
|
|
||||||
/// <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.
|
|
||||||
/// </summary>
|
|
||||||
private readonly JsonSerializerOptions jsonSerializerOptions = new()
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Special auth client.
|
|
||||||
/// </summary>
|
|
||||||
private readonly HttpClient httpClient = httpClientFactory.CreateClient("Auth");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Authentication state.
|
|
||||||
/// </summary>
|
|
||||||
private bool authenticated = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Default principal for anonymous (not authenticated) users.
|
|
||||||
/// </summary>
|
|
||||||
private readonly ClaimsPrincipal Unauthenticated =
|
|
||||||
new(new ClaimsIdentity());
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Register a new user.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="email">The user's email address.</param>
|
|
||||||
/// <param name="password">The user's password.</param>
|
|
||||||
/// <returns>The result serialized to a <see cref="FormResult"/>.
|
|
||||||
/// </returns>
|
|
||||||
public async Task<FormResult> RegisterAsync(string email, string password)
|
|
||||||
{
|
|
||||||
string[] defaultDetail = ["An unknown error prevented registration from succeeding."];
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// make the request
|
|
||||||
var result = await httpClient.PostAsJsonAsync("backend/register", new
|
|
||||||
{
|
|
||||||
email,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
|
|
||||||
// successful?
|
|
||||||
if (result.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return new FormResult { Succeeded = true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// body should contain details about why it failed
|
|
||||||
var details = await result.Content.ReadAsStringAsync();
|
|
||||||
var problemDetails = JsonDocument.Parse(details);
|
|
||||||
var errors = new List<string>();
|
|
||||||
var errorList = problemDetails.RootElement.GetProperty("errors");
|
|
||||||
|
|
||||||
foreach (var errorEntry in errorList.EnumerateObject())
|
|
||||||
{
|
|
||||||
if (errorEntry.Value.ValueKind == JsonValueKind.String)
|
|
||||||
{
|
|
||||||
errors.Add(errorEntry.Value.GetString()!);
|
|
||||||
}
|
|
||||||
else if (errorEntry.Value.ValueKind == JsonValueKind.Array)
|
|
||||||
{
|
|
||||||
errors.AddRange(
|
|
||||||
errorEntry.Value.EnumerateArray().Select(
|
|
||||||
e => e.GetString() ?? string.Empty)
|
|
||||||
.Where(e => !string.IsNullOrEmpty(e)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the error list
|
|
||||||
return new FormResult
|
|
||||||
{
|
|
||||||
Succeeded = false,
|
|
||||||
ErrorList = problemDetails == null ? defaultDetail : [.. errors]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
// unknown error
|
|
||||||
return new FormResult
|
|
||||||
{
|
|
||||||
Succeeded = false,
|
|
||||||
ErrorList = defaultDetail
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// User login.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="email">The user's email address.</param>
|
|
||||||
/// <param name="password">The user's password.</param>
|
|
||||||
/// <returns>The result of the login request serialized to a <see cref="FormResult"/>.</returns>
|
|
||||||
public async Task<FormResult> LoginAsync(string email, string password)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// login with cookies
|
|
||||||
var result = await httpClient.PostAsJsonAsync("backend/login?useCookies=true", new
|
|
||||||
{
|
|
||||||
email,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
|
|
||||||
// success?
|
|
||||||
if (result.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
// need to refresh auth state
|
|
||||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
|
||||||
|
|
||||||
// success!
|
|
||||||
return new FormResult { Succeeded = true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
// unknown error
|
|
||||||
return new FormResult
|
|
||||||
{
|
|
||||||
Succeeded = false,
|
|
||||||
ErrorList = ["Invalid email and/or password."]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get authentication state.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Called by Blazor anytime and authentication-based decision needs to be made, then cached
|
|
||||||
/// until the changed state notification is raised.
|
|
||||||
/// </remarks>
|
|
||||||
/// <returns>The authentication state asynchronous request.</returns>
|
|
||||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
|
||||||
{
|
|
||||||
authenticated = false;
|
|
||||||
|
|
||||||
// default to not authenticated
|
|
||||||
var user = Unauthenticated;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// the user info endpoint is secured, so if the user isn't logged in this will fail
|
|
||||||
var userResponse = await httpClient.GetAsync("backend/manage/info");
|
|
||||||
|
|
||||||
// throw if user info wasn't retrieved
|
|
||||||
userResponse.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
// user is authenticated,so let's build their authenticated identity
|
|
||||||
var userJson = await userResponse.Content.ReadAsStringAsync();
|
|
||||||
var userInfo = JsonSerializer.Deserialize<UserInfo>(userJson, jsonSerializerOptions);
|
|
||||||
|
|
||||||
if (userInfo != null)
|
|
||||||
{
|
|
||||||
// in our system name and email are the same
|
|
||||||
var claims = new List<Claim>
|
|
||||||
{
|
|
||||||
new(ClaimTypes.Name, userInfo.Email),
|
|
||||||
new(ClaimTypes.Email, userInfo.Email)
|
|
||||||
};
|
|
||||||
|
|
||||||
// add any additional claims
|
|
||||||
claims.AddRange(
|
|
||||||
userInfo.Claims
|
|
||||||
.Where(c => c.Key != ClaimTypes.Name && c.Key != ClaimTypes.Email)
|
|
||||||
.Select(c => new Claim(c.Key, c.Value)));
|
|
||||||
|
|
||||||
// tap the roles endpoint for the user's roles
|
|
||||||
var rolesResponse = await httpClient.GetAsync("backend/roles");
|
|
||||||
|
|
||||||
// throw if request fails
|
|
||||||
rolesResponse.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
// read the response into a string
|
|
||||||
var rolesJson = await rolesResponse.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
// deserialize the roles string into an array
|
|
||||||
var roles = JsonSerializer.Deserialize<RoleClaim[]>(rolesJson, jsonSerializerOptions);
|
|
||||||
|
|
||||||
// if there are roles, add them to the claims collection
|
|
||||||
if (roles?.Length > 0)
|
|
||||||
{
|
|
||||||
foreach (var role in roles)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(role.Type) && !string.IsNullOrEmpty(role.Value))
|
|
||||||
{
|
|
||||||
claims.Add(new Claim(role.Type, role.Value, role.ValueType, role.Issuer, role.OriginalIssuer));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the principal
|
|
||||||
var id = new ClaimsIdentity(claims, nameof(CookieAuthenticationStateProvider));
|
|
||||||
user = new ClaimsPrincipal(id);
|
|
||||||
authenticated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
// return the state
|
|
||||||
return new AuthenticationState(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogoutAsync()
|
|
||||||
{
|
|
||||||
const string Empty = "{}";
|
|
||||||
var emptyContent = new StringContent(Empty, Encoding.UTF8, "application/json");
|
|
||||||
await httpClient.PostAsync("backend/logout", emptyContent);
|
|
||||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> CheckAuthenticatedAsync()
|
|
||||||
{
|
|
||||||
await GetAuthenticationStateAsync();
|
|
||||||
return authenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ask for an email to be sent which contains a reset code. This reset code is used during <see cref="ChangePassword"/>
|
|
||||||
/// </summary>
|
|
||||||
/// <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("backend/forgotPassword", new { email });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FormResult> ChangePassword(string email, string resetCode, string newPassword)
|
|
||||||
{
|
|
||||||
var body = new
|
|
||||||
{
|
|
||||||
email,
|
|
||||||
resetCode,
|
|
||||||
newPassword
|
|
||||||
};
|
|
||||||
var response = await httpClient.PostAsJsonAsync("backend/resetPassword", body);
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return new FormResult { Succeeded = true };
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return new FormResult
|
|
||||||
{
|
|
||||||
Succeeded = false,
|
|
||||||
ErrorList = [await response.Content.ReadAsStringAsync()]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RoleClaim
|
|
||||||
{
|
|
||||||
public string? Issuer { get; set; }
|
|
||||||
public string? OriginalIssuer { get; set; }
|
|
||||||
public string? Type { get; set; }
|
|
||||||
public string? Value { get; set; }
|
|
||||||
public string? ValueType { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,14 @@
|
|||||||
<HeadOutlet @rendermode="RenderMode.InteractiveServer" />
|
<HeadOutlet @rendermode="RenderMode.InteractiveServer" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<!--
|
||||||
|
data-enhance:
|
||||||
|
Enables Blazor's "Enhanced Form Handling" globally for the document body.
|
||||||
|
This intercepts standard <form> POST requests (like our login implementation) and performs them via fetch,
|
||||||
|
patching the DOM with the response instead of triggering a full page reload.
|
||||||
|
This provides a SPA-like experience even when using server-side endpoints for Cookies/Auth.
|
||||||
|
-->
|
||||||
|
<body data-enhance>
|
||||||
<Routes @rendermode="RenderMode.InteractiveServer" />
|
<Routes @rendermode="RenderMode.InteractiveServer" />
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<span>@context.User.Identity?.Name</span>
|
<span>@context.User.Identity?.Name</span>
|
||||||
<a href="logout">Logout</a>
|
<a href="backend/Account/Logout">Logout</a>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
<a href="login">Login</a>
|
<a href="login">Login</a>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<button class="href" @onclick="CreateSession">Create</button>
|
<button class="href" @onclick="CreateSession">Create</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="logout">Logout</a>
|
<a href="backend/Account/Logout">Logout</a>
|
||||||
</li>
|
</li>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
|
|||||||
@@ -1,41 +1,33 @@
|
|||||||
@page "/login"
|
@page "/login"
|
||||||
@inject IAccountManagement Acct
|
|
||||||
@inject NavigationManager navigator
|
@inject NavigationManager navigator
|
||||||
|
|
||||||
<main class="PrimaryTheme">
|
<main class="PrimaryTheme">
|
||||||
|
|
||||||
<form class="LoginForm" @onsubmit="HandleSubmit">
|
<form class="LoginForm" method="post" action="backend/Account/Login">
|
||||||
<h1 style="grid-area: h1">Login</h1>
|
<h1 style="grid-area: h1">Login</h1>
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<div>You're logged in as @context.User.Identity?.Name.</div>
|
<div>You're logged in as @context.User.Identity?.Name.</div>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
@if (errorList.Length > 0)
|
@if (!string.IsNullOrEmpty(Error))
|
||||||
{
|
{
|
||||||
<div class="Errors" style="grid-area: errors">
|
<div class="Errors" style="grid-area: errors">
|
||||||
<ul>
|
<ul>
|
||||||
@foreach (var error in errorList)
|
<li>@Error</li>
|
||||||
{
|
|
||||||
<li>@error</li>
|
|
||||||
}
|
|
||||||
</ul>
|
</ul>
|
||||||
<div class="ErrorProgress" @key="errorKey"></div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<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="text" style="grid-area: emailControl" @bind-value="email" readonly="@isEmailSubmitted" />
|
<input required id="email" name="email" type="text" style="grid-area: emailControl" />
|
||||||
|
|
||||||
@if (isEmailSubmitted)
|
<label for="password" style="grid-area: passLabel">Password</label>
|
||||||
{
|
<input required id="password" name="password" type="password" style="grid-area: passControl" />
|
||||||
<label for="password" style="grid-area: passLabel">Password</label>
|
|
||||||
<input required id="password" name="passwordInput" type="password" style="grid-area: passControl" @bind-value="password" />
|
|
||||||
|
|
||||||
<a href="forgot" style="grid-area: resetLink; place-self: end;">Reset password</a>
|
<a href="forgot" style="grid-area: resetLink; place-self: end;">Reset password</a>
|
||||||
}
|
|
||||||
|
|
||||||
<button style="grid-area: button" type="submit">@(isEmailSubmitted ? "Login" : "Next")</button>
|
<button style="grid-area: button" type="submit">Login</button>
|
||||||
</NotAuthorized>
|
</NotAuthorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
</form>
|
</form>
|
||||||
@@ -43,95 +35,6 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[SupplyParameterFromQuery]
|
||||||
private string email = string.Empty;
|
public string? Error { get; set; }
|
||||||
private string password = string.Empty;
|
|
||||||
private string[] errorList = [];
|
|
||||||
private System.Threading.CancellationTokenSource? _clearErrorCts;
|
|
||||||
private bool isEmailSubmitted = false;
|
|
||||||
private Guid errorKey;
|
|
||||||
|
|
||||||
private async Task HandleSubmit()
|
|
||||||
{
|
|
||||||
if (!isEmailSubmitted)
|
|
||||||
{
|
|
||||||
SubmitEmail();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await DoLoginAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SubmitEmail()
|
|
||||||
{
|
|
||||||
SetErrors([]);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(email))
|
|
||||||
{
|
|
||||||
SetErrors(["Email is required."]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isEmailSubmitted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DoLoginAsync()
|
|
||||||
{
|
|
||||||
SetErrors([]);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(email))
|
|
||||||
{
|
|
||||||
SetErrors(["Email is required."]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(password))
|
|
||||||
{
|
|
||||||
SetErrors(["Password is required."]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await Acct.LoginAsync(email, password);
|
|
||||||
|
|
||||||
if (result.Succeeded)
|
|
||||||
{
|
|
||||||
email = password = string.Empty;
|
|
||||||
|
|
||||||
navigator.NavigateTo("");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SetErrors(result.ErrorList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetErrors(string[] errors)
|
|
||||||
{
|
|
||||||
_clearErrorCts?.Cancel();
|
|
||||||
errorList = errors;
|
|
||||||
|
|
||||||
if (errors.Length > 0)
|
|
||||||
{
|
|
||||||
errorKey = Guid.NewGuid();
|
|
||||||
_clearErrorCts = new System.Threading.CancellationTokenSource();
|
|
||||||
_ = ClearErrorsAfterDelay(_clearErrorCts.Token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ClearErrorsAfterDelay(System.Threading.CancellationToken token)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(10000, token);
|
|
||||||
errorList = [];
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -39,9 +39,8 @@ builder.Services.Configure<ApiKeys>(builder.Configuration.GetSection("ApiKeys"))
|
|||||||
builder.Services.AddTransient<ILocalStorage, LocalStorage>();
|
builder.Services.AddTransient<ILocalStorage, LocalStorage>();
|
||||||
builder.Services.AddScoped<ShogiService>();
|
builder.Services.AddScoped<ShogiService>();
|
||||||
builder.Services.AddScoped<GameHubNode>();
|
builder.Services.AddScoped<GameHubNode>();
|
||||||
builder.Services.AddScoped<CookieAuthenticationStateProvider>();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<CookieAuthenticationStateProvider>());
|
builder.Services.AddScoped<IAccountManagement, ServerAccountManager>();
|
||||||
builder.Services.AddScoped<IAccountManagement>(sp => sp.GetRequiredService<CookieAuthenticationStateProvider>());
|
|
||||||
|
|
||||||
AddIdentity(builder, builder.Configuration);
|
AddIdentity(builder, builder.Configuration);
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
|
|||||||
@@ -1,35 +1,22 @@
|
|||||||
{
|
{
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"Kestrel": {
|
"Shogi UI": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "",
|
"environmentVariables": {
|
||||||
"environmentVariables": {
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
},
|
||||||
"VaultUri": "https://gameboardshogiuisocketsv.vault.azure.net/",
|
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||||
"AZURE_USERNAME": "Hauth@live.com"
|
|
||||||
},
|
},
|
||||||
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
"Swagger UI": {
|
||||||
},
|
"commandName": "Project",
|
||||||
"Swagger": {
|
"launchBrowser": true,
|
||||||
"commandName": "Project",
|
"launchUrl": "swagger",
|
||||||
"launchBrowser": true,
|
"environmentVariables": {
|
||||||
"launchUrl": "swagger",
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
"environmentVariables": {
|
},
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||||
"VaultUri": "https://gameboardshogiuisocketsv.vault.azure.net/",
|
|
||||||
"AZURE_USERNAME": "Hauth@live.com"
|
|
||||||
},
|
|
||||||
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "http://localhost:50728/",
|
|
||||||
"sslPort": 44315
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user