27a3ea76c5
- Introduced a new section in the Profile hub for viewing invitees, enhancing user engagement and tracking of referrals. - Added buttons for accessing the invitees page from the Profile index, improving navigation. - Enhanced the AuthDialog and PhoneVerifyForm components to support inviter mobile input, allowing users to specify who referred them. - Updated RouteConstants to include a new route for invitees, ensuring proper navigation within the application. These changes improve the user experience by facilitating better referral tracking and enhancing the profile management features.
487 lines
17 KiB
C#
487 lines
17 KiB
C#
using Blazored.LocalStorage;
|
|
using CMSMicroservice.Protobuf.Protos.User;
|
|
using FrontOffice.Main.Utilities;
|
|
using Grpc.Core;
|
|
using Microsoft.AspNetCore.Components;
|
|
using MudBlazor;
|
|
using MetadataAlias = Grpc.Core.Metadata; // resolve ambiguity with MudBlazor.Metadata
|
|
|
|
namespace FrontOffice.Main.Shared;
|
|
|
|
public partial class AuthDialog : IDisposable
|
|
{
|
|
[Parameter] public bool HideCancelButton { get; set; }
|
|
// [Parameter] public bool EnableCaptcha { get; set; }
|
|
[Parameter] public bool InlineMode { get; set; }
|
|
|
|
public enum AuthStep { Phone, Verify }
|
|
private const int DefaultResendCooldown = 120;
|
|
public const int MaxVerificationAttempts = 5;
|
|
private const string PhoneStorageKey = "auth:phone-number";
|
|
private const string RedirectStorageKey = "auth:redirect";
|
|
private const string TokenStorageKey = "auth:token";
|
|
private const string OtpPurpose = "Login";
|
|
|
|
public AuthStep _currentStep = AuthStep.Phone;
|
|
|
|
private readonly CreateNewOtpTokenRequest _phoneRequest = new();
|
|
private PhoneVerifyForm? _phoneVerifyForm;
|
|
|
|
private readonly VerifyOtpTokenRequest _verifyRequest = new();
|
|
|
|
private bool _isBusy;
|
|
private string? _phoneNumber;
|
|
private string? _errorMessage;
|
|
private string? _infoMessage;
|
|
private Timer? _resendTimer;
|
|
private int _resendRemaining;
|
|
private int _attemptsLeft = MaxVerificationAttempts;
|
|
private CancellationTokenSource? _operationCts;
|
|
|
|
// Captcha fields
|
|
private string? _captchaCode;
|
|
private string? _captchaInput;
|
|
|
|
// Referral code field
|
|
private string? _referralCode;
|
|
// Inviter mobile (دعوتکننده)
|
|
private string? _inviterMobile;
|
|
|
|
[Inject] private ILocalStorageService LocalStorage { get; set; } = default!;
|
|
[Inject] private UserContract.UserContractClient UserClient { get; set; } = default!;
|
|
|
|
[CascadingParameter]
|
|
private IMudDialogInstance MudDialog { get; set; }
|
|
|
|
[Parameter] public EventCallback OnLoginSuccess { get; set; }
|
|
|
|
private bool IsVerificationLocked => _attemptsLeft <= 0;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
_phoneRequest.Purpose = OtpPurpose;
|
|
_verifyRequest.Purpose = OtpPurpose;
|
|
|
|
// if (EnableCaptcha)
|
|
// {
|
|
GenerateCaptcha();
|
|
// }
|
|
var storedPhone = await LocalStorage.GetItemAsync<string>(PhoneStorageKey);
|
|
if (!string.IsNullOrWhiteSpace(storedPhone))
|
|
{
|
|
_phoneRequest.Mobile = storedPhone;
|
|
}
|
|
|
|
_referralCode = await LocalStorage.GetItemAsync<string>("referral:code");
|
|
_inviterMobile = await LocalStorage.GetItemAsync<string>("inviter:mobile");
|
|
// await LocalStorage.RemoveItemAsync(TokenStorageKey);
|
|
}
|
|
|
|
private async Task OnReferralCodeChanged(string? value)
|
|
{
|
|
_referralCode = value;
|
|
if (!string.IsNullOrWhiteSpace(value))
|
|
await LocalStorage.SetItemAsync("referral:code", value);
|
|
else
|
|
await LocalStorage.RemoveItemAsync("referral:code");
|
|
}
|
|
|
|
private async Task OnInviterMobileChanged(string? value)
|
|
{
|
|
_inviterMobile = value;
|
|
if (!string.IsNullOrWhiteSpace(value))
|
|
await LocalStorage.SetItemAsync("inviter:mobile", value);
|
|
else
|
|
await LocalStorage.RemoveItemAsync("inviter:mobile");
|
|
}
|
|
|
|
private void GenerateCaptcha()
|
|
{
|
|
_captchaCode = Guid.NewGuid().ToString("N")[..6].ToUpperInvariant();
|
|
_captchaInput = string.Empty;
|
|
}
|
|
|
|
public async Task SendOtpAsync()
|
|
{
|
|
_errorMessage = null;
|
|
var phoneForm = _phoneVerifyForm?.GetPhoneForm();
|
|
if (phoneForm is null)
|
|
return;
|
|
|
|
await phoneForm.Validate();
|
|
if (!phoneForm.IsValid)
|
|
return;
|
|
|
|
// if (EnableCaptcha)
|
|
// {
|
|
if (string.IsNullOrWhiteSpace(_captchaInput) || !string.Equals(_captchaInput.Trim(), _captchaCode, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_errorMessage = "کد کپچا صحیح نیست.";
|
|
return;
|
|
}
|
|
// }
|
|
|
|
_isBusy = true;
|
|
_operationCts?.Cancel();
|
|
_operationCts?.Dispose();
|
|
_operationCts = new CancellationTokenSource();
|
|
|
|
try
|
|
{
|
|
var validationResult = true; // _phoneRequestValidator.Validate(_phoneRequest);
|
|
if (!validationResult)
|
|
{
|
|
_errorMessage = "لطفاً شماره تلفن معتبر وارد کنید."; // string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct());
|
|
return;
|
|
}
|
|
|
|
var metadata = await BuildAuthMetadataAsync();
|
|
CreateNewOtpTokenResponse response = metadata is not null
|
|
? await UserClient.CreateNewOtpTokenAsync(_phoneRequest, metadata, cancellationToken: _operationCts.Token)
|
|
: await UserClient.CreateNewOtpTokenAsync(_phoneRequest, cancellationToken: _operationCts.Token);
|
|
|
|
if (response?.Success != true)
|
|
{
|
|
_errorMessage = string.IsNullOrWhiteSpace(response?.Message)
|
|
? "ارسال رمز پویا با خطا مواجه شد. لطفاً دوباره تلاش کنید."
|
|
: response!.Message;
|
|
return;
|
|
}
|
|
|
|
await LocalStorage.SetItemAsync(PhoneStorageKey, _phoneRequest.Mobile);
|
|
_phoneNumber = _phoneRequest.Mobile;
|
|
_currentStep = AuthStep.Verify;
|
|
StartResendCountdown();
|
|
}
|
|
catch (RpcException rpcEx)
|
|
{
|
|
_errorMessage = !string.IsNullOrWhiteSpace(rpcEx.Status.Detail)
|
|
? rpcEx.Status.Detail
|
|
: "ارسال رمز پویا با خطا مواجه شد. لطفاً دوباره تلاش کنید.";
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// ignored - user canceled operation
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_errorMessage = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
_isBusy = false;
|
|
ClearOperationToken();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
public async Task<bool> VerifyOtpAsync()
|
|
{
|
|
_errorMessage = null;
|
|
_infoMessage = null;
|
|
|
|
var verifyForm = _phoneVerifyForm?.GetVerifyForm();
|
|
if (verifyForm is null)
|
|
return false;
|
|
|
|
await verifyForm.Validate();
|
|
if (!verifyForm.IsValid)
|
|
return false;
|
|
|
|
if (IsVerificationLocked)
|
|
{
|
|
_errorMessage = "تعداد تلاشهای مجاز به پایان رسیده است. لطفاً رمز جدید دریافت کنید.";
|
|
return false;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(_phoneNumber))
|
|
{
|
|
_errorMessage = "شماره موبایل یافت نشد. لطفاً دوباره تلاش کنید.";
|
|
return false;
|
|
}
|
|
|
|
_isBusy = true;
|
|
var cancellationToken = PrepareOperationToken();
|
|
|
|
try
|
|
{
|
|
_verifyRequest.Mobile = _phoneNumber;
|
|
|
|
var storedReferralCode = await LocalStorage.GetItemAsync<string>("referral:code");
|
|
var effectiveReferralCode = !string.IsNullOrWhiteSpace(_referralCode)
|
|
? _referralCode
|
|
: storedReferralCode;
|
|
if (!string.IsNullOrWhiteSpace(effectiveReferralCode))
|
|
{
|
|
_verifyRequest.ParentReferralCode = effectiveReferralCode;
|
|
}
|
|
|
|
var storedInviterMobile = await LocalStorage.GetItemAsync<string>("inviter:mobile");
|
|
var effectiveInviterMobile = !string.IsNullOrWhiteSpace(_inviterMobile)
|
|
? _inviterMobile
|
|
: storedInviterMobile;
|
|
if (!string.IsNullOrWhiteSpace(effectiveInviterMobile))
|
|
{
|
|
_verifyRequest.InviterMobile = effectiveInviterMobile;
|
|
}
|
|
|
|
var validationResult = true; // _verifyRequestValidator.Validate(_verifyRequest);
|
|
if (!validationResult)
|
|
{
|
|
_errorMessage = "لطفاً کد تأیید معتبر وارد کنید."; // string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct());
|
|
return false;
|
|
}
|
|
|
|
var metadata = await BuildAuthMetadataAsync();
|
|
VerifyOtpTokenResponse response = metadata is not null
|
|
? await UserClient.VerifyOtpTokenAsync(_verifyRequest, metadata, cancellationToken: cancellationToken)
|
|
: await UserClient.VerifyOtpTokenAsync(_verifyRequest, cancellationToken: cancellationToken);
|
|
|
|
if (response is null)
|
|
{
|
|
_errorMessage = "تأیید رمز پویا انجام نشد. لطفاً دوباره تلاش کنید.";
|
|
return false;
|
|
}
|
|
|
|
if (response.Success)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(response.Token))
|
|
{
|
|
await LocalStorage.SetItemAsync(TokenStorageKey, response.Token);
|
|
}
|
|
else
|
|
{
|
|
await LocalStorage.RemoveItemAsync(TokenStorageKey);
|
|
}
|
|
|
|
await LocalStorage.RemoveItemAsync(PhoneStorageKey);
|
|
await LocalStorage.RemoveItemAsync(RedirectStorageKey);
|
|
await LocalStorage.RemoveItemAsync("referral:code");
|
|
await LocalStorage.RemoveItemAsync("inviter:mobile");
|
|
|
|
_attemptsLeft = MaxVerificationAttempts;
|
|
_verifyRequest.Code = string.Empty;
|
|
|
|
|
|
|
|
if (!InlineMode)
|
|
{
|
|
MudDialog?.Close();
|
|
}
|
|
|
|
await OnLoginSuccess.InvokeAsync();
|
|
await InvokeAsync(StateHasChanged);
|
|
MainService.OnOnChangeHandler();
|
|
// await OnLoginSuccessAsync();
|
|
return true;
|
|
}
|
|
|
|
// اگه سرور RemainingAttempts برگردونده، از اون استفاده کن
|
|
if (response.RemainingAttempts > 0)
|
|
_attemptsLeft = response.RemainingAttempts;
|
|
|
|
RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message);
|
|
}
|
|
catch (RpcException rpcEx)
|
|
{
|
|
await HandleVerificationFailureAsync(rpcEx);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// ignored - user canceled operation
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_errorMessage = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
_isBusy = false;
|
|
ClearOperationToken();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
private async Task HandleVerificationFailureAsync(RpcException rpcEx)
|
|
{
|
|
switch (rpcEx.Status.StatusCode)
|
|
{
|
|
case StatusCode.PermissionDenied:
|
|
case StatusCode.InvalidArgument:
|
|
RegisterFailedAttempt(!string.IsNullOrWhiteSpace(rpcEx.Status.Detail)
|
|
? rpcEx.Status.Detail
|
|
: "کد نادرست است.");
|
|
break;
|
|
case StatusCode.Unauthenticated:
|
|
await ResetAuthenticationAsync();
|
|
_errorMessage = "نشست کاربری منقضی شده است. لطفاً دوباره وارد شوید.";
|
|
_currentStep = AuthStep.Phone;
|
|
break;
|
|
case StatusCode.DeadlineExceeded:
|
|
case StatusCode.NotFound:
|
|
_errorMessage = "کد منقضی شده است. لطفاً رمز جدید دریافت کنید.";
|
|
break;
|
|
default:
|
|
_errorMessage = !string.IsNullOrWhiteSpace(rpcEx.Status.Detail)
|
|
? rpcEx.Status.Detail
|
|
: "تأیید رمز پویا انجام نشد. لطفاً دوباره تلاش کنید.";
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void RegisterFailedAttempt(string baseMessage)
|
|
{
|
|
_attemptsLeft = Math.Max(0, _attemptsLeft - 1);
|
|
_errorMessage = _attemptsLeft > 0
|
|
? $"{baseMessage} {_attemptsLeft} تلاش باقی مانده است."
|
|
: $"{baseMessage} تلاشهای مجاز شما به پایان رسیده است. لطفاً رمز جدید دریافت کنید.";
|
|
}
|
|
|
|
private async Task ResendOtpAsync()
|
|
{
|
|
if (_resendRemaining > 0 || _isBusy || string.IsNullOrWhiteSpace(_phoneNumber))
|
|
return;
|
|
|
|
_errorMessage = null;
|
|
_infoMessage = null;
|
|
_isBusy = true;
|
|
var cancellationToken = PrepareOperationToken();
|
|
|
|
try
|
|
{
|
|
var request = new CreateNewOtpTokenRequest { Mobile = _phoneNumber, Purpose = OtpPurpose };
|
|
var metadata = await BuildAuthMetadataAsync();
|
|
CreateNewOtpTokenResponse response = metadata is not null
|
|
? await UserClient.CreateNewOtpTokenAsync(request, metadata, cancellationToken: cancellationToken)
|
|
: await UserClient.CreateNewOtpTokenAsync(request, cancellationToken: cancellationToken);
|
|
|
|
if (response?.Success != true)
|
|
{
|
|
_errorMessage = string.IsNullOrWhiteSpace(response?.Message)
|
|
? "ارسال مجدد رمز پویا با خطا مواجه شد."
|
|
: response!.Message;
|
|
return;
|
|
}
|
|
|
|
_infoMessage = string.IsNullOrWhiteSpace(response.Message) ? "کد جدید ارسال شد." : response.Message;
|
|
_attemptsLeft = MaxVerificationAttempts;
|
|
_verifyRequest.Code = string.Empty;
|
|
StartResendCountdown();
|
|
}
|
|
catch (RpcException rpcEx)
|
|
{
|
|
await HandleResendFailureAsync(rpcEx);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// ignored
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_errorMessage = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
_isBusy = false;
|
|
ClearOperationToken();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
|
|
private async Task HandleResendFailureAsync(RpcException rpcEx)
|
|
{
|
|
switch (rpcEx.Status.StatusCode)
|
|
{
|
|
case StatusCode.Unauthenticated:
|
|
await ResetAuthenticationAsync();
|
|
_errorMessage = "نشست کاربری منقضی شده است. لطفاً دوباره وارد شوید.";
|
|
_currentStep = AuthStep.Phone;
|
|
break;
|
|
default:
|
|
_errorMessage = !string.IsNullOrWhiteSpace(rpcEx.Status.Detail)
|
|
? rpcEx.Status.Detail
|
|
: "ارسال مجدد رمز پویا با خطا مواجه شد.";
|
|
break;
|
|
}
|
|
}
|
|
|
|
private async Task ChangePhoneAsync()
|
|
{
|
|
await LocalStorage.RemoveItemAsync(PhoneStorageKey);
|
|
_currentStep = AuthStep.Phone;
|
|
_phoneNumber = null;
|
|
_verifyRequest.Code = string.Empty;
|
|
_attemptsLeft = MaxVerificationAttempts;
|
|
_resendTimer?.Dispose();
|
|
_resendTimer = null;
|
|
_resendRemaining = 0;
|
|
// if (EnableCaptcha)
|
|
GenerateCaptcha();
|
|
}
|
|
|
|
private void StartResendCountdown(int seconds = DefaultResendCooldown)
|
|
{
|
|
_resendRemaining = seconds;
|
|
_resendTimer?.Dispose();
|
|
_resendTimer = new Timer(_ =>
|
|
{
|
|
var remaining = Interlocked.Add(ref _resendRemaining, -1);
|
|
if (remaining <= 0)
|
|
{
|
|
Interlocked.Exchange(ref _resendRemaining, 0);
|
|
_resendTimer?.Dispose();
|
|
_resendTimer = null;
|
|
}
|
|
_ = InvokeAsync(StateHasChanged);
|
|
}, null, 1000, 1000);
|
|
}
|
|
|
|
private async Task<MetadataAlias?> BuildAuthMetadataAsync()
|
|
{
|
|
var token = await LocalStorage.GetItemAsync<string>(TokenStorageKey);
|
|
if (string.IsNullOrWhiteSpace(token)) return null;
|
|
return new MetadataAlias { { "Authorization", $"Bearer {token}" } };
|
|
}
|
|
|
|
private async Task ResetAuthenticationAsync()
|
|
{
|
|
await LocalStorage.RemoveItemAsync(TokenStorageKey);
|
|
await LocalStorage.RemoveItemAsync(PhoneStorageKey);
|
|
await LocalStorage.RemoveItemAsync(RedirectStorageKey);
|
|
}
|
|
|
|
private CancellationToken PrepareOperationToken()
|
|
{
|
|
_operationCts?.Cancel();
|
|
_operationCts?.Dispose();
|
|
_operationCts = new CancellationTokenSource();
|
|
return _operationCts.Token;
|
|
}
|
|
|
|
private void ClearOperationToken()
|
|
{
|
|
_operationCts?.Dispose();
|
|
_operationCts = null;
|
|
}
|
|
|
|
private void Cancel()
|
|
{
|
|
if (!InlineMode)
|
|
MudDialog?.Close();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_operationCts?.Cancel();
|
|
_operationCts?.Dispose();
|
|
_operationCts = null;
|
|
_resendTimer?.Dispose();
|
|
_resendTimer = null;
|
|
}
|
|
|
|
private string GetDialogTitle() => _currentStep == AuthStep.Phone ? "ورود/ثبتنام به حساب کاربری" : "تأیید رمز پویا";
|
|
}
|