initial commit
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
@page "/account/forgot-password"
|
||||
@layout AuthenticationLayout
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Indotalent.Shared.Consts
|
||||
@using Microsoft.JSInterop
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject ISnackbar Snackbar
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<PageTitle>Forgot Password - @GlobalConsts.AppInitial</PageTitle>
|
||||
|
||||
<style>
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
font-size: 0.8125rem;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: #594AE2;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.auth-link:hover {
|
||||
color: #4A3DC8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Forgot Password?</h2>
|
||||
<p>Enter your email address below and we'll send you a link to reset your password.</p>
|
||||
</div>
|
||||
|
||||
<MudForm @ref="form" @bind-IsValid="@success">
|
||||
|
||||
<label class="form-label" for="email-input">Email Address</label>
|
||||
<MudTextField T="string"
|
||||
@bind-Value="model.Email"
|
||||
id="email-input"
|
||||
Variant="Variant.Outlined"
|
||||
Placeholder="name@example.com"
|
||||
Margin="Margin.Dense"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Email"
|
||||
Required="true"
|
||||
Validation="@(new EmailAddressAttribute() { ErrorMessage = "Invalid email address format" })"
|
||||
For="@(() => model.Email)"
|
||||
Class="mb-5" />
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
FullWidth="true"
|
||||
Disabled="!success || isProcessing"
|
||||
OnClick="HandleForgotPassword"
|
||||
Style="text-transform:none; border-radius: 0.5rem; height: 48px; font-weight: 600;">
|
||||
@if (isProcessing)
|
||||
{
|
||||
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true" Color="Color.Inherit" />
|
||||
<span class="ms-2">Sending link...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Send Reset Link</span>
|
||||
}
|
||||
</MudButton>
|
||||
</MudForm>
|
||||
|
||||
<div style="text-align: center; margin-top: 1.5rem;">
|
||||
<span style="font-size: 0.875rem; color: #64748b;">Remember your password? </span>
|
||||
<a class="auth-link" href="/account/login">Back to Sign In</a>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private MudForm form = default!;
|
||||
private bool success;
|
||||
private bool isProcessing;
|
||||
private ForgotPasswordModel model = new();
|
||||
|
||||
private class ForgotPasswordModel
|
||||
{
|
||||
public string Email { get; set; } = "";
|
||||
}
|
||||
|
||||
private async Task HandleForgotPassword()
|
||||
{
|
||||
await form.Validate();
|
||||
if (!success) return;
|
||||
|
||||
isProcessing = true;
|
||||
StateHasChanged();
|
||||
|
||||
var result = await JSRuntime.InvokeAsync<ApiResponse>("apiForgotPassword", model.Email);
|
||||
|
||||
if (result.Status == 200)
|
||||
{
|
||||
Snackbar.Add("If your email is registered, a reset link has been sent.", Severity.Success);
|
||||
await Task.Delay(500);
|
||||
Navigation.NavigateTo("/account/login");
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(result.Title ?? "Failed to send reset link", Severity.Error);
|
||||
}
|
||||
|
||||
isProcessing = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private class ApiResponse
|
||||
{
|
||||
public int Status { get; set; }
|
||||
public string? Title { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
<script>
|
||||
window.apiForgotPassword = async (email) => {
|
||||
try {
|
||||
const response = await fetch('/api/account/forgot-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return { status: 200, title: 'Success' };
|
||||
} else {
|
||||
const data = await response.json();
|
||||
let msg = 'Error processing request';
|
||||
if (data.errors && data.errors.length > 0) msg = data.errors[0];
|
||||
return { status: response.status, title: msg };
|
||||
}
|
||||
} catch (error) {
|
||||
return { status: 500, title: 'Network error' };
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,70 @@
|
||||
using Indotalent.Data.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Text;
|
||||
|
||||
namespace Indotalent.Features.Account.ForgotPassword.Cqrs;
|
||||
|
||||
public class ForgotPasswordRequest
|
||||
{
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ForgotPasswordResponse
|
||||
{
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public record ForgotPasswordCommand(ForgotPasswordRequest Data) : IRequest<ForgotPasswordResponse>;
|
||||
|
||||
public class ForgotPasswordHandler : IRequestHandler<ForgotPasswordCommand, ForgotPasswordResponse>
|
||||
{
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IEmailSender<ApplicationUser> _emailSender;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public ForgotPasswordHandler(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IEmailSender<ApplicationUser> emailSender,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_emailSender = emailSender;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public async Task<ForgotPasswordResponse> Handle(ForgotPasswordCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await _userManager.FindByEmailAsync(request.Data.Email);
|
||||
|
||||
if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
|
||||
{
|
||||
return new ForgotPasswordResponse { Message = "If your email is registered, you will receive a reset link." };
|
||||
}
|
||||
|
||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
|
||||
|
||||
var requestHttp = _httpContextAccessor.HttpContext?.Request;
|
||||
var scheme = requestHttp?.Scheme ?? "https";
|
||||
var host = requestHttp?.Host.Value ?? "localhost:8080";
|
||||
|
||||
var callbackUrl = $"{scheme}://{host}/account/reset-password?userId={user.Id}&code={code}";
|
||||
|
||||
var message = $@"
|
||||
<div style=""font-family: Arial, sans-serif;"">
|
||||
<h3>Password Reset Request</h3>
|
||||
<p>Hello, {user.FullName}!</p>
|
||||
<p>We received a request to reset your password. Click the link below to proceed:</p>
|
||||
<p><a href=""{callbackUrl}"" style=""color: #2196F3; font-weight: bold;"">Reset Password</a></p>
|
||||
<br/>
|
||||
<p style=""font-size: 0.8em; color: #666;"">If you didn't request this, you can safely ignore this email.</p>
|
||||
<p style=""font-size: 0.8em; color: #666;"">{callbackUrl}</p>
|
||||
</div>";
|
||||
|
||||
await _emailSender.SendPasswordResetLinkAsync(user, user.Email!, message);
|
||||
|
||||
return new ForgotPasswordResponse { Message = "Reset link has been sent." };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user