@page "/password-recovery" @inherits LocalizableComponentBase @Localizer["Forgot your password?"] @* A forgotten password (PrivaPub's /clientapi/user/recover): the login's name, or its verified address, asks for a link, and PrivaPub answers everyone with the same sentence, so the page never tells whether a login or an address exists. The link comes back here with its code (?rc=), which leaves the address bar and the history at once and stays in the page; a new password set with it ends every session the login had, and the reader signs in with it. *@
@if (CascadingState.IsSignedIn) {

@Localizer["You are signed in"]

@Localizer["Home"]
} else if (recoveryCode is null && sentence is not null) {

@Localizer["Check your inbox"]

@sentence

@Localizer["Login"]
} else if (recoveryCode is null) {

@Localizer["Forgot your password?"]

@Localizer["Give your login's username or its email address: if the login has a verified address, a link to choose a new password is sent to it."]

@Localizer["or"] @if (isWorking) { } @Localizer["Send the recovery link"] @Localizer["I remember it: sign in"]
} else if (IsLoading) { } else if (isCodeValid is false) {

@Localizer["This recovery link doesn't work"]

@Localizer["A link works for one hour, and only once. Ask for a new one."]

@Localizer["Ask for a new link"]
} else { @* also while the code couldn't be checked (offline, or the check failed): the answer to the new password says *@

@Localizer["Choose a new password"]

@Localizer["Every device signed in to this login is signed out."]

@if (isWorking) { } @Localizer["Change the password"]
}
@code { [Inject] AuthService AuthService { get; set; } readonly PasswordRecoveryForm request = new(); NewPasswordForm newPassword = new(); /// The code of the link the page was opened from, kept here once it left the address bar. string recoveryCode; /// Whether the code works: null until PrivaPub said. bool? isCodeValid; /// PrivaPub's answer to a request, the same for everyone. string sentence; string error; bool isWorking; [SupplyParameterFromQuery(Name = "rc")] public string LinkCode { get; set; } protected override async Task OnInitializedAsync() { if (LinkCode is { Length: > 0 } code) { recoveryCode = code.Trim(); newPassword.RecoveryCode = recoveryCode; // the code is as good as the password for an hour: out of the address bar and the history at once Navigation.NavigateTo("password-recovery", replace: true); } if (CascadingState.IsOffline) { WhenOnline(LoadData); StartOnlinePolling(); IsLoading = false; return; } await LoadData(); IsLoading = false; } async Task LoadData() { if (recoveryCode is null) return; var result = await AuthService.CheckRecoveryCode(recoveryCode); if (result.IsInvalid) { error = result.ErrorMessage; return; } isCodeValid = result.Data is true; } static string NullIfBlank(string value) => string.IsNullOrWhiteSpace(value) ? null : value; async Task Request() { error = default; isWorking = true; request.UserName = request.UserName?.Trim(); request.Email = request.Email?.Trim().ToLowerInvariant(); var result = await AuthService.RequestRecovery(request); isWorking = false; if (result.IsInvalid) { error = result.ErrorMessage; return; } sentence = result.Data is string { Length: > 0 } said ? said : Localizer["If that account has an email address, a recovery link is on its way. It works for one hour."]; } void AskAgain() { recoveryCode = null; isCodeValid = null; newPassword = new(); error = default; } async Task Recover() { error = default; isWorking = true; var result = await AuthService.Recover(newPassword); isWorking = false; if (result.StatusCode is (int)System.Net.HttpStatusCode.NotFound) { // used or expired meanwhile isCodeValid = false; return; } if (result.IsInvalid) { error = result.ErrorMessage; return; } Toast.AddResponseSuccess(Localizer["Password changed: sign in with the new one."]); Navigation.NavigateTo("login"); } }