mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-23 22:28:22 -05:00
Add mobile unlock scaffolding to AliasVault.client web app (#1347)
This commit is contained in:
@@ -112,10 +112,32 @@ else
|
||||
<a href="/user/forgot-password" class="ml-auto text-sm text-primary-700 hover:underline dark:text-primary-500">@Localizer["LostPasswordLink"]</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">@Localizer["LoginToAccountButton"]</button>
|
||||
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800 flex items-center justify-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path>
|
||||
</svg>
|
||||
@Localizer["LoginToAccountButton"]
|
||||
</button>
|
||||
|
||||
<div class="relative hidden md:block">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 text-gray-500 bg-white dark:bg-gray-800 dark:text-gray-400">@Localizer["OrText"]</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/user/mobile-unlock" class="hidden md:flex w-full px-5 py-3 text-base font-medium text-center text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:hover:bg-gray-600 dark:focus:ring-gray-700 items-center justify-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
@Localizer["MobileDeviceLink"]
|
||||
</a>
|
||||
|
||||
@if (Config.PublicRegistrationEnabled)
|
||||
{
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400 text-center">
|
||||
@Localizer["NoAccountYetText"] <a href="/user/setup" class="text-primary-700 hover:underline dark:text-primary-500">@Localizer["CreateNewVaultLink"]</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
249
apps/server/AliasVault.Client/Auth/Pages/MobileUnlock.razor
Normal file
249
apps/server/AliasVault.Client/Auth/Pages/MobileUnlock.razor
Normal file
@@ -0,0 +1,249 @@
|
||||
@page "/user/mobile-unlock"
|
||||
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
|
||||
@layout Auth.Layout.MainLayout
|
||||
@attribute [AllowAnonymous]
|
||||
@using System.Text.Json
|
||||
@using AliasVault.Client.Auth.Components
|
||||
@using AliasVault.Client.Auth.Services
|
||||
@using AliasVault.Client.Utilities
|
||||
@using AliasVault.Cryptography.Client
|
||||
@using Microsoft.Extensions.Localization
|
||||
@implements IDisposable
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
@Localizer["PageTitle"]
|
||||
</h2>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div class="mb-4 p-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400">
|
||||
@_errorMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm">
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-6">
|
||||
@Localizer["ScanQrCodeDescription"]
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col items-center mb-6">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<SmallLoadingIndicator />
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_qrCodeUrl))
|
||||
{
|
||||
<div id="mobile-unlock-qr" data-url="@_qrCodeUrl" class="@(_isLoading ? "hidden" : "") mb-4 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">
|
||||
@FormatTime(_timeRemaining)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<button type="button" @onclick="HandleCancel" class="w-full text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700">
|
||||
@Localizer["CancelButton"]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FooterLogin />
|
||||
|
||||
@code {
|
||||
private string? _qrCodeUrl;
|
||||
private string? _errorMessage;
|
||||
private int _timeRemaining = 120; // 2 minutes in seconds
|
||||
private MobileUnlockUtility? _mobileUnlockUtility;
|
||||
private bool _isLoading = true;
|
||||
private System.Threading.Timer? _countdownTimer;
|
||||
|
||||
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Auth.MobileUnlock", "AliasVault.Client");
|
||||
private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Check if already authenticated
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (authState.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
await InitiateMobileUnlockAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize mobile unlock on component mount.
|
||||
/// </summary>
|
||||
private async Task InitiateMobileUnlockAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
StateHasChanged();
|
||||
|
||||
// Initialize mobile unlock utility
|
||||
_mobileUnlockUtility = new MobileUnlockUtility(Http, JsInteropService, ScopedServices.GetRequiredService<ILogger<MobileUnlockUtility>>());
|
||||
|
||||
// Initiate mobile unlock and get QR code data
|
||||
var requestId = await _mobileUnlockUtility.InitiateAsync();
|
||||
|
||||
// Generate QR code with AliasVault prefix for mobile unlock
|
||||
_qrCodeUrl = $"aliasvault://mobile-unlock/{requestId}";
|
||||
|
||||
// Render QR code first while still showing loading animation
|
||||
StateHasChanged();
|
||||
await Task.Delay(100); // Give DOM time to render
|
||||
await JsInteropService.GenerateQrCode("mobile-unlock-qr");
|
||||
|
||||
// Wait for QR code to be fully rendered before hiding loading animation
|
||||
await Task.Delay(500);
|
||||
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
|
||||
// Start countdown timer
|
||||
StartCountdownTimer();
|
||||
|
||||
// Start polling for response
|
||||
await _mobileUnlockUtility.StartPollingAsync(
|
||||
HandleSuccessfulAuthAsync,
|
||||
HandleErrorAsync);
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
// 404 means the mobile unlock endpoint doesn't exist - server version is too old
|
||||
// TODO: this check can be removed at a later time when v1.0 is ready and 0.25.0 release has been out for a while.
|
||||
_isLoading = false;
|
||||
_errorMessage = Localizer["ServerVersionTooOld"];
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_isLoading = false;
|
||||
_errorMessage = Localizer["InitializationError"];
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle successful authentication.
|
||||
/// </summary>
|
||||
private async Task HandleSuccessfulAuthAsync(string username, string token, string refreshToken, string decryptionKey, string salt, string encryptionType, string encryptionSettings)
|
||||
{
|
||||
try
|
||||
{
|
||||
_isLoading = true;
|
||||
_qrCodeUrl = null;
|
||||
StateHasChanged();
|
||||
|
||||
// Store the tokens in local storage
|
||||
await AuthService.StoreAccessTokenAsync(token);
|
||||
await AuthService.StoreRefreshTokenAsync(refreshToken);
|
||||
|
||||
// Convert decryption key from base64 string to byte array
|
||||
var decryptionKeyBytes = Convert.FromBase64String(decryptionKey);
|
||||
|
||||
// Store the encryption key in memory
|
||||
await AuthService.StoreEncryptionKeyAsync(decryptionKeyBytes);
|
||||
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
GlobalNotificationService.ClearMessages();
|
||||
|
||||
// Redirect to the page the user was trying to access before if set
|
||||
var localStorageReturnUrl = await LocalStorage.GetItemAsync<string>(ReturnUrlKey);
|
||||
if (!string.IsNullOrEmpty(localStorageReturnUrl))
|
||||
{
|
||||
await LocalStorage.RemoveItemAsync(ReturnUrlKey);
|
||||
NavigationManager.NavigateTo(localStorageReturnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_isLoading = false;
|
||||
_errorMessage = Localizer["LoginError"];
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle error.
|
||||
/// </summary>
|
||||
private void HandleErrorAsync(string errorMessage)
|
||||
{
|
||||
_isLoading = false;
|
||||
_errorMessage = errorMessage;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle cancel button.
|
||||
/// </summary>
|
||||
private void HandleCancel()
|
||||
{
|
||||
_mobileUnlockUtility?.Cleanup();
|
||||
_countdownTimer?.Dispose();
|
||||
NavigationManager.NavigateTo("/user/login");
|
||||
}
|
||||
|
||||
/// <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();
|
||||
_mobileUnlockUtility?.StopPolling();
|
||||
}
|
||||
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
_mobileUnlockUtility?.Dispose();
|
||||
_countdownTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="MobileUnlockUtility.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Client.Auth.Services;
|
||||
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AliasVault.Client.Services.JsInterop;
|
||||
using AliasVault.Shared.Models.WebApi.Auth;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for logging in with mobile app functionality.
|
||||
/// </summary>
|
||||
public sealed class MobileUnlockUtility : IDisposable
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly JsInteropService _jsInteropService;
|
||||
private readonly ILogger<MobileUnlockUtility> _logger;
|
||||
|
||||
private Timer? _pollingTimer;
|
||||
private string? _requestId;
|
||||
private string? _privateKey;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MobileUnlockUtility"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpClient">The HTTP client.</param>
|
||||
/// <param name="jsInteropService">The JS interop service.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public MobileUnlockUtility(HttpClient httpClient, JsInteropService jsInteropService, ILogger<MobileUnlockUtility> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_jsInteropService = jsInteropService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a mobile unlock request and returns the request ID for QR code generation.
|
||||
/// </summary>
|
||||
/// <returns>The request ID.</returns>
|
||||
/// <exception cref="HttpRequestException">Thrown when the request fails with status code.</exception>
|
||||
public async Task<string> InitiateAsync()
|
||||
{
|
||||
// Generate RSA key pair
|
||||
var keyPair = await _jsInteropService.GenerateRsaKeyPair();
|
||||
_privateKey = keyPair.PrivateKey;
|
||||
|
||||
// Send public key to server
|
||||
var request = new MobileUnlockInitiateRequest(keyPair.PublicKey);
|
||||
var response = await _httpClient.PostAsJsonAsync("v1/Auth/mobile-unlock/initiate", request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Failed to initiate mobile unlock: {response.StatusCode}", null, response.StatusCode);
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<MobileUnlockInitiateResponse>();
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to parse mobile unlock initiate response");
|
||||
}
|
||||
|
||||
_requestId = result.RequestId;
|
||||
return _requestId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts polling the server for mobile unlock response.
|
||||
/// </summary>
|
||||
/// <param name="onSuccess">Callback for successful authentication.</param>
|
||||
/// <param name="onError">Callback for errors.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task StartPollingAsync(Func<string, string, string, string, string, string, string, Task> onSuccess, Action<string> onError)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_requestId) || string.IsNullOrEmpty(_privateKey))
|
||||
{
|
||||
throw new InvalidOperationException("Must call InitiateAsync() before starting polling");
|
||||
}
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
// Start polling timer (every 3 seconds)
|
||||
_pollingTimer = new Timer(async _ => await PollServerAsync(onSuccess, onError), null, TimeSpan.Zero, TimeSpan.FromSeconds(3));
|
||||
|
||||
// Auto-stop after 2 minutes
|
||||
Task.Delay(TimeSpan.FromMinutes(2), _cancellationTokenSource.Token)
|
||||
.ContinueWith(
|
||||
_ =>
|
||||
{
|
||||
if (!_cancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
StopPolling();
|
||||
onError("Mobile unlock request timed out");
|
||||
}
|
||||
},
|
||||
TaskScheduler.Default);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops polling the server.
|
||||
/// </summary>
|
||||
public void StopPolling()
|
||||
{
|
||||
_pollingTimer?.Dispose();
|
||||
_pollingTimer = null;
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up resources.
|
||||
/// </summary>
|
||||
public void Cleanup()
|
||||
{
|
||||
StopPolling();
|
||||
_privateKey = null;
|
||||
_requestId = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
private async Task PollServerAsync(Func<string, string, string, string, string, string, string, Task> onSuccess, Action<string> onError)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_requestId) || _cancellationTokenSource?.IsCancellationRequested == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"v1/Auth/mobile-unlock/poll/{_requestId}");
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
StopPolling();
|
||||
_privateKey = null;
|
||||
_requestId = null;
|
||||
onError("Mobile unlock request expired or not found");
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Polling failed: {response.StatusCode}");
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<MobileUnlockPollResponse>();
|
||||
|
||||
if (result?.Fulfilled == true && !string.IsNullOrEmpty(result.EncryptedDecryptionKey) && !string.IsNullOrEmpty(result.Username) && result.Token != null && !string.IsNullOrEmpty(result.Salt) && !string.IsNullOrEmpty(result.EncryptionType) && !string.IsNullOrEmpty(result.EncryptionSettings))
|
||||
{
|
||||
// Stop polling
|
||||
StopPolling();
|
||||
|
||||
// Decrypt the decryption key using private key
|
||||
var decryptionKey = await _jsInteropService.DecryptWithPrivateKey(result.EncryptedDecryptionKey, _privateKey!);
|
||||
|
||||
// Clear sensitive data
|
||||
_privateKey = null;
|
||||
_requestId = null;
|
||||
|
||||
// Call success callback
|
||||
await onSuccess(result.Username, result.Token.Token, result.Token.RefreshToken, decryptionKey, result.Salt, result.EncryptionType, result.EncryptionSettings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during mobile unlock polling");
|
||||
StopPolling();
|
||||
_privateKey = null;
|
||||
_requestId = null;
|
||||
onError(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@
|
||||
<comment>Login button text</comment>
|
||||
</data>
|
||||
<data name="LoginToAccountButton" xml:space="preserve">
|
||||
<value>Login to your account</value>
|
||||
<value>Log in to your account</value>
|
||||
<comment>Extended login button text</comment>
|
||||
</data>
|
||||
|
||||
@@ -126,6 +126,14 @@
|
||||
<value>Log in with an authenticator code instead.</value>
|
||||
<comment>Link text for logging in with authenticator</comment>
|
||||
</data>
|
||||
<data name="MobileDeviceLink" xml:space="preserve">
|
||||
<value>Log in using mobile app</value>
|
||||
<comment>Link text for mobile device login</comment>
|
||||
</data>
|
||||
<data name="OrText" xml:space="preserve">
|
||||
<value>or</value>
|
||||
<comment>Divider text between login options</comment>
|
||||
</data>
|
||||
|
||||
<!-- Descriptions and help text -->
|
||||
<data name="TwoFactorAuthenticationDescription" xml:space="preserve">
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="PageTitle" xml:space="preserve">
|
||||
<value>Log in using Mobile App</value>
|
||||
<comment>Page title for mobile unlock feature</comment>
|
||||
</data>
|
||||
<data name="ScanQrCodeDescription" xml:space="preserve">
|
||||
<value>Scan this QR code with your AliasVault mobile app to login</value>
|
||||
<comment>Description instructing user to scan QR code</comment>
|
||||
</data>
|
||||
<data name="CancelButton" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
<comment>Button text to cancel mobile unlock</comment>
|
||||
</data>
|
||||
<data name="InitializationError" xml:space="preserve">
|
||||
<value>Failed to initialize mobile login. Please try again.</value>
|
||||
<comment>Error message when initialization fails</comment>
|
||||
</data>
|
||||
<data name="LoginError" xml:space="preserve">
|
||||
<value>Failed to complete login. Please try again.</value>
|
||||
<comment>Error message when login fails</comment>
|
||||
</data>
|
||||
<data name="ServerVersionTooOld" xml:space="preserve">
|
||||
<value>The AliasVault server needs to be updated to a newer version in order to use this feature. Please contact the server admin if you need help.</value>
|
||||
<comment>Error message when server version is too old for mobile unlock</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -235,7 +235,7 @@ public sealed class JsInteropService(IJSRuntime jsRuntime)
|
||||
try
|
||||
{
|
||||
// Invoke the JavaScript function and get the result as a byte array
|
||||
await jsRuntime.InvokeVoidAsync("generateQrCode", "authenticator-uri");
|
||||
await jsRuntime.InvokeVoidAsync("generateQrCode", elementId);
|
||||
}
|
||||
catch (JSException ex)
|
||||
{
|
||||
|
||||
@@ -1515,6 +1515,10 @@ video {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.border-4 {
|
||||
border-width: 4px;
|
||||
}
|
||||
|
||||
.border-b {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
@@ -2715,6 +2719,11 @@ video {
|
||||
--tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-gray-200:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-gray-300:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity));
|
||||
@@ -3190,6 +3199,11 @@ video {
|
||||
color: rgb(248 185 99 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:border-gray-600:hover:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-blue-500:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
|
||||
@@ -3514,6 +3528,10 @@ video {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.md\:flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.md\:hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user