diff --git a/src/FrontOffice.Main/Pages/Auth/Phone.razor b/src/FrontOffice.Main/Pages/Auth/Phone.razor index cc6b662..24eeca7 100644 --- a/src/FrontOffice.Main/Pages/Auth/Phone.razor +++ b/src/FrontOffice.Main/Pages/Auth/Phone.razor @@ -1,4 +1,4 @@ -@page "/auth/phone" +@attribute [Route(RouteConstants.Auth.Phone)] ورود | تأیید شماره موبایل @@ -52,3 +52,4 @@ + diff --git a/src/FrontOffice.Main/Pages/Auth/Phone.razor.cs b/src/FrontOffice.Main/Pages/Auth/Phone.razor.cs index 3934c3d..9f4f4fa 100644 --- a/src/FrontOffice.Main/Pages/Auth/Phone.razor.cs +++ b/src/FrontOffice.Main/Pages/Auth/Phone.razor.cs @@ -1,10 +1,8 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.ComponentModel.DataAnnotations; using Blazored.LocalStorage; using FrontOffice.BFF.User.Protobuf.Protos.User; +using FrontOffice.BFF.User.Protobuf.Validator; +using FrontOffice.Main.Utilities; using Grpc.Core; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.WebUtilities; @@ -16,8 +14,11 @@ public partial class Phone : IDisposable { private const string PhoneStorageKey = "auth:phone-number"; private const string RedirectStorageKey = "auth:redirect"; + private const string TokenStorageKey = "auth:token"; private const string OtpPurpose = "Login"; + private static readonly CreateNewOtpTokenRequestValidator RequestValidator = new(); + private readonly PhoneInputModel _model = new(); private MudForm? _form; private bool _isBusy; @@ -80,7 +81,24 @@ public partial class Phone : IDisposable Purpose = OtpPurpose }; - var response = await UserClient.CreateNewOtpTokenAsync(request, cancellationToken: _sendCts.Token); + var validationResult = RequestValidator.Validate(request); + if (!validationResult.IsValid) + { + _errorMessage = string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct()); + return; + } + + var metadata = await BuildAuthMetadataAsync(); + CreateNewOtpTokenResponse response; + if (metadata is not null) + { + response = await UserClient.CreateNewOtpTokenAsync(request, metadata, cancellationToken: _sendCts.Token); + } + else + { + response = await UserClient.CreateNewOtpTokenAsync(request, cancellationToken: _sendCts.Token); + } + if (response?.Success != true) { _errorMessage = string.IsNullOrWhiteSpace(response?.Message) @@ -99,7 +117,7 @@ public partial class Phone : IDisposable await LocalStorage.RemoveItemAsync(RedirectStorageKey); } - var target = "/auth/verify?phone=" + Uri.EscapeDataString(_model.PhoneNumber); + var target = $"{RouteConstants.Auth.Verify}?phone={Uri.EscapeDataString(_model.PhoneNumber)}"; if (!string.IsNullOrEmpty(_redirect)) { target += "&redirect=" + Uri.EscapeDataString(_redirect); @@ -129,6 +147,20 @@ public partial class Phone : IDisposable } } + private async Task BuildAuthMetadataAsync() + { + var token = await LocalStorage.GetItemAsync(TokenStorageKey); + if (string.IsNullOrWhiteSpace(token)) + { + return null; + } + + return new Metadata + { + { "Authorization", $"Bearer {token}" } + }; + } + public void Dispose() { _sendCts?.Cancel(); @@ -138,12 +170,11 @@ public partial class Phone : IDisposable private sealed class PhoneInputModel { - [Required(ErrorMessage = "وارد کردن شماره موبایل الزامی است.")] - [RegularExpression(@"^09\\d{9}$", ErrorMessage = "شماره موبایل معتبر نیست.")] + [Required(ErrorMessage = "???? ???? ????? ?????? ?????? ???.")] + [RegularExpression(@"^09\\d{9}$", ErrorMessage = "????? ?????? ????? ????.")] public string PhoneNumber { get; set; } = string.Empty; - [Range(typeof(bool), "true", "true", ErrorMessage = "پذیرش شرایط و قوانین ضروری است.")] + [Range(typeof(bool), "true", "true", ErrorMessage = "????? ????? ? ?????? ????? ???.")] public bool AcceptTerms { get; set; } } } - diff --git a/src/FrontOffice.Main/Pages/Auth/Verify.razor b/src/FrontOffice.Main/Pages/Auth/Verify.razor index aee4973..c99b845 100644 --- a/src/FrontOffice.Main/Pages/Auth/Verify.razor +++ b/src/FrontOffice.Main/Pages/Auth/Verify.razor @@ -1,4 +1,4 @@ -@page "/auth/verify" +@attribute [Route(RouteConstants.Auth.Verify)] تأیید رمز پویا @@ -82,3 +82,4 @@ + diff --git a/src/FrontOffice.Main/Pages/Auth/Verify.razor.cs b/src/FrontOffice.Main/Pages/Auth/Verify.razor.cs index cae2994..6a03459 100644 --- a/src/FrontOffice.Main/Pages/Auth/Verify.razor.cs +++ b/src/FrontOffice.Main/Pages/Auth/Verify.razor.cs @@ -1,10 +1,7 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.ComponentModel.DataAnnotations; using Blazored.LocalStorage; using FrontOffice.BFF.User.Protobuf.Protos.User; +using FrontOffice.BFF.User.Protobuf.Validator; using FrontOffice.Main.Utilities; using Grpc.Core; using Microsoft.AspNetCore.Components; @@ -22,6 +19,8 @@ public partial class Verify : IDisposable private const string RedirectStorageKey = "auth:redirect"; private const string TokenStorageKey = "auth:token"; + private static readonly VerifyOtpTokenRequestValidator VerifyRequestValidator = new(); + private readonly OtpInputModel _model = new(); private MudForm? _form; private bool _isBusy; @@ -60,7 +59,8 @@ public partial class Verify : IDisposable if (string.IsNullOrWhiteSpace(_phoneNumber)) { - NavigateBackToPhone(); + await ResetAuthenticationAsync(); + Navigation.NavigateTo(RouteConstants.Auth.Phone, forceLoad: true); return; } @@ -96,13 +96,14 @@ public partial class Verify : IDisposable if (IsVerificationLocked) { - _errorMessage = "???????? ???? ?? ????? ????? ???. ????? ??? ???? ?????? ????."; + _errorMessage = "تعداد تلاش‌های مجاز به پایان رسیده است. لطفاً رمز جدید دریافت کنید."; return; } if (string.IsNullOrWhiteSpace(_phoneNumber)) { - NavigateBackToPhone(); + await ResetAuthenticationAsync(); + Navigation.NavigateTo(RouteConstants.Auth.Phone, forceLoad: true); return; } @@ -118,10 +119,27 @@ public partial class Verify : IDisposable Code = _model.Code }; - var response = await UserClient.VerifyOtpTokenAsync(request, cancellationToken: cancellationToken); + var validationResult = VerifyRequestValidator.Validate(request); + if (!validationResult.IsValid) + { + _errorMessage = string.Join(" ", validationResult.Errors.Select(e => e.ErrorMessage).Distinct()); + return; + } + + var metadata = await BuildAuthMetadataAsync(); + VerifyOtpTokenResponse response; + if (metadata is not null) + { + response = await UserClient.VerifyOtpTokenAsync(request, metadata, cancellationToken: cancellationToken); + } + else + { + response = await UserClient.VerifyOtpTokenAsync(request, cancellationToken: cancellationToken); + } + if (response is null) { - _errorMessage = "????? ??? ???? ????? ???. ????? ?????? ???? ????."; + _errorMessage = "تأیید رمز پویا انجام نشد. لطفاً دوباره تلاش کنید."; return; } @@ -131,6 +149,10 @@ public partial class Verify : IDisposable { await LocalStorage.SetItemAsync(TokenStorageKey, response.Token); } + else + { + await LocalStorage.RemoveItemAsync(TokenStorageKey); + } await LocalStorage.RemoveItemAsync(PhoneStorageKey); await LocalStorage.RemoveItemAsync(RedirectStorageKey); @@ -146,11 +168,11 @@ public partial class Verify : IDisposable return; } - RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "?? ?????? ???." : response.Message); + RegisterFailedAttempt(string.IsNullOrWhiteSpace(response.Message) ? "کد نادرست است." : response.Message); } catch (RpcException rpcEx) { - HandleVerificationFailure(rpcEx); + await HandleVerificationFailureAsync(rpcEx); } catch (OperationCanceledException) { @@ -167,25 +189,29 @@ public partial class Verify : IDisposable } } - private void HandleVerificationFailure(RpcException rpcEx) + private async Task HandleVerificationFailureAsync(RpcException rpcEx) { switch (rpcEx.Status.StatusCode) { case StatusCode.PermissionDenied: case StatusCode.InvalidArgument: - case StatusCode.Unauthenticated: RegisterFailedAttempt(!string.IsNullOrWhiteSpace(rpcEx.Status.Detail) ? rpcEx.Status.Detail - : "?? ?????? ???."); + : "کد نادرست است."); + break; + case StatusCode.Unauthenticated: + await ResetAuthenticationAsync(); + _errorMessage = "نشست کاربری منقضی شده است. لطفاً دوباره وارد شوید."; + Navigation.NavigateTo(RouteConstants.Auth.Phone, forceLoad: true); break; case StatusCode.DeadlineExceeded: case StatusCode.NotFound: - _errorMessage = "?? ????? ??? ???. ????? ??? ???? ?????? ????."; + _errorMessage = "کد منقضی شده است. لطفاً رمز جدید دریافت کنید."; break; default: _errorMessage = !string.IsNullOrWhiteSpace(rpcEx.Status.Detail) ? rpcEx.Status.Detail - : "????? ??? ???? ????? ???. ????? ?????? ???? ????."; + : "تأیید رمز پویا انجام نشد. لطفاً دوباره تلاش کنید."; break; } } @@ -196,11 +222,11 @@ public partial class Verify : IDisposable if (_attemptsLeft > 0) { - _errorMessage = $"{baseMessage} {_attemptsLeft} ???? ???? ????? ???."; + _errorMessage = $"{baseMessage} {_attemptsLeft} تلاش باقی مانده است."; } else { - _errorMessage = $"{baseMessage} ???????? ???? ??? ?? ????? ????? ???. ????? ??? ???? ?????? ????."; + _errorMessage = $"{baseMessage} تلاش‌های مجاز شما به پایان رسیده است. لطفاً رمز جدید دریافت کنید."; } } @@ -224,17 +250,27 @@ public partial class Verify : IDisposable Purpose = OtpPurpose }; - var response = await UserClient.CreateNewOtpTokenAsync(request, cancellationToken: cancellationToken); + var metadata = await BuildAuthMetadataAsync(); + CreateNewOtpTokenResponse response; + if (metadata is not null) + { + response = await UserClient.CreateNewOtpTokenAsync(request, metadata, cancellationToken: cancellationToken); + } + else + { + response = 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; @@ -243,9 +279,7 @@ public partial class Verify : IDisposable } catch (RpcException rpcEx) { - _errorMessage = !string.IsNullOrWhiteSpace(rpcEx.Status.Detail) - ? rpcEx.Status.Detail - : "????? ???? ??? ???? ?? ??? ????? ??."; + await HandleResendFailureAsync(rpcEx); } catch (OperationCanceledException) { @@ -262,6 +296,23 @@ public partial class Verify : IDisposable } } + private async Task HandleResendFailureAsync(RpcException rpcEx) + { + switch (rpcEx.Status.StatusCode) + { + case StatusCode.Unauthenticated: + await ResetAuthenticationAsync(); + _errorMessage = "نشست کاربری منقضی شده است. لطفاً دوباره وارد شوید."; + Navigation.NavigateTo(RouteConstants.Auth.Phone, forceLoad: true); + break; + default: + _errorMessage = !string.IsNullOrWhiteSpace(rpcEx.Status.Detail) + ? rpcEx.Status.Detail + : "ارسال مجدد رمز پویا با خطا مواجه شد."; + break; + } + } + private async Task ChangePhoneAsync() { await LocalStorage.RemoveItemAsync(PhoneStorageKey); @@ -270,7 +321,7 @@ public partial class Verify : IDisposable private void NavigateBackToPhone() { - var target = "/auth/phone"; + var target = RouteConstants.Auth.Phone; if (!string.IsNullOrWhiteSpace(_redirect)) { target += "?redirect=" + Uri.EscapeDataString(_redirect); @@ -297,6 +348,27 @@ public partial class Verify : IDisposable }, null, 1000, 1000); } + private async Task BuildAuthMetadataAsync() + { + var token = await LocalStorage.GetItemAsync(TokenStorageKey); + if (string.IsNullOrWhiteSpace(token)) + { + return null; + } + + return new Metadata + { + { "Authorization", $"Bearer {token}" } + }; + } + + private async Task ResetAuthenticationAsync() + { + await LocalStorage.RemoveItemAsync(TokenStorageKey); + await LocalStorage.RemoveItemAsync(PhoneStorageKey); + await LocalStorage.RemoveItemAsync(RedirectStorageKey); + } + private CancellationToken PrepareOperationToken() { _operationCts?.Cancel(); @@ -328,6 +400,3 @@ public partial class Verify : IDisposable public string Code { get; set; } = string.Empty; } } - - - diff --git a/src/FrontOffice.Main/Utilities/RouteConstants.cs b/src/FrontOffice.Main/Utilities/RouteConstants.cs index f82b596..8e01e68 100644 --- a/src/FrontOffice.Main/Utilities/RouteConstants.cs +++ b/src/FrontOffice.Main/Utilities/RouteConstants.cs @@ -1,4 +1,4 @@ -namespace FrontOffice.Main.Utilities; +namespace FrontOffice.Main.Utilities; public static class RouteConstants { @@ -6,4 +6,10 @@ public static class RouteConstants { public const string MainPage = "/"; } -} \ No newline at end of file + + public static class Auth + { + public const string Phone = "/auth/phone"; + public const string Verify = "/auth/verify"; + } +}