initial commit

This commit is contained in:
2026-07-21 14:41:46 +07:00
commit 1bfa3dd1c2
1159 changed files with 88228 additions and 0 deletions
@@ -0,0 +1,151 @@
@page "/account/reset-password"
@layout AuthenticationLayout
@using Indotalent.Shared.Consts
@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime
@inject ISnackbar Snackbar
@inject NavigationManager Navigation
<PageTitle>Reset 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: #1e293b; }
.page-header p { font-size: 0.875rem; color: #64748b; margin-top: 0.25rem; line-height: 1.5; }
.form-label { display: block; font-size: 0.8125rem; font-weight: 600; color: #475569; margin-bottom: 0.375rem; }
</style>
<div class="page-header">
<h2>Create New Password</h2>
<p>Please enter your new password below.</p>
</div>
<MudForm @ref="form" @bind-IsValid="@success">
<label class="form-label">New Password</label>
<MudTextField T="string"
@bind-Value="model.NewPassword"
Variant="Variant.Outlined"
Placeholder="Enter new password"
Margin="Margin.Dense"
InputType="@(showPassword ? InputType.Text : InputType.Password)"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lock"
AdornmentEndIcon="@(showPassword? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff)"
OnAdornmentEndClick="() => showPassword = !showPassword"
Required="true"
Class="mb-4" />
<label class="form-label">Confirm New Password</label>
<MudTextField T="string"
@bind-Value="model.ConfirmPassword"
Variant="Variant.Outlined"
Placeholder="Repeat new password"
Margin="Margin.Dense"
InputType="@(showPassword ? InputType.Text : InputType.Password)"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lock"
Required="true"
Validation="@(new Func<string, string?>(PasswordMatch))"
Class="mb-6" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
Disabled="!success || isProcessing"
OnClick="HandleResetPassword"
Style="text-transform:none; border-radius: 8px; height: 48px; font-weight: 600;">
@if (isProcessing)
{
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true" />
<span class="ms-2">Resetting...</span>
}
else
{
<span>Reset Password</span>
}
</MudButton>
</MudForm>
@code {
[SupplyParameterFromQuery] public string? userId { get; set; }
[SupplyParameterFromQuery] public string? code { get; set; }
private MudForm form = default!;
private bool success;
private bool isProcessing;
private bool showPassword;
private ResetModel model = new();
private class ResetModel
{
public string NewPassword { get; set; } = "";
public string ConfirmPassword { get; set; } = "";
}
private string? PasswordMatch(string arg)
{
if (model.NewPassword != arg) return "Passwords do not match";
return null;
}
private async Task HandleResetPassword()
{
await form.Validate();
if (!success) return;
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(code))
{
Snackbar.Add("Invalid reset token or user ID.", Severity.Error);
return;
}
isProcessing = true;
StateHasChanged();
var result = await JSRuntime.InvokeAsync<ApiResponse>("apiResetPassword", userId, code, model.NewPassword);
if (result.Status == 200)
{
Snackbar.Add("Password reset successful! You can now sign in.", Severity.Success);
await Task.Delay(500);
Navigation.NavigateTo("/account/login");
}
else
{
Snackbar.Add(result.Title ?? "Failed to reset password", Severity.Error);
}
isProcessing = false;
StateHasChanged();
}
private class ApiResponse
{
public int Status { get; set; }
public string? Title { get; set; }
}
}
<script>
window.apiResetPassword = async (userId, code, newPassword) => {
try {
const response = await fetch('/api/account/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, code, newPassword })
});
if (response.ok) {
return { status: 200, title: 'Success' };
} else {
const data = await response.json();
let msg = 'Failed to reset password';
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,51 @@
using Indotalent.Data.Entities;
using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.WebUtilities;
using System.Text;
namespace Indotalent.Features.Account.ResetPassword.Cqrs;
public class ResetPasswordRequest
{
public string UserId { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
public string NewPassword { get; set; } = string.Empty;
}
public class ResetPasswordResponse
{
public string Message { get; set; } = string.Empty;
}
public record ResetPasswordCommand(ResetPasswordRequest Data) : IRequest<ResetPasswordResponse>;
public class ResetPasswordHandler : IRequestHandler<ResetPasswordCommand, ResetPasswordResponse>
{
private readonly UserManager<ApplicationUser> _userManager;
public ResetPasswordHandler(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task<ResetPasswordResponse> Handle(ResetPasswordCommand request, CancellationToken cancellationToken)
{
var user = await _userManager.FindByIdAsync(request.Data.UserId);
if (user == null)
{
throw new Exception("User not found.");
}
var decodedCode = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(request.Data.Code));
var result = await _userManager.ResetPasswordAsync(user, decodedCode, request.Data.NewPassword);
if (!result.Succeeded)
{
var errors = result.Errors.Select(x => x.Description).ToList();
throw new Exception(string.Join(", ", errors));
}
return new ResetPasswordResponse { Message = "Password has been reset successfully." };
}
}