Files
aliasvault/apps/server/AliasVault.Client/wwwroot/js/utilities.js
2025-04-30 19:03:18 +02:00

338 lines
12 KiB
JavaScript

function downloadFileFromStream(fileName, contentStreamReference) {
const arrayBuffer = new Uint8Array(contentStreamReference).buffer;
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
URL.revokeObjectURL(url);
}
window.topMenuClickOutsideHandler = (dotNetHelper) => {
document.addEventListener('click', (event) => {
const userMenu = document.getElementById('userMenuDropdown');
const userMenuButton = document.getElementById('userMenuDropdownButton');
const mobileMenu = document.getElementById('mobileMenuDropdown');
const mobileMenuButton = document.getElementById('toggleMobileMenuButton');
// Handle user menu
if (userMenu && !userMenu.contains(event.target) && !userMenuButton.contains(event.target)) {
dotNetHelper.invokeMethodAsync('CloseUserMenu');
}
// Handle mobile menu
if (mobileMenu && !mobileMenu.contains(event.target)) {
if (!mobileMenuButton.contains(event.target)) {
dotNetHelper.invokeMethodAsync('CloseMobileMenu');
}
}
});
};
window.clipboardCopy = {
copyText: function (text) {
navigator.clipboard.writeText(text).then(function () { })
.catch(function (error) {
alert(error);
});
}
};
// Primarily used by E2E tests.
window.blazorNavigate = (url) => {
Blazor.navigateTo(url);
};
window.focusElement = (elementId) => {
const element = document.getElementById(elementId);
if (element) {
element.focus();
}
};
window.blurElement = (elementId) => {
const element = document.getElementById(elementId);
if (element) {
element.blur();
}
};
function initializeDarkMode() {
if (localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
function initDarkModeSwitcher() {
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
const themeToggleBtn = document.getElementById('theme-toggle');
if (!themeToggleBtn || !themeToggleDarkIcon || !themeToggleLightIcon) {
return;
}
if (localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleDarkIcon?.classList.remove('hidden');
} else {
themeToggleLightIcon?.classList.remove('hidden');
}
let event = new Event('dark-mode');
themeToggleBtn.addEventListener('click', function () {
// toggle icons
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
// toggle dark mode
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
document.dispatchEvent(event);
});
}
initializeDarkMode();
window.initTopMenu = function() {
initDarkModeSwitcher();
};
/**
* Generate a QR code for the given id element that has a data-url attribute.
* @param id
*/
function generateQrCode(id) {
// Find the element by id
const element = document.getElementById(id);
// Check if the element exists
if (!element) {
return;
}
// Get the data-url attribute
const dataUrl = element.getAttribute('data-url');
// Check if data-url exists
if (!dataUrl) {
return;
}
// Create a container for the QR code
const qrContainer = document.createElement('div');
qrContainer.id = `qrcode-${id}`;
element.appendChild(qrContainer);
// Initialize QRCode object
let qrcode = new QRCode(qrContainer, {
width: 256,
height: 256,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
qrcode.makeCode(dataUrl);
}
/**
* Gets or creates a WebAuthn credential and derives a key from it.
* @param {string} credentialIdToUse - The credentialId to use if one exists.
* @param {string} salt - The salt to use when deriving the key.
* @returns {Promise<string>} The derived key as a base64 string.
*/
async function getWebAuthnCredentialAndDeriveKey(credentialIdToUse, salt) {
const rpId = window.location.hostname;
const challenge = crypto.getRandomValues(new Uint8Array(32));
try {
const existingCredential = await navigator.credentials.get({
publicKey: {
challenge,
rpId,
userVerification: "discouraged",
allowCredentials: [{
id: Uint8Array.from(atob(credentialIdToUse), c => c.charCodeAt(0)),
type: 'public-key'
}],
extensions: {
prf: {
eval: {
first: Uint8Array.from(atob(salt), c => c.charCodeAt(0)),
},
},
},
}
});
const extensionsResult = existingCredential.getClientExtensionResults();
if (!extensionsResult?.prf) {
return { Error: "PRF_NOT_SUPPORTED" };
}
if (!extensionsResult.prf?.results?.first) {
return { Error: "PRF_DERIVATION_FAILED" };
}
const derivedKey = extensionsResult.prf.results.first;
return {
DerivedKey: btoa(String.fromCharCode.apply(null, new Uint8Array(derivedKey)))
};
} catch (error) {
console.error("Error getting WebAuthn credential:", error);
return { Error: "WEBAUTHN_GET_ERROR", Message: error.message };
}
}
/**
* Creates a WebAuthn credential and derives a key from it.
* @param {string} username - The username to associate with the credential.
* * @returns {Promise<{credentialId: string, salt: string, derivedKey: string} | null>} An object containing the credentialId, salt and derived key, or null if unsuccessful.
*/
async function createWebAuthnCredentialAndDeriveKey(username) {
const rpId = window.location.hostname;
const challenge = crypto.getRandomValues(new Uint8Array(32));
const salt = crypto.getRandomValues(new Uint8Array(32));
try {
// Create the credential
const newCredential = await navigator.credentials.create({
publicKey: {
challenge,
rp: {
name: "AliasVault",
id: rpId},
user: {
id: crypto.getRandomValues(new Uint8Array(32)),
name: username,
displayName: username
},
pubKeyCredParams: [
{ alg: -7, type: "public-key" }, // ES256
{ alg: -257, type: "public-key" }, // RS256
{ alg: -37, type: "public-key" }, // PS256
{ alg: -8, type: "public-key" }, // EdDSA
{ alg: -35, type: "public-key" }, // ES384
{ alg: -36, type: "public-key" }, // ES512
{ alg: -259, type: "public-key" }, // RS384
{ alg: -258, type: "public-key" }, // RS512
{ alg: -38, type: "public-key" }, // PS384
{ alg: -39, type: "public-key" }, // PS512
],
authenticatorSelection: {
userVerification: "discouraged",
residentKey: "discouraged",
requireResidentKey: false,
},
extensions: {
prf: {
eval: {
first: salt,
},
},
},
}
});
let extensionsResult = newCredential.getClientExtensionResults();
if (!extensionsResult.prf) {
return { Error: "PRF_NOT_SUPPORTED" };
}
if (!extensionsResult.prf?.results?.first) {
alert("Your authenticator has been successfully registered. Please use your authenticator again to complete the process.")
// Note: Some authenticators do not return the derived key in the create response. In this case,
// we need to read the credential to get the derived key. This is required for certain passkeys
// such as Yubikey.
const existingCredential = await navigator.credentials.get({
publicKey: {
challenge,
rpId,
userVerification: "discouraged",
allowCredentials: [{
id: newCredential.rawId,
type: 'public-key'
}],
extensions: {
prf: {
eval: {
first: salt,
},
},
},
}
});
extensionsResult = existingCredential.getClientExtensionResults();
}
if (!extensionsResult.prf?.results?.first) {
return { Error: "PRF_DERIVATION_FAILED" };
}
const derivedKey = extensionsResult.prf.results.first;
const credentialId = new Uint8Array(newCredential.rawId);
return {
CredentialId: btoa(String.fromCharCode.apply(null, credentialId)),
Salt: btoa(String.fromCharCode.apply(null, salt)),
DerivedKey: btoa(String.fromCharCode.apply(null, new Uint8Array(derivedKey))),
};
} catch (createError) {
console.error("Error creating new WebAuthn credential:", createError);
return { Error: "WEBAUTHN_CREATE_ERROR", Message: createError.message };
}
}
// Store the event listener references.
const visibilityChangeHandlers = new Map();
/**
* Registers visibility callback that is invoked when the visibility state of the current page/tab changes.
*
* @param {any} dotnetHelper
*/
window.registerVisibilityCallback = function (dotnetHelper) {
// Create a named function so we can reference it later for removal.
const handler = function() {
dotnetHelper.invokeMethodAsync('OnVisibilityChange', !document.hidden);
};
visibilityChangeHandlers.set(dotnetHelper, handler);
document.addEventListener("visibilitychange", handler);
// Initial call to set the correct initial state.
dotnetHelper.invokeMethodAsync('OnVisibilityChange', !document.hidden);
};
/**
* Unregisters any previously registered visibility callbacks to prevent memory leaks.
*
* @param {any} dotnetHelper
*/
window.unregisterVisibilityCallback = function (dotnetHelper) {
// Get the stored handler.
const handler = visibilityChangeHandlers.get(dotnetHelper);
if (handler) {
// Remove the event listener with the same function reference.
document.removeEventListener("visibilitychange", handler);
visibilityChangeHandlers.delete(dotnetHelper);
}
};