221 lines
6.8 KiB
Plaintext
221 lines
6.8 KiB
Plaintext
@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 ===== */
|
|
.page-header {
|
|
text-align: center;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.page-header h2 {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: #1E293B;
|
|
margin: 0 0 0.25rem 0;
|
|
}
|
|
|
|
.page-header p {
|
|
font-size: 0.875rem;
|
|
color: #94A3B8;
|
|
margin: 0;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* ===== FORM LABEL ===== */
|
|
.form-label {
|
|
display: block;
|
|
font-size: 0.8125rem;
|
|
font-weight: 600;
|
|
color: #475569;
|
|
margin-bottom: 0.375rem;
|
|
}
|
|
|
|
/* ===== SUBMIT BUTTON ===== */
|
|
.btn-submit {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem 1.75rem;
|
|
background: #8B5CF6;
|
|
color: #ffffff;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
border-radius: 0.5rem;
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
border: none;
|
|
width: 100%;
|
|
font-family: "Poppins", sans-serif;
|
|
height: 48px;
|
|
}
|
|
|
|
.btn-submit:hover:not(:disabled) {
|
|
background: #7C3AED;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
|
|
}
|
|
|
|
.btn-submit:disabled {
|
|
opacity: 0.7;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
/* ===== SPINNER ANIMATION ===== */
|
|
@@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.spinner {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
</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">
|
|
|
|
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
<div>
|
|
<label class="form-label" for="new-password-field">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"
|
|
id="new-password-field" />
|
|
</div>
|
|
|
|
<div>
|
|
<label class="form-label" for="confirm-password-field">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))"
|
|
id="confirm-password-field" />
|
|
</div>
|
|
|
|
<button class="btn-submit" type="button" disabled="@(!success || isProcessing)" @onclick="HandleResetPassword">
|
|
@if (isProcessing)
|
|
{
|
|
<svg class="spinner" style="width: 1rem; height: 1rem;" fill="none" viewBox="0 0 24 24">
|
|
<circle style="opacity: 0.25;" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
<path style="opacity: 0.75;" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
|
</svg>
|
|
<span>Resetting...</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Reset Password</span>
|
|
}
|
|
</button>
|
|
</div>
|
|
</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> |