Forgot password

This commit is contained in:
2024-11-26 17:42:46 +00:00
parent 964f3bfb30
commit 1ed1ad09af
6 changed files with 193 additions and 14 deletions

View File

@@ -1,6 +1,7 @@
namespace Shogi.UI.Identity;
using Microsoft.AspNetCore.Components.Authorization;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Text;
@@ -235,6 +236,38 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
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("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("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; }

View File

@@ -28,4 +28,6 @@ public interface IAccountManagement
public Task<FormResult> RegisterAsync(string email, string password);
public Task<bool> CheckAuthenticatedAsync();
Task<HttpResponseMessage> RequestPasswordReset(string email);
Task<FormResult> ChangePassword(string email, string resetCode, string newPassword);
}

View File

@@ -0,0 +1,125 @@
@page "/forgot"
@inject IAccountManagement Acct
<main class="PrimaryTheme">
<h1>Forgot Password</h1>
@if (isReset)
{
<p>Your password has been reset. Log in with your new password any time.</p>
} else if (isCodeSent)
{
<p>Look for an email from shogi@lucaserver.space with a reset code and fill out the form.</p>
}
<section class="ForgotForm">
@if (errorList.Length > 0)
{
<ul class="Errors" style="grid-area: errors">
@foreach (var error in errorList)
{
<li>@error</li>
}
</ul>
}
<label>
Email
@if (!isCodeSent)
{
<input required name="email" type="email" @bind-value="email" />
}
else
{
<span>@email</span>
}
</label>
@if (!isCodeSent)
{
<button @onclick="SendResetCode">Send a reset code</button>
}
else
{
<label>
Reset code
<input required name="resetCode" type="text" @bind-value="code" />
</label>
<label>
New Password
<input required name="newPassword" type="password" @bind-value="newPassword" />
</label>
<label>
Confirm Password
<input required name="confirmPassword" type="password" @bind-value="confirmPassword" />
</label>
<button @onclick="ChangePassword">Change my password</button>
}
</section>
</main>
@code {
private bool isCodeSent = false;
private bool isReset = false;
private string email = string.Empty;
private string code = string.Empty;
private string newPassword = string.Empty;
private string confirmPassword = string.Empty;
private string[] errorList = [];
async Task SendResetCode()
{
if (string.IsNullOrWhiteSpace(email))
{
errorList = ["Email is required"];
return;
}
var response = await Acct.RequestPasswordReset(email);
isCodeSent = response.IsSuccessStatusCode;
if (!response.IsSuccessStatusCode)
{
errorList = [await response.Content.ReadAsStringAsync()];
}
}
async Task ChangePassword()
{
var errors = new List<string>(5);
if (string.IsNullOrWhiteSpace(email))
{
errors.Add("Email is required");
}
if (string.IsNullOrWhiteSpace(code))
{
errors.Add("Reset code is required");
}
if (string.IsNullOrWhiteSpace(newPassword))
{
errors.Add("New password is required");
}
if (!newPassword.Equals(confirmPassword))
{
errors.Add("Your new password and confirm password do not match");
}
var result = await Acct.ChangePassword(email, code, newPassword);
if (result.Succeeded)
{
isReset = true;
ClearFormFields();
}
else
{
errorList = result.ErrorList;
}
}
void ClearFormFields() {
email = code = newPassword = confirmPassword = string.Empty;
}
}

View File

@@ -0,0 +1,25 @@
main {
padding: 1rem;
}
.ForgotForm {
display: inline-flex;
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.ForgotForm label {
display: flex;
justify-content: space-between;
gap: 1rem;
}
.ForgotForm button {
align-self: end;
}
.ForgotForm .Errors {
color: darkred;
background-color: var(--foregroundColor)
}

View File

@@ -27,6 +27,8 @@
<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>
<button style="grid-area: button" @onclick="DoLoginAsync">Login</button>
</NotAuthorized>
</AuthorizeView>

View File

@@ -1,28 +1,20 @@
main {
/*display: grid;
grid-template-areas:
"header header header"
". form ."
". . .";
grid-template-rows: auto 1fr 1fr;
place-items: center;
*/
padding: 1rem;
}
.LoginForm {
grid-area: form;
display: inline-grid;
grid-template-areas:
"errors errors"
"emailLabel emailControl"
"passLabel passControl"
". resetLink"
"button button";
gap: 0.5rem 3rem;
}
.LoginForm .Errors {
color: darkred;
}
.LoginForm .Errors {
color: darkred;
background-color: var(--foregroundColor);
}