//----------------------------------------------------------------------- // // Copyright (c) aliasvault. All rights reserved. // Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. // //----------------------------------------------------------------------- namespace AliasVault.Client.Main.Pages; using System.Collections.Generic; using AliasClientDb; using AliasVault.Client.Main.Utilities; using AliasVault.Client.Services; using AliasVault.Client.Services.Auth; using AliasVault.RazorComponents.Models; using Blazored.LocalStorage; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Localization; /// /// Base authorized page that all pages that are part of the logged in website should inherit from. /// All pages that inherit from this class will receive default injected components that are used globally. /// Also, a default set of breadcrumbs is added in the parent OnInitialized method. /// public abstract class MainBase : OwningComponentBase { private bool _parametersInitialSet; /// /// Gets or sets the NavigationManager. /// [Inject] public NavigationManager NavigationManager { get; set; } = null!; /// /// Gets or sets the AuthenticationStateProvider. /// [Inject] public AuthenticationStateProvider AuthStateProvider { get; set; } = null!; /// /// Gets or sets the GlobalNotificationService. /// [Inject] public GlobalNotificationService GlobalNotificationService { get; set; } = null!; /// /// Gets or sets the GlobalLoadingService in order to manipulate the global loading spinner animation. /// [Inject] public GlobalLoadingService GlobalLoadingSpinner { get; set; } = null!; /// /// Gets or sets the LocalizerFactory. /// [Inject] public IStringLocalizerFactory LocalizerFactory { get; set; } = null!; /// /// Gets or sets the JsInteropService. /// [Inject] public JsInteropService JsInteropService { get; set; } = null!; /// /// Gets or sets the DbService. /// [Inject] public DbService DbService { get; set; } = null!; /// /// Gets or sets the EmailService. /// [Inject] public EmailService EmailService { get; set; } = null!; /// /// Gets or sets the KeyboardShortcutService. /// [Inject] public KeyboardShortcutService KeyboardShortcutService { get; set; } = null!; /// /// Gets or sets the AuthService. /// [Inject] public AuthService AuthService { get; set; } = null!; /// /// Gets or sets the Config instance with values from appsettings.json. /// [Inject] public Config Config { get; set; } = null!; /// /// Gets or sets the LocalStorage. /// [Inject] public ILocalStorageService LocalStorage { get; set; } = null!; /// /// Gets or sets the FolderService. /// [Inject] public FolderService FolderService { get; set; } = null!; /// /// Gets the SharedLocalizer. This is used to access shared resource translations like buttons, etc. /// protected IStringLocalizer SharedLocalizer => LocalizerFactory.Create("SharedResources", "AliasVault.Client"); /// /// Gets or sets the breadcrumb items for the page. A default set of breadcrumbs is added in the parent OnInitialized method. /// protected List BreadcrumbItems { get; set; } = []; /// /// Initializes the component asynchronously. /// /// A representing the asynchronous operation. protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); _parametersInitialSet = false; // Add base breadcrumbs BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = SharedLocalizer["Home"], Url = NavigationManager.BaseUri, ShowHomeIcon = true }); bool willRedirect = await RedirectIfNoEncryptionKey(); if (willRedirect) { // Keep the page from loading if a redirect is imminent. while (true) { await Task.Delay(200); } } // Check if DB is initialized, if not, redirect to sync page. if (!DbService.GetState().CurrentState.IsInitialized()) { var currentRelativeUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); await LocalStorage.SetItemAsync(StorageKeys.ReturnUrl, currentRelativeUrl); NavigationManager.NavigateTo("/sync"); while (true) { await Task.Delay(200); } } } /// protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); bool willRedirect = await RedirectIfNoEncryptionKey(); if (willRedirect) { // Keep the page from loading if a redirect is imminent. while (true) { await Task.Delay(200); } } // Check if DB is initialized, if not, redirect to setup page. if (!DbService.GetState().CurrentState.IsInitialized()) { var currentRelativeUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); await LocalStorage.SetItemAsync(StorageKeys.ReturnUrl, currentRelativeUrl); NavigationManager.NavigateTo("/sync"); while (true) { await Task.Delay(200); } } } /// /// Gets the username from the authentication state asynchronously. /// /// The username. protected async Task GetUsernameAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); return authState.User.Identity?.Name ?? "[Unknown]"; } /// /// Sets the parameters asynchronously. /// /// A representing the asynchronous operation. protected override async Task OnParametersSetAsync() { await base.OnParametersSetAsync(); // This is to prevent the OnParametersSetAsync method from running together with OnInitialized on initial page load. if (!_parametersInitialSet) { _parametersInitialSet = true; } } /// /// Builds breadcrumb navigation for folder hierarchy using async folder loading. /// This helper method recursively builds folder breadcrumbs from the given folder up to the root. /// /// The folder ID to build breadcrumbs for. /// Whether the last folder (current page) should be clickable. Default is true for navigation to items within folders, false when on the folder page itself. /// A representing the asynchronous operation. protected async Task BuildFolderBreadcrumbsAsync(Guid folderId, bool makeLastClickable = true) { // Load all folders to build the path var foldersWithCounts = await FolderService.GetAllWithCountsAsync(); // Convert FolderWithCount to Folder for use with FolderTreeUtilities var allFolders = foldersWithCounts.Select(f => new Folder { Id = f.Id, Name = f.Name, ParentFolderId = f.ParentFolderId, Weight = f.Weight, }).ToList(); // Get the folder ID path from root to current folder var folderIdPath = FolderTreeUtilities.GetFolderIdPath(folderId, allFolders); if (folderIdPath.Count == 0) { return; } // Add breadcrumb for each folder in the path (from root to current) for (int i = 0; i < folderIdPath.Count; i++) { var currentFolderId = folderIdPath[i]; var folder = allFolders.FirstOrDefault(f => f.Id == currentFolderId); if (folder != null) { bool isLastFolder = i == folderIdPath.Count - 1; bool shouldBeClickable = !isLastFolder || makeLastClickable; BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = folder.Name, Url = shouldBeClickable ? $"/items/folder/{folder.Id}" : null, }); } } } /// /// Builds breadcrumb navigation for folder hierarchy and adds it to BreadcrumbItems. /// This helper method can be called from any page to add folder breadcrumbs. /// /// The folder ID to build breadcrumbs for. /// List of all folders (for path computation). /// Label for the "Items" breadcrumb (default: "Items" from localization). /// Whether to include the current folder as the last breadcrumb (default: true). protected void AddFolderBreadcrumbs(Guid? folderId, List allFolders, string? itemsLabel = null, bool includeCurrentFolder = true) { if (!folderId.HasValue || allFolders.Count == 0) { return; } // Get the folder path (list of folder names from root to current) var folderPath = FolderTreeUtilities.GetFolderPath(folderId, allFolders); var folderIdPath = FolderTreeUtilities.GetFolderIdPath(folderId, allFolders); if (folderPath.Count == 0) { return; } // Add breadcrumb for "Items" (vault home) var itemsLabelText = itemsLabel ?? SharedLocalizer["Items"]; BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = itemsLabelText, Url = "/items", }); // Determine how many folders to add as breadcrumbs int endIndex = includeCurrentFolder ? folderPath.Count : folderPath.Count - 1; // Add breadcrumb for each folder in the path for (int i = 0; i < endIndex; i++) { var currentFolderId = folderIdPath[i]; var folderName = folderPath[i]; // Last item should not have a URL if includeCurrentFolder is true bool isLastItem = includeCurrentFolder && i == folderPath.Count - 1; BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = folderName, Url = isLastItem ? null : $"/items/folder/{currentFolderId}", }); } } /// /// Checks if the encryption key is set. If not, redirect to the unlock screen /// where the user can re-enter the master password so the encryption key gets refreshed. /// /// This method should be called on every authenticated page load. /// private async Task RedirectIfNoEncryptionKey() { // If not logged in, let the normal login process handle it. var authState = await AuthStateProvider.GetAuthenticationStateAsync(); if (!authState.User.Identity?.IsAuthenticated ?? true) { return true; } // Check that encryption key is set. If not, redirect to unlock screen. if (!AuthService.IsEncryptionKeySet()) { // If returnUrl is not set and current URL is not unlock page, set it to the current URL. var localStorageReturnUrl = await LocalStorage.GetItemAsync(StorageKeys.ReturnUrl); if (string.IsNullOrEmpty(localStorageReturnUrl)) { var currentUrl = NavigationManager.Uri; if (!currentUrl.Contains("unlock", StringComparison.OrdinalIgnoreCase)) { var currentRelativeUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); await LocalStorage.SetItemAsync(StorageKeys.ReturnUrl, currentRelativeUrl); } } NavigationManager.NavigateTo("/unlock"); return true; } return false; } }