mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-01-31 17:32:29 -05:00
320 lines
10 KiB
Plaintext
320 lines
10 KiB
Plaintext
@using AliasVault.Client.Auth.Models
|
|
@using AliasVault.Client.Auth.Services
|
|
@using AliasVault.Client.Main.Components.Layout
|
|
@using AliasVault.Client.Utilities
|
|
@using Microsoft.Extensions.Localization
|
|
@using Microsoft.Extensions.DependencyInjection
|
|
@implements IDisposable
|
|
|
|
@if (IsOpen)
|
|
{
|
|
<ModalWrapper>
|
|
<!-- Backdrop -->
|
|
<div class="fixed inset-0 bg-black bg-opacity-80 transition-opacity" @onclick="HandleClose"></div>
|
|
|
|
<!-- Modal -->
|
|
<div class="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-6 pb-6 pt-5 text-left shadow-xl transition-all w-full max-w-md mx-4">
|
|
<!-- Close button -->
|
|
<button type="button" class="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none" @onclick="HandleClose">
|
|
<span class="sr-only">@SharedLocalizer["Close"]</span>
|
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Content -->
|
|
<div class="mt-3">
|
|
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">
|
|
@Title
|
|
</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
@Description
|
|
</p>
|
|
|
|
@if (!string.IsNullOrEmpty(_errorMessage))
|
|
{
|
|
<div class="mb-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-700 rounded text-red-700 dark:text-red-400 text-sm">
|
|
@_errorMessage
|
|
</div>
|
|
}
|
|
|
|
@if (!string.IsNullOrEmpty(_qrCodeUrl))
|
|
{
|
|
<div class="flex flex-col items-center mb-4">
|
|
<div id="@_qrElementId" data-url="@_qrCodeUrl" class="@(_isLoading ? "hidden" : "") mb-3 p-4 bg-white rounded-lg border-4 border-gray-200 dark:border-gray-600">
|
|
<!-- QR code will be rendered here -->
|
|
</div>
|
|
@if (!_isLoading)
|
|
{
|
|
<div class="text-gray-700 dark:text-gray-300 text-sm font-medium">
|
|
@FormatTime(_timeRemaining)
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@if (_isLoading && string.IsNullOrEmpty(_errorMessage))
|
|
{
|
|
<div class="flex justify-center py-8">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
|
</div>
|
|
}
|
|
|
|
<button type="button" @onclick="HandleClose" class="mt-4 w-full inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500">
|
|
@SharedLocalizer["Cancel"]
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</ModalWrapper>
|
|
}
|
|
|
|
@code {
|
|
private string? _qrCodeUrl;
|
|
private MobileLoginErrorCode? _errorCode;
|
|
private string? _errorMessage;
|
|
private int _timeRemaining = 120; // 2 minutes in seconds
|
|
private MobileLoginUtility? _mobileLoginUtility;
|
|
private bool _isLoading = true;
|
|
private System.Threading.Timer? _countdownTimer;
|
|
private string _qrElementId = $"mobile-unlock-qr-{Guid.NewGuid():N}";
|
|
|
|
[Inject]
|
|
private HttpClient Http { get; set; } = default!;
|
|
|
|
[Inject]
|
|
private JsInteropService JsInteropService { get; set; } = default!;
|
|
|
|
[Inject]
|
|
private ILogger<MobileUnlockModal> Logger { get; set; } = default!;
|
|
|
|
[Inject]
|
|
private IStringLocalizerFactory LocalizerFactory { get; set; } = default!;
|
|
|
|
[Inject]
|
|
private IServiceProvider ScopedServices { get; set; } = default!;
|
|
|
|
private IStringLocalizer SharedLocalizer => LocalizerFactory.Create("SharedResources", "AliasVault.Client");
|
|
|
|
private IStringLocalizer MobileLoginLocalizer => LocalizerFactory.Create("MobileLogin", "AliasVault.Client");
|
|
|
|
/// <summary>
|
|
/// Whether the modal is open.
|
|
/// </summary>
|
|
[Parameter]
|
|
public bool IsOpen { get; set; }
|
|
|
|
/// <summary>
|
|
/// Callback when the modal is closed.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback OnClose { get; set; }
|
|
|
|
/// <summary>
|
|
/// Callback when mobile login/unlock succeeds with the result.
|
|
/// </summary>
|
|
[Parameter]
|
|
public EventCallback<MobileLoginResult> OnSuccess { get; set; }
|
|
|
|
/// <summary>
|
|
/// Mode - 'login' or 'unlock'.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string Mode { get; set; } = "login";
|
|
|
|
/// <summary>
|
|
/// Modal title based on mode.
|
|
/// </summary>
|
|
private string Title => Mode == "unlock"
|
|
? LocalizerFactory.Create("Pages.Auth.MobileUnlockModal", "AliasVault.Client")["UnlockTitle"]
|
|
: LocalizerFactory.Create("Pages.Auth.MobileUnlockModal", "AliasVault.Client")["PageTitle"];
|
|
|
|
/// <summary>
|
|
/// Modal description based on mode.
|
|
/// </summary>
|
|
private string Description => LocalizerFactory.Create("Pages.Auth.MobileUnlockModal", "AliasVault.Client")["ScanQrCodeDescription"];
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
await base.OnParametersSetAsync();
|
|
|
|
if (IsOpen && _mobileLoginUtility == null)
|
|
{
|
|
await InitiateMobileLoginAsync();
|
|
}
|
|
else if (!IsOpen)
|
|
{
|
|
Cleanup();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize mobile login when modal opens.
|
|
/// </summary>
|
|
private async Task InitiateMobileLoginAsync()
|
|
{
|
|
try
|
|
{
|
|
_isLoading = true;
|
|
_errorCode = null;
|
|
_errorMessage = null;
|
|
_qrCodeUrl = null;
|
|
_timeRemaining = 120;
|
|
StateHasChanged();
|
|
|
|
// Initialize mobile login utility
|
|
var utilityLogger = ScopedServices.GetRequiredService<ILogger<MobileLoginUtility>>();
|
|
_mobileLoginUtility = new MobileLoginUtility(Http, JsInteropService, utilityLogger);
|
|
|
|
// Initiate mobile login and get QR code data
|
|
var requestId = await _mobileLoginUtility.InitiateAsync();
|
|
|
|
// Generate QR code with AliasVault prefix for mobile login
|
|
_qrCodeUrl = $"aliasvault://open/mobile-unlock/{requestId}";
|
|
|
|
// Render QR code while showing loading
|
|
StateHasChanged();
|
|
await Task.Delay(100); // Give DOM time to render
|
|
await JsInteropService.GenerateQrCode(_qrElementId);
|
|
|
|
// Wait for QR code to be fully rendered before hiding loading
|
|
await Task.Delay(300);
|
|
|
|
_isLoading = false;
|
|
StateHasChanged();
|
|
|
|
// Start countdown timer
|
|
StartCountdownTimer();
|
|
|
|
// Start polling for response
|
|
await _mobileLoginUtility.StartPollingAsync(
|
|
HandleSuccessAsync,
|
|
HandleErrorAsync);
|
|
}
|
|
catch (MobileLoginException ex)
|
|
{
|
|
Logger.LogError(ex, "Error initiating mobile login");
|
|
_isLoading = false;
|
|
_errorCode = ex.ErrorCode;
|
|
_errorMessage = GetErrorMessage(ex.ErrorCode);
|
|
StateHasChanged();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Error initiating mobile login");
|
|
_isLoading = false;
|
|
_errorCode = MobileLoginErrorCode.Generic;
|
|
_errorMessage = GetErrorMessage(MobileLoginErrorCode.Generic);
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle successful mobile login/unlock.
|
|
/// </summary>
|
|
private async Task HandleSuccessAsync(MobileLoginResult result)
|
|
{
|
|
try
|
|
{
|
|
// Call parent success callback
|
|
await OnSuccess.InvokeAsync(result);
|
|
|
|
// Close modal after successful processing
|
|
await HandleClose();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Error handling mobile login success");
|
|
_errorMessage = SharedLocalizer["ErrorUnknown"];
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle error.
|
|
/// </summary>
|
|
private void HandleErrorAsync(MobileLoginErrorCode errorCode)
|
|
{
|
|
_isLoading = false;
|
|
_qrCodeUrl = null; // Hide QR code when error occurs
|
|
_errorCode = errorCode;
|
|
_errorMessage = GetErrorMessage(errorCode);
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get translated error message for error code.
|
|
/// </summary>
|
|
private string GetErrorMessage(MobileLoginErrorCode errorCode)
|
|
{
|
|
return errorCode switch
|
|
{
|
|
MobileLoginErrorCode.Timeout => MobileLoginLocalizer["ErrorTimeout"],
|
|
MobileLoginErrorCode.Generic => SharedLocalizer["ErrorUnknown"],
|
|
_ => SharedLocalizer["ErrorUnknown"]
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle modal close.
|
|
/// </summary>
|
|
private async Task HandleClose()
|
|
{
|
|
Cleanup();
|
|
await OnClose.InvokeAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleanup resources.
|
|
/// </summary>
|
|
private void Cleanup()
|
|
{
|
|
_mobileLoginUtility?.Cleanup();
|
|
_mobileLoginUtility?.Dispose();
|
|
_mobileLoginUtility = null;
|
|
_countdownTimer?.Dispose();
|
|
_countdownTimer = null;
|
|
_qrCodeUrl = null;
|
|
_errorCode = null;
|
|
_errorMessage = null;
|
|
_timeRemaining = 120;
|
|
_isLoading = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Format time remaining as MM:SS.
|
|
/// </summary>
|
|
private string FormatTime(int seconds)
|
|
{
|
|
var mins = seconds / 60;
|
|
var secs = seconds % 60;
|
|
return $"{mins}:{secs:D2}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start countdown timer.
|
|
/// </summary>
|
|
private void StartCountdownTimer()
|
|
{
|
|
_countdownTimer = new System.Threading.Timer(_ =>
|
|
{
|
|
if (_timeRemaining > 0)
|
|
{
|
|
_timeRemaining--;
|
|
InvokeAsync(StateHasChanged);
|
|
}
|
|
else
|
|
{
|
|
_countdownTimer?.Dispose();
|
|
_mobileLoginUtility?.StopPolling();
|
|
}
|
|
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
Cleanup();
|
|
}
|
|
}
|