Forgot password
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
namespace Shogi.UI.Identity;
|
namespace Shogi.UI.Identity;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -235,6 +236,38 @@ public class CookieAuthenticationStateProvider : AuthenticationStateProvider, IA
|
|||||||
return _authenticated;
|
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 class RoleClaim
|
||||||
{
|
{
|
||||||
public string? Issuer { get; set; }
|
public string? Issuer { get; set; }
|
||||||
|
|||||||
@@ -28,4 +28,6 @@ public interface IAccountManagement
|
|||||||
public Task<FormResult> RegisterAsync(string email, string password);
|
public Task<FormResult> RegisterAsync(string email, string password);
|
||||||
|
|
||||||
public Task<bool> CheckAuthenticatedAsync();
|
public Task<bool> CheckAuthenticatedAsync();
|
||||||
|
Task<HttpResponseMessage> RequestPasswordReset(string email);
|
||||||
|
Task<FormResult> ChangePassword(string email, string resetCode, string newPassword);
|
||||||
}
|
}
|
||||||
|
|||||||
125
Shogi.UI/Pages/Identity/ForgotPassword.razor
Normal file
125
Shogi.UI/Pages/Identity/ForgotPassword.razor
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Shogi.UI/Pages/Identity/ForgotPassword.razor.css
Normal file
25
Shogi.UI/Pages/Identity/ForgotPassword.razor.css
Normal 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)
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@
|
|||||||
<label for="password" style="grid-area: passLabel">Password</label>
|
<label for="password" style="grid-area: passLabel">Password</label>
|
||||||
<input required id="password" name="passwordInput" type="password" style="grid-area: passControl" @bind-value="password" />
|
<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>
|
<button style="grid-area: button" @onclick="DoLoginAsync">Login</button>
|
||||||
</NotAuthorized>
|
</NotAuthorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
main {
|
main {
|
||||||
/*display: grid;
|
padding: 1rem;
|
||||||
grid-template-areas:
|
|
||||||
"header header header"
|
|
||||||
". form ."
|
|
||||||
". . .";
|
|
||||||
grid-template-rows: auto 1fr 1fr;
|
|
||||||
|
|
||||||
place-items: center;
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.LoginForm {
|
.LoginForm {
|
||||||
grid-area: form;
|
grid-area: form;
|
||||||
|
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"errors errors"
|
"errors errors"
|
||||||
"emailLabel emailControl"
|
"emailLabel emailControl"
|
||||||
"passLabel passControl"
|
"passLabel passControl"
|
||||||
|
". resetLink"
|
||||||
"button button";
|
"button button";
|
||||||
gap: 0.5rem 3rem;
|
gap: 0.5rem 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LoginForm .Errors {
|
.LoginForm .Errors {
|
||||||
color: darkred;
|
color: darkred;
|
||||||
|
background-color: var(--foregroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user