From 8f9ce3dc8a2cde6735bda35e7989819ffc7a7938 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 4 Apr 2023 08:07:48 -0400 Subject: [PATCH 001/125] Revert "[PS-1918] Make autofill doc-scanner traverse into ShadowRoot (#4119)" (#5147) This reverts commit 208be8dfbfe1ee9ee903c2c2ea576887e9cbe7d4. --- apps/browser/src/autofill/content/autofill.js | 348 +++++------------- 1 file changed, 99 insertions(+), 249 deletions(-) diff --git a/apps/browser/src/autofill/content/autofill.js b/apps/browser/src/autofill/content/autofill.js index e79470b265d..5d46a17292b 100644 --- a/apps/browser/src/autofill/content/autofill.js +++ b/apps/browser/src/autofill/content/autofill.js @@ -31,115 +31,21 @@ /* MODIFICATIONS FROM ORIGINAL - 1. Populate isFirefox - 2. Remove isChrome and isSafari since they are not used. - 3. Unminify and format to meet Mozilla review requirements. - 4. Remove unnecessary input types from getFormElements query selector and limit number of elements returned. - 5. Remove fakeTested prop. - 6. Rename com.agilebits.* stuff to com.bitwarden.* - 7. Remove "some useful globals" on window - 8. Add ability to autofill span[data-bwautofill] elements - 9. Add new handler, for new command that responds with page details in response callback + 1. Populate isFirefox + 2. Remove isChrome and isSafari since they are not used. + 3. Unminify and format to meet Mozilla review requirements. + 4. Remove unnecessary input types from getFormElements query selector and limit number of elements returned. + 5. Remove fakeTested prop. + 6. Rename com.agilebits.* stuff to com.bitwarden.* + 7. Remove "some useful globals" on window + 8. Add ability to autofill span[data-bwautofill] elements + 9. Add new handler, for new command that responds with page details in response callback 10. Handle sandbox iframe and sandbox rule in CSP 11. Work on array of saved urls instead of just one to determine if we should autofill non-https sites 12. Remove setting of attribute com.browser.browser.userEdited on user-inputs 13. Handle null value URLs in urlNotSecure - 14. Implement new HTML element query logic to be able to traverse into ShadowRoot */ - /* - * `openOrClosedShadowRoot` is only available to WebExtensions. - * We need to use the correct implementation based on browser. - */ - // START MODIFICATION - var getShadowRoot; - - if (chrome.dom && chrome.dom.openOrClosedShadowRoot) { - // Chromium 88+ - // https://developer.chrome.com/docs/extensions/reference/dom/ - getShadowRoot = function (element) { - if (!(element instanceof HTMLElement)) { - return null; - } - - return chrome.dom.openOrClosedShadowRoot(element); - }; - } else { - getShadowRoot = function (element) { - // `openOrClosedShadowRoot` is currently available for Firefox 63+ - // https://developer.mozilla.org/en-US/docs/Web/API/Element/openOrClosedShadowRoot - // Fallback to usual shadowRoot if it doesn't exist, which will only find open ShadowRoots, not closed ones. - // https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot#browser_compatibility - return element.openOrClosedShadowRoot || element.shadowRoot; - }; - } - - /* - * Returns elements like Document.querySelectorAll does, but traverses the document and shadow - * roots, yielding a visited node only if it passes the predicate in filterCallback. - */ - function queryDocAll(doc, rootEl, filterCallback) { - var accumulatedNodes = []; - - // mutates accumulatedNodes - accumulatingQueryDocAll(doc, rootEl, filterCallback, accumulatedNodes); - - return accumulatedNodes; - } - - function accumulatingQueryDocAll(doc, rootEl, filterCallback, accumulatedNodes) { - var treeWalker = doc.createTreeWalker(rootEl, NodeFilter.SHOW_ELEMENT); - var node; - - while (node = treeWalker.nextNode()) { - if (filterCallback(node)) { - accumulatedNodes.push(node); - } - - // If node contains a ShadowRoot we want to step into it and also traverse all child nodes inside. - var nodeShadowRoot = getShadowRoot(node); - - if (!nodeShadowRoot) { - continue; - } - - // recursively traverse into ShadowRoot - accumulatingQueryDocAll(doc, nodeShadowRoot, filterCallback, accumulatedNodes); - } - } - - /* - * Returns an element like Document.querySelector does, but traverses the document and shadow - * roots, yielding a visited node only if it passes the predicate in filterCallback. - */ - function queryDoc(doc, rootEl, filterCallback) { - var treeWalker = doc.createTreeWalker(rootEl, NodeFilter.SHOW_ELEMENT); - var node; - - while (node = treeWalker.nextNode()) { - if (filterCallback(node)) { - return node; - } - - // If node contains a ShadowRoot we want to step into it and also traverse all child nodes inside. - var nodeShadowRoot = getShadowRoot(node); - - if (!nodeShadowRoot) { - continue; - } - - // recursively traverse into ShadowRoot - var subQueryResult = queryDoc(doc, nodeShadowRoot, filterCallback); - - if (subQueryResult) { - return subQueryResult; - } - } - - return null; - } - // END MODIFICATION - function collect(document, undefined) { // START MODIFICATION var isFirefox = navigator.userAgent.indexOf('Firefox') !== -1 || navigator.userAgent.indexOf('Gecko/') !== -1; @@ -152,8 +58,8 @@ /** * For a given element `el`, returns the value of the attribute `attrName`. - * @param {HTMLElement} el - * @param {string} attrName + * @param {HTMLElement} el + * @param {string} attrName * @returns {string} The value of the attribute */ function getElementAttrValue(el, attrName) { @@ -190,7 +96,7 @@ /** * Returns the value of the given element. - * @param {HTMLElement} el + * @param {HTMLElement} el * @returns {any} Value of the element */ function getElementValue(el) { @@ -218,7 +124,7 @@ /** * If `el` is a `` element */ function getSelectElementOptions(el) { @@ -241,7 +147,7 @@ /** * If `el` is in a data table, get the label in the row directly above it - * @param {HTMLElement} el + * @param {HTMLElement} el * @returns {string} A string containing the label, or null if not found */ function getLabelTop(el) { @@ -281,7 +187,7 @@ /** * Get the contents of the elements that are labels for `el` - * @param {HTMLElement} el + * @param {HTMLElement} el * @returns {string} A string containing all of the `innerText` or `textContent` values for all elements that are labels for `el` */ function getLabelTag(el) { @@ -292,22 +198,12 @@ theLabels = Array.prototype.slice.call(el.labels); } else { if (el.id) { - // START MODIFICATION - var elId = JSON.stringify(el.id); - var labelsByReferencedId = queryDocAll(theDoc, theDoc.body, function (node) { - return node.nodeName === 'LABEL' && node.htmlFor === elId; - }); - theLabels = theLabels.concat(labelsByReferencedId); - // END MODIFICATION + theLabels = theLabels.concat(Array.prototype.slice.call( + queryDoc(theDoc, 'label[for=' + JSON.stringify(el.id) + ']'))); } if (el.name) { - // START MODIFICATION - var elName = JSON.stringify(el.name); - docLabel = queryDocAll(theDoc, theDoc.body, function (node) { - return node.nodeName === 'LABEL' && node.htmlFor === elName; - }); - // END MODIFICATION + docLabel = queryDoc(theDoc, 'label[for=' + JSON.stringify(el.name) + ']'); for (var labelIndex = 0; labelIndex < docLabel.length; labelIndex++) { if (-1 === theLabels.indexOf(docLabel[labelIndex])) { @@ -343,10 +239,10 @@ /** * Add property `prop` with value `val` to the object `obj` - * @param {object} obj - * @param {string} prop - * @param {any} val - * @param {*} d + * @param {object} obj + * @param {string} prop + * @param {any} val + * @param {*} d */ function addProp(obj, prop, val, d) { if (0 !== d && d === val || null === val || void 0 === val) { @@ -358,27 +254,34 @@ /** * Converts the string `s` to lowercase - * @param {string} s + * @param {string} s * @returns Lowercase string */ function toLowerString(s) { return 'string' === typeof s ? s.toLowerCase() : ('' + s).toLowerCase(); } - // START MODIFICATION - // renamed queryDoc to queryDocAll and moved to top - // END MODIFICATION + + /** + * Query the document `doc` for elements matching the selector `selector` + * @param {Document} doc + * @param {string} query + * @returns {HTMLElement[]} An array of elements matching the selector + */ + function queryDoc(doc, query) { + var els = []; + try { + els = doc.querySelectorAll(query); + } catch (e) { } + return els; + } + // end helpers var theView = theDoc.defaultView ? theDoc.defaultView : window, passwordRegEx = RegExp('((\\\\b|_|-)pin(\\\\b|_|-)|password|passwort|kennwort|(\\\\b|_|-)passe(\\\\b|_|-)|contraseña|senha|密码|adgangskode|hasło|wachtwoord)', 'i'); // get all the docs - // START MODIFICATION - var formNodes = queryDocAll(theDoc, theDoc.body, function (el) { - return el.nodeName === 'FORM'; - }); - var theForms = formNodes.map(function (formEl, elIndex) { - // END MODIFICATION + var theForms = Array.prototype.slice.call(queryDoc(theDoc, 'form')).map(function (formEl, elIndex) { var op = {}, formOpId = '__form__' + elIndex; @@ -536,11 +439,7 @@ }; // get proper page title. maybe they are using the special meta tag? - // START MODIFICATION - var theTitle = queryDoc(theDoc, theDoc, function (node) { - return node.hasAttribute('data-onepassword-title'); - }); - // END MODIFICATION + var theTitle = document.querySelector('[data-onepassword-title]') if (theTitle && theTitle.dataset[DISPLAY_TITLE_ATTRIBUE]) { pageDetails.displayTitle = theTitle.dataset.onepasswordTitle; } @@ -553,8 +452,8 @@ /** * Do the event on the element. * @param {HTMLElement} kedol The element to do the event on - * @param {string} fonor The event name - * @returns + * @param {string} fonor The event name + * @returns */ function doEventOnElement(kedol, fonor) { var quebo; @@ -566,7 +465,7 @@ /** * Clean up the string `s` to remove non-printable characters and whitespace. - * @param {string} s + * @param {string} s * @returns {string} Clean text */ function cleanText(s) { @@ -578,7 +477,7 @@ /** * If `el` is a text node, add the node's text to `arr`. * If `el` is an element node, add the element's `textContent or `innerText` to `arr`. - * @param {string[]} arr An array of `textContent` or `innerText` values + * @param {string[]} arr An array of `textContent` or `innerText` values * @param {HTMLElement} el The element to push to the array */ function checkNodeType(arr, el) { @@ -612,9 +511,9 @@ /** * Recursively gather all of the text values from the elements preceding `el` in the DOM - * @param {HTMLElement} el + * @param {HTMLElement} el * @param {string[]} arr An array of `textContent` or `innerText` values - * @param {number} steps The number of steps to take up the DOM tree + * @param {number} steps The number of steps to take up the DOM tree */ function shiftForLeftLabel(el, arr, steps) { var sib; @@ -645,7 +544,7 @@ /** * Determine if the element is visible. * Visible is define as not having `display: none` or `visibility: hidden`. - * @param {HTMLElement} el + * @param {HTMLElement} el * @returns {boolean} Returns `true` if the element is visible and `false` otherwise */ function isElementVisible(el) { @@ -656,10 +555,7 @@ // walk the dom tree until we reach the top for (var elStyle; theEl && theEl !== document;) { // Calculate the style of the element - // START MODIFICATION - elStyle = el.getComputedStyle && theEl instanceof Element ? el.getComputedStyle(theEl, null) : theEl.style; - // END MODIFICATION - + elStyle = el.getComputedStyle ? el.getComputedStyle(theEl, null) : theEl.style; // If there's no computed style at all, we're done, as we know that it's not hidden if (!elStyle) { return true; @@ -680,7 +576,7 @@ /** * Determine if the element is "viewable" on the screen. * "Viewable" is defined as being visible in the DOM and being within the confines of the viewport. - * @param {HTMLElement} el + * @param {HTMLElement} el * @returns {boolean} Returns `true` if the element is viewable and `false` otherwise */ function isElementViewable(el) { @@ -719,7 +615,7 @@ // If the right side of the bounding rectangle is outside the viewport, the x coordinate of the center point is the window width (minus offset) divided by 2. // If the right side of the bounding rectangle is inside the viewport, the x coordinate of the center point is the width of the bounding rectangle divided by 2. // If the bottom of the bounding rectangle is outside the viewport, the y coordinate of the center point is the window height (minus offset) divided by 2. - // If the bottom side of the bounding rectangle is inside the viewport, the y coordinate of the center point is the height of the bounding rectangle divided by + // If the bottom side of the bounding rectangle is inside the viewport, the y coordinate of the center point is the height of the bounding rectangle divided by // We then use elementFromPoint to find the element at that point. for (var pointEl = el.ownerDocument.elementFromPoint(leftOffset + (rect.right > window.innerWidth ? (window.innerWidth - leftOffset) / 2 : rect.width / 2), topOffset + (rect.bottom > window.innerHeight ? (window.innerHeight - topOffset) / 2 : rect.height / 2)); pointEl && pointEl !== el && pointEl !== document;) { // If the element we found is a label, and the element we're checking has labels @@ -741,7 +637,7 @@ /** * Retrieve the element from the document with the specified `opid` property - * @param {number} opId + * @param {number} opId * @returns {HTMLElement} The element with the specified `opiId`, or `null` if no such element exists */ function getElementForOPID(opId) { @@ -769,28 +665,6 @@ } } - var ignoredInputTypes = { - hidden: true, - submit: true, - reset: true, - button: true, - image: true, - file: true, - }; - - /* - * inputEl MUST BE an instanceof HTMLInputElement, else inputEl.type.toLowerCase will throw an error - */ - function isRelevantInputField(inputEl) { - if (inputEl.hasAttribute('data-bwignore')) { - return false; - } - - const isIgnoredInputType = ignoredInputTypes.hasOwnProperty(inputEl.type.toLowerCase()); - - return !isIgnoredInputType; - } - /** * Query `theDoc` for form elements that we can use for autofill, ranked by importance and limited by `limit` * @param {Document} theDoc The Document to query @@ -799,19 +673,13 @@ */ function getFormElements(theDoc, limit) { // START MODIFICATION - - var els = queryDocAll(theDoc, theDoc.body, function (el) { - switch (el.nodeName) { - case 'SELECT': - return true; - case 'SPAN': - return el.hasAttribute('data-bwautofill'); - case 'INPUT': - return isRelevantInputField(el); - default: - return false; - } - }); + var els = []; + try { + var elsList = theDoc.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="reset"])' + + ':not([type="button"]):not([type="image"]):not([type="file"]):not([data-bwignore]), select, ' + + 'span[data-bwautofill]'); + els = Array.prototype.slice.call(elsList); + } catch (e) { } if (!limit || els.length <= limit) { return els; @@ -841,12 +709,12 @@ } return returnEls; + // END MODIFICATION } - // END MODIFICATION /** * Focus the element `el` and optionally restore its original value - * @param {HTMLElement} el + * @param {HTMLElement} el * @param {boolean} setVal Set the value of the element to its original value */ function focusElement(el, setVal) { @@ -871,12 +739,6 @@ var markTheFilling = true, animateTheFilling = true; - function queryPasswordInputs() { - return queryDocAll(document, document.body, function (el) { - return el.nodeName === 'INPUT' && el.type.toLowerCase() === 'password'; - }) - } - // Check if URL is not secure when the original saved one was function urlNotSecure(savedURLs) { var passwordInputs = null; @@ -884,7 +746,7 @@ return false; } - return savedURLs.some(url => url?.indexOf('https://') === 0) && 'http:' === document.location.protocol && (passwordInputs = queryPasswordInputs(), + return savedURLs.some(url => url?.indexOf('https://') === 0) && 'http:' === document.location.protocol && (passwordInputs = document.querySelectorAll('input[type=password]'), 0 < passwordInputs.length && (confirmResult = confirm('Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page.\n\nDo you still wish to fill this login?'), 0 == confirmResult)) ? true : false; } @@ -1021,8 +883,8 @@ /** * Find all elements matching `query` and fill them using the value `op` from the fill script - * @param {string} query - * @param {string} op + * @param {string} query + * @param {string} op * @returns {HTMLElement} */ function doFillByQuery(query, op) { @@ -1035,8 +897,8 @@ /** * Assign `valueToSet` to all elements in the DOM that match `query`. - * @param {string} query - * @param {string} valueToSet + * @param {string} query + * @param {string} valueToSet * @returns {Array} Array of elements that were set. */ function doSimpleSetByQuery(query, valueToSet) { @@ -1050,8 +912,8 @@ /** * Do a a click and focus on the element with the given `opId`. - * @param {number} opId - * @returns + * @param {number} opId + * @returns */ function doFocusByOpId(opId) { var el = getElementByOpId(opId) @@ -1065,8 +927,8 @@ /** * Do a click on the element with the given `opId`. - * @param {number} opId - * @returns + * @param {number} opId + * @returns */ function doClickByOpId(opId) { var el = getElementByOpId(opId); @@ -1074,9 +936,9 @@ } /** - * Do a `click` and `focus` on all elements that match the query. - * @param {string} query - * @returns + * Do a `click` and `focus` on all elements that match the query. + * @param {string} query + * @returns */ function doClickByQuery(query) { query = selectAllFromDoc(query); @@ -1099,8 +961,8 @@ /** * Fll an element `el` using the value `op` from the fill script - * @param {HTMLElement} el - * @param {string} op + * @param {HTMLElement} el + * @param {string} op */ function fillTheElement(el, op) { var shouldCheck; @@ -1132,7 +994,7 @@ /** * Do all the fill operations needed on the element `el`. - * @param {HTMLElement} el + * @param {HTMLElement} el * @param {*} afterValSetFunc The function to perform after the operations are complete. */ function doAllFillOperations(el, afterValSetFunc) { @@ -1156,8 +1018,8 @@ /** * Normalize the event based on API support - * @param {HTMLElement} el - * @param {string} eventName + * @param {HTMLElement} el + * @param {string} eventName * @returns {Event} A normalized event */ function normalizeEvent(el, eventName) { @@ -1184,7 +1046,7 @@ /** * Simulate the entry of a value into an element. * Clicks the element, focuses it, and then fires a keydown, keypress, and keyup event. - * @param {HTMLElement} el + * @param {HTMLElement} el */ function setValueForElement(el) { var valueToSet = el.value; @@ -1199,7 +1061,7 @@ /** * Simulate the entry of a value into an element by using events. * Dispatches a keydown, keypress, and keyup event, then fires the `input` and `change` events before removing focus. - * @param {HTMLElement} el + * @param {HTMLElement} el */ function setValueForElementByEvent(el) { var valueToSet = el.value, @@ -1219,7 +1081,7 @@ /** * Click on an element `el` - * @param {HTMLElement} el + * @param {HTMLElement} el * @returns {boolean} Returns true if the element was clicked and false if it was not able to be clicked */ function clickElement(el) { @@ -1236,12 +1098,9 @@ */ function getAllFields() { var r = RegExp('((\\\\b|_|-)pin(\\\\b|_|-)|password|passwort|kennwort|passe|contraseña|senha|密码|adgangskode|hasło|wachtwoord)', 'i'); - return queryDocAll(document, document.body, function (el) { - return el.nodeName === 'INPUT' && - el.type.toLowerCase() === 'text' && - el.value && - r.test(el.value); - }); + return Array.prototype.slice.call(selectAllFromDoc("input[type='text']")).filter(function (el) { + return el.value && r.test(el.value); + }, this); } /** @@ -1257,7 +1116,7 @@ /** * Determine if we can apply styling to `el` to indicate that it was filled. - * @param {HTMLElement} el + * @param {HTMLElement} el * @returns {boolean} Returns true if we can see the element to apply styling. */ function canSeeElementToStyle(el) { @@ -1266,9 +1125,7 @@ a: { currentEl = el; for (var owner = el.ownerDocument, owner = owner ? owner.defaultView : {}, theStyle; currentEl && currentEl !== document;) { - // START MODIFICATION - theStyle = owner.getComputedStyle && currentEl instanceof Element ? owner.getComputedStyle(currentEl, null) : currentEl.style; - // END MODIFICATION + theStyle = owner.getComputedStyle ? owner.getComputedStyle(currentEl, null) : currentEl.style; if (!theStyle) { currentEl = true; break a; @@ -1292,7 +1149,7 @@ /** * Find the element for the given `opid`. - * @param {number} theOpId + * @param {number} theOpId * @returns {HTMLElement} The element for the given `opid`, or `null` if not found. */ function getElementByOpId(theOpId) { @@ -1302,19 +1159,12 @@ } try { // START MODIFICATION - var filteredElements = queryDocAll(document, document.body, function (el) { - switch (el.nodeName) { - case 'INPUT': - case 'SELECT': - case 'BUTTON': - return el.opid === theOpId; - case 'SPAN': - return el.hasAttribute('data-bwautofill') && el.opid === theOpId; - } - - return false; - }); + var elements = Array.prototype.slice.call(selectAllFromDoc('input, select, button, ' + + 'span[data-bwautofill]')); // END MODIFICATION + var filteredElements = elements.filter(function (o) { + return o.opid == theOpId; + }); if (0 < filteredElements.length) { theElement = filteredElements[0], 1 < filteredElements.length && console.warn('More than one element found with opid ' + theOpId); @@ -1331,20 +1181,20 @@ /** * Helper for doc.querySelectorAll - * @param {string} theSelector - * @returns + * @param {string} theSelector + * @returns */ function selectAllFromDoc(theSelector) { - // START MODIFICATION - return queryDocAll(document, document, function(node) { - return node.matches(theSelector); - }); - // END MODIFICATION + var d = document, elements = []; + try { + elements = d.querySelectorAll(theSelector); + } catch (e) { } + return elements; } /** * Focus an element and optionally re-set its value after focusing - * @param {HTMLElement} el + * @param {HTMLElement} el * @param {boolean} setValue Re-set the value after focusing */ function doFocusElement(el, setValue) { From 68d5558b9fb0661aac6f0ac4fffed7a3c42e371c Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Tue, 4 Apr 2023 15:50:15 -0400 Subject: [PATCH 002/125] added a check on the view component to check if the folder is null before rendering the html on the browser (#5112) --- .../src/vault/popup/components/vault/view.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/vault/popup/components/vault/view.component.html b/apps/browser/src/vault/popup/components/vault/view.component.html index b9f2a807a52..a698aaf718a 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.html +++ b/apps/browser/src/vault/popup/components/vault/view.component.html @@ -477,7 +477,7 @@ -
+
- + {{ "projectsNoItemsTitle" | i18n }} {{ "projectsNoItemsMessage" | i18n }} - + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html index 572f1a8049a..61b103a05b4 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html @@ -3,11 +3,11 @@
- + {{ "secretsNoItemsTitle" | i18n }} {{ "secretsTrashNoItemsMessage" | i18n }} - - + + {{ "secretsNoItemsTitle" | i18n }} {{ "secretsNoItemsMessage" | i18n }} - + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts index 47fa4f1aa45..33a2a97cb16 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { MultiSelectModule } from "@bitwarden/components"; +import { MultiSelectModule, NoItemsModule } from "@bitwarden/components"; import { CoreOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/core"; import { ProductSwitcherModule } from "@bitwarden/web-vault/app/layouts/product-switcher/product-switcher.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; @@ -10,19 +10,24 @@ import { AccessRemovalDialogComponent } from "./access-policies/dialogs/access-r import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component"; import { HeaderComponent } from "./header.component"; import { NewMenuComponent } from "./new-menu.component"; -import { NoItemsComponent } from "./no-items.component"; import { ProjectsListComponent } from "./projects-list.component"; import { SecretsListComponent } from "./secrets-list.component"; @NgModule({ - imports: [SharedModule, ProductSwitcherModule, MultiSelectModule, CoreOrganizationModule], + imports: [ + SharedModule, + ProductSwitcherModule, + MultiSelectModule, + CoreOrganizationModule, + NoItemsModule, + ], exports: [ SharedModule, + NoItemsModule, AccessRemovalDialogComponent, BulkStatusDialogComponent, HeaderComponent, NewMenuComponent, - NoItemsComponent, ProjectsListComponent, SecretsListComponent, AccessSelectorComponent, @@ -32,7 +37,6 @@ import { SecretsListComponent } from "./secrets-list.component"; BulkStatusDialogComponent, HeaderComponent, NewMenuComponent, - NoItemsComponent, ProjectsListComponent, SecretsListComponent, AccessSelectorComponent, diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index a5766287bf1..b1b4ea06070 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -16,6 +16,7 @@ export * from "./link"; export * from "./menu"; export * from "./multi-select"; export * from "./navigation"; +export * from "./no-items"; export * from "./progress"; export * from "./radio-button"; export * from "./table"; diff --git a/libs/components/src/no-items/index.ts b/libs/components/src/no-items/index.ts new file mode 100644 index 00000000000..a368ad18f9f --- /dev/null +++ b/libs/components/src/no-items/index.ts @@ -0,0 +1 @@ +export * from "./no-items.module"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/no-items.component.html b/libs/components/src/no-items/no-items.component.html similarity index 100% rename from bitwarden_license/bit-web/src/app/secrets-manager/shared/no-items.component.html rename to libs/components/src/no-items/no-items.component.html diff --git a/libs/components/src/no-items/no-items.component.ts b/libs/components/src/no-items/no-items.component.ts new file mode 100644 index 00000000000..a172a2eeabd --- /dev/null +++ b/libs/components/src/no-items/no-items.component.ts @@ -0,0 +1,14 @@ +import { Component } from "@angular/core"; + +import { Icons } from ".."; + +/** + * Component for displaying a message when there are no items to display. Expects title, description and button slots. + */ +@Component({ + selector: "bit-no-items", + templateUrl: "./no-items.component.html", +}) +export class NoItemsComponent { + protected icon = Icons.Search; +} diff --git a/libs/components/src/no-items/no-items.module.ts b/libs/components/src/no-items/no-items.module.ts new file mode 100644 index 00000000000..9fe6eb37aa9 --- /dev/null +++ b/libs/components/src/no-items/no-items.module.ts @@ -0,0 +1,13 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; + +import { IconModule } from "../icon"; + +import { NoItemsComponent } from "./no-items.component"; + +@NgModule({ + imports: [CommonModule, IconModule], + exports: [NoItemsComponent], + declarations: [NoItemsComponent], +}) +export class NoItemsModule {} diff --git a/libs/components/src/no-items/no-items.stories.ts b/libs/components/src/no-items/no-items.stories.ts new file mode 100644 index 00000000000..0b08aafbc11 --- /dev/null +++ b/libs/components/src/no-items/no-items.stories.ts @@ -0,0 +1,35 @@ +import { Meta, moduleMetadata, Story } from "@storybook/angular"; + +import { ButtonModule } from "../button"; + +import { NoItemsModule } from "./no-items.module"; + +export default { + title: "Component Library/No Items", + decorators: [ + moduleMetadata({ + imports: [ButtonModule, NoItemsModule], + }), + ], +} as Meta; + +const Template: Story = (args) => ({ + props: args, + template: ` + + No items found + Your description here. + + + `, +}); + +export const Default = Template.bind({}); From 7263579eafe3002660637a645d89de861d15bb2e Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 7 Apr 2023 11:11:20 -0400 Subject: [PATCH 018/125] [PM-329] Detangle SearchService & CipherService (#4838) * Remove Circular Dependency * Fix Vault Searching * Remove Unused cipherServiceOptions * Add searchService Parameter to CipherService * Fix instantiation of CipherService in test --- .../browser/cipher-context-menu-handler.ts | 7 ------- .../browser/context-menu-clicked-handler.ts | 7 ------- apps/browser/src/background/main.background.ts | 5 +++-- .../service_factories/search-service.factory.ts | 12 +----------- apps/browser/src/listeners/onCommandListener.ts | 4 ---- apps/browser/src/listeners/update-badge.ts | 8 +------- .../src/popup/services/popup-search.service.ts | 4 +--- .../src/popup/services/services.module.ts | 9 ++------- .../service_factories/cipher-service.factory.ts | 16 +++++++--------- .../components/vault/vault-items.component.ts | 4 ++-- apps/cli/src/bw.ts | 6 +++--- .../src/vault/app/vault/vault-items.component.ts | 9 +++++++-- .../individual-vault/vault-items.component.ts | 5 +++-- .../app/vault/org-vault/vault-items.component.ts | 3 ++- .../src/services/jslib-services.module.ts | 10 +++++----- .../vault/components/vault-items.component.ts | 4 +++- libs/common/src/abstractions/search.service.ts | 2 +- libs/common/src/services/search.service.ts | 15 +++++---------- .../src/vault/services/cipher.service.spec.ts | 2 +- libs/common/src/vault/services/cipher.service.ts | 10 +++++----- 20 files changed, 52 insertions(+), 90 deletions(-) diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts index b47683c8ce6..c8d9dfa64aa 100644 --- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts @@ -1,4 +1,3 @@ -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { StateFactory } from "@bitwarden/common/factories/stateFactory"; @@ -14,7 +13,6 @@ import { AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; import { CachedServices } from "../../background/service_factories/factory-options"; -import { searchServiceFactory } from "../../background/service_factories/search-service.factory"; import { BrowserApi } from "../../browser/browserApi"; import { Account } from "../../models/account"; import { @@ -45,14 +43,10 @@ export class CipherContextMenuHandler { static async create(cachedServices: CachedServices) { const stateFactory = new StateFactory(GlobalState, Account); - let searchService: SearchService | null = null; const serviceOptions: AuthServiceInitOptions & CipherServiceInitOptions = { apiServiceOptions: { logoutCallback: NOT_IMPLEMENTED, }, - cipherServiceOptions: { - searchServiceFactory: () => searchService, - }, cryptoFunctionServiceOptions: { win: self, }, @@ -80,7 +74,6 @@ export class CipherContextMenuHandler { stateFactory: stateFactory, }, }; - searchService = await searchServiceFactory(cachedServices, serviceOptions); return new CipherContextMenuHandler( await MainContextMenuHandler.mv3Create(cachedServices), await authServiceFactory(cachedServices, serviceOptions), diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index b70fed36f79..4b75942e031 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -1,5 +1,4 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { TotpService } from "@bitwarden/common/abstractions/totp.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -19,7 +18,6 @@ import LockedVaultPendingNotificationsItem from "../../background/models/lockedV import { eventCollectionServiceFactory } from "../../background/service_factories/event-collection-service.factory"; import { CachedServices } from "../../background/service_factories/factory-options"; import { passwordGenerationServiceFactory } from "../../background/service_factories/password-generation-service.factory"; -import { searchServiceFactory } from "../../background/service_factories/search-service.factory"; import { stateServiceFactory } from "../../background/service_factories/state-service.factory"; import { BrowserApi } from "../../browser/browserApi"; import { Account } from "../../models/account"; @@ -63,14 +61,10 @@ export class ContextMenuClickedHandler { static async mv3Create(cachedServices: CachedServices) { const stateFactory = new StateFactory(GlobalState, Account); - let searchService: SearchService | null = null; const serviceOptions: AuthServiceInitOptions & CipherServiceInitOptions = { apiServiceOptions: { logoutCallback: NOT_IMPLEMENTED, }, - cipherServiceOptions: { - searchServiceFactory: () => searchService, - }, cryptoFunctionServiceOptions: { win: self, }, @@ -98,7 +92,6 @@ export class ContextMenuClickedHandler { stateFactory: stateFactory, }, }; - searchService = await searchServiceFactory(cachedServices, serviceOptions); const generatePasswordToClipboardCommand = new GeneratePasswordToClipboardCommand( await passwordGenerationServiceFactory(cachedServices, serviceOptions), diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7b1e3f95058..09985455170 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -303,12 +303,14 @@ export default class MainBackground { this.apiService, this.fileUploadService ); + this.searchService = new SearchService(this.logService, this.i18nService); + this.cipherService = new CipherService( this.cryptoService, this.settingsService, this.apiService, this.i18nService, - () => this.searchService, + this.searchService, this.stateService, this.encryptService, this.cipherFileUploadService @@ -325,7 +327,6 @@ export default class MainBackground { this.i18nService, this.stateService ); - this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService); this.syncNotifierService = new SyncNotifierService(); this.organizationService = new BrowserOrganizationService(this.stateService); this.policyService = new BrowserPolicyService(this.stateService, this.organizationService); diff --git a/apps/browser/src/background/service_factories/search-service.factory.ts b/apps/browser/src/background/service_factories/search-service.factory.ts index 09d26237e14..eb6213c2b39 100644 --- a/apps/browser/src/background/service_factories/search-service.factory.ts +++ b/apps/browser/src/background/service_factories/search-service.factory.ts @@ -1,11 +1,6 @@ import { SearchService as AbstractSearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/services/search.service"; -import { - cipherServiceFactory, - CipherServiceInitOptions, -} from "../../vault/background/service_factories/cipher-service.factory"; - import { CachedServices, factory, FactoryOptions } from "./factory-options"; import { i18nServiceFactory, I18nServiceInitOptions } from "./i18n-service.factory"; import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory"; @@ -13,7 +8,6 @@ import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory" type SearchServiceFactoryOptions = FactoryOptions; export type SearchServiceInitOptions = SearchServiceFactoryOptions & - CipherServiceInitOptions & LogServiceInitOptions & I18nServiceInitOptions; @@ -26,10 +20,6 @@ export function searchServiceFactory( "searchService", opts, async () => - new SearchService( - await cipherServiceFactory(cache, opts), - await logServiceFactory(cache, opts), - await i18nServiceFactory(cache, opts) - ) + new SearchService(await logServiceFactory(cache, opts), await i18nServiceFactory(cache, opts)) ); } diff --git a/apps/browser/src/listeners/onCommandListener.ts b/apps/browser/src/listeners/onCommandListener.ts index ed6b5990701..7ea7de9a0f0 100644 --- a/apps/browser/src/listeners/onCommandListener.ts +++ b/apps/browser/src/listeners/onCommandListener.ts @@ -1,4 +1,3 @@ -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { StateFactory } from "@bitwarden/common/factories/stateFactory"; import { GlobalState } from "@bitwarden/common/models/domain/global-state"; @@ -60,9 +59,6 @@ const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise => { i18nServiceOptions: { systemLanguage: BrowserApi.getUILanguage(self), }, - cipherServiceOptions: { - searchServiceFactory: null as () => SearchService, // No dependence on search service - }, }; const logService = await logServiceFactory(cachedServices, opts); const authService = await authServiceFactory(cachedServices, opts); diff --git a/apps/browser/src/listeners/update-badge.ts b/apps/browser/src/listeners/update-badge.ts index de469117bc0..ba95579a96c 100644 --- a/apps/browser/src/listeners/update-badge.ts +++ b/apps/browser/src/listeners/update-badge.ts @@ -9,7 +9,6 @@ import { ContainerService } from "@bitwarden/common/services/container.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { authServiceFactory } from "../auth/background/service-factories/auth-service.factory"; -import { searchServiceFactory } from "../background/service_factories/search-service.factory"; import { stateServiceFactory } from "../background/service_factories/state-service.factory"; import { BrowserApi } from "../browser/browserApi"; import { Account } from "../models/account"; @@ -279,12 +278,7 @@ export class UpdateBadge { }; this.stateService = await stateServiceFactory(serviceCache, opts); this.authService = await authServiceFactory(serviceCache, opts); - const searchService = await searchServiceFactory(serviceCache, opts); - - this.cipherService = await cipherServiceFactory(serviceCache, { - ...opts, - cipherServiceOptions: { searchServiceFactory: () => searchService }, - }); + this.cipherService = await cipherServiceFactory(serviceCache, opts); // Needed for cipher decryption if (!self.bitwardenContainerService) { diff --git a/apps/browser/src/popup/services/popup-search.service.ts b/apps/browser/src/popup/services/popup-search.service.ts index b36ea5a060f..21b6e307959 100644 --- a/apps/browser/src/popup/services/popup-search.service.ts +++ b/apps/browser/src/popup/services/popup-search.service.ts @@ -1,16 +1,14 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service"; import { SearchService } from "@bitwarden/common/services/search.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; export class PopupSearchService extends SearchService { constructor( private mainSearchService: SearchService, - cipherService: CipherService, consoleLogService: ConsoleLogService, i18nService: I18nService ) { - super(cipherService, consoleLogService, i18nService); + super(consoleLogService, i18nService); } clearIndex() { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index dae7bb34ae5..044ebaa9436 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -164,19 +164,14 @@ function getBgService(service: keyof MainBackground) { }, { provide: SearchServiceAbstraction, - useFactory: ( - cipherService: CipherService, - logService: ConsoleLogService, - i18nService: I18nServiceAbstraction - ) => { + useFactory: (logService: ConsoleLogService, i18nService: I18nServiceAbstraction) => { return new PopupSearchService( getBgService("searchService")(), - cipherService, logService, i18nService ); }, - deps: [CipherService, LogServiceAbstraction, I18nServiceAbstraction], + deps: [LogServiceAbstraction, I18nServiceAbstraction], }, { provide: AuditService, useFactory: getBgService("auditService"), deps: [] }, { diff --git a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts index 42e2ab879cb..24cc6f2f89a 100644 --- a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts +++ b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts @@ -1,4 +1,3 @@ -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { CipherService as AbstractCipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; @@ -27,6 +26,10 @@ import { i18nServiceFactory, I18nServiceInitOptions, } from "../../../background/service_factories/i18n-service.factory"; +import { + searchServiceFactory, + SearchServiceInitOptions, +} from "../../../background/service_factories/search-service.factory"; import { SettingsServiceInitOptions, settingsServiceFactory, @@ -36,11 +39,7 @@ import { StateServiceInitOptions, } from "../../../background/service_factories/state-service.factory"; -type CipherServiceFactoryOptions = FactoryOptions & { - cipherServiceOptions?: { - searchServiceFactory?: () => SearchService; - }; -}; +type CipherServiceFactoryOptions = FactoryOptions; export type CipherServiceInitOptions = CipherServiceFactoryOptions & CryptoServiceInitOptions & @@ -48,6 +47,7 @@ export type CipherServiceInitOptions = CipherServiceFactoryOptions & ApiServiceInitOptions & CipherFileUploadServiceInitOptions & I18nServiceInitOptions & + SearchServiceInitOptions & StateServiceInitOptions & EncryptServiceInitOptions; @@ -65,9 +65,7 @@ export function cipherServiceFactory( await settingsServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), - opts.cipherServiceOptions?.searchServiceFactory === undefined - ? () => cache.searchService as SearchService - : opts.cipherServiceOptions.searchServiceFactory, + await searchServiceFactory(cache, opts), await stateServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), await cipherFileUploadServiceFactory(cache, opts) diff --git a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts index 2bc40d0d57a..c3c6da6332a 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts @@ -66,10 +66,10 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn private folderService: FolderService, private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService, - private cipherService: CipherService, + cipherService: CipherService, private vaultFilterService: VaultFilterService ) { - super(searchService); + super(searchService, cipherService); this.applySavedState = (window as any).previousPopupUrl != null && !(window as any).previousPopupUrl.startsWith("/ciphers"); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 7245c33f3d7..4b6c522b076 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -239,12 +239,14 @@ export class Main { this.sendService ); + this.searchService = new SearchService(this.logService, this.i18nService); + this.cipherService = new CipherService( this.cryptoService, this.settingsService, this.apiService, this.i18nService, - null, + this.searchService, this.stateService, this.encryptService, this.cipherFileUploadService @@ -267,8 +269,6 @@ export class Main { this.stateService ); - this.searchService = new SearchService(this.cipherService, this.logService, this.i18nService); - this.providerService = new ProviderService(this.stateService); this.organizationService = new OrganizationService(this.stateService); diff --git a/apps/desktop/src/vault/app/vault/vault-items.component.ts b/apps/desktop/src/vault/app/vault/vault-items.component.ts index c35f7730515..fc4c8327ce5 100644 --- a/apps/desktop/src/vault/app/vault/vault-items.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-items.component.ts @@ -2,6 +2,7 @@ import { Component } from "@angular/core"; import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { SearchBarService } from "../../../app/layout/search/search-bar.service"; @@ -12,8 +13,12 @@ import { SearchBarService } from "../../../app/layout/search/search-bar.service" }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class VaultItemsComponent extends BaseVaultItemsComponent { - constructor(searchService: SearchService, searchBarService: SearchBarService) { - super(searchService); + constructor( + searchService: SearchService, + searchBarService: SearchBarService, + cipherService: CipherService + ) { + super(searchService, cipherService); // eslint-disable-next-line rxjs-angular/prefer-takeuntil searchBarService.searchText$.subscribe((searchText) => { diff --git a/apps/web/src/app/vault/individual-vault/vault-items.component.ts b/apps/web/src/app/vault/individual-vault/vault-items.component.ts index de193529471..98f52bf5ca3 100644 --- a/apps/web/src/app/vault/individual-vault/vault-items.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-items.component.ts @@ -118,7 +118,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDe protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected vaultFilterService: VaultFilterService, - protected cipherService: CipherService, + cipherService: CipherService, protected eventCollectionService: EventCollectionService, protected totpService: TotpService, protected stateService: StateService, @@ -129,7 +129,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDe private organizationService: OrganizationService, private tokenService: TokenService ) { - super(searchService); + super(searchService, cipherService); } ngOnDestroy() { @@ -223,6 +223,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDe } async doSearch(indexedCiphers?: CipherView[]) { + indexedCiphers = indexedCiphers ?? (await this.cipherService.getAllDecrypted()); this.ciphers = await this.searchService.searchCiphers( this.searchText, [this.filter, this.deletedFilter], diff --git a/apps/web/src/app/vault/org-vault/vault-items.component.ts b/apps/web/src/app/vault/org-vault/vault-items.component.ts index f3b00729cb7..3481b1ab183 100644 --- a/apps/web/src/app/vault/org-vault/vault-items.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-items.component.ts @@ -110,7 +110,8 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDe (c) => c.organizationId === this.organization?.id ); } - await this.searchService.indexCiphers(this.organization?.id, this.allCiphers); + + this.searchService.indexCiphers(this.allCiphers, this.organization?.id); } async refreshCollections(): Promise { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index e2f2e50275e..0a909c8f8ff 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,4 +1,4 @@ -import { Injector, LOCALE_ID, NgModule } from "@angular/core"; +import { LOCALE_ID, NgModule } from "@angular/core"; import { AvatarUpdateService as AccountUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service"; import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/abstractions/anonymousHub.service"; @@ -251,7 +251,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; settingsService: SettingsServiceAbstraction, apiService: ApiServiceAbstraction, i18nService: I18nServiceAbstraction, - injector: Injector, + searchService: SearchServiceAbstraction, stateService: StateServiceAbstraction, encryptService: EncryptService, fileUploadService: CipherFileUploadServiceAbstraction @@ -261,7 +261,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; settingsService, apiService, i18nService, - () => injector.get(SearchServiceAbstraction), + searchService, stateService, encryptService, fileUploadService @@ -271,7 +271,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; SettingsServiceAbstraction, ApiServiceAbstraction, I18nServiceAbstraction, - Injector, // TODO: Get rid of this circular dependency! + SearchServiceAbstraction, StateServiceAbstraction, EncryptService, CipherFileUploadServiceAbstraction, @@ -475,7 +475,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; { provide: SearchServiceAbstraction, useClass: SearchService, - deps: [CipherServiceAbstraction, LogService, I18nServiceAbstraction], + deps: [LogService, I18nServiceAbstraction], }, { provide: NotificationsServiceAbstraction, diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 8848cd51343..1240c80ed69 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -2,6 +2,7 @@ import { Directive, EventEmitter, Input, Output } from "@angular/core"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @Directive() @@ -31,7 +32,7 @@ export class VaultItemsComponent { this._searchText = value; } - constructor(protected searchService: SearchService) {} + constructor(protected searchService: SearchService, protected cipherService: CipherService) {} async load(filter: (cipher: CipherView) => boolean = null, deleted = false) { this.deleted = deleted ?? false; @@ -92,6 +93,7 @@ export class VaultItemsComponent { protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; protected async doSearch(indexedCiphers?: CipherView[]) { + indexedCiphers = indexedCiphers ?? (await this.cipherService.getAllDecrypted()); this.ciphers = await this.searchService.searchCiphers( this.searchText, [this.filter, this.deletedFilter], diff --git a/libs/common/src/abstractions/search.service.ts b/libs/common/src/abstractions/search.service.ts index 3e449e92366..352d2ef08b8 100644 --- a/libs/common/src/abstractions/search.service.ts +++ b/libs/common/src/abstractions/search.service.ts @@ -5,7 +5,7 @@ export abstract class SearchService { indexedEntityId?: string = null; clearIndex: () => void; isSearchable: (query: string) => boolean; - indexCiphers: (indexedEntityGuid?: string, ciphersToIndex?: CipherView[]) => Promise; + indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => void; searchCiphers: ( query: string, filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[], diff --git a/libs/common/src/services/search.service.ts b/libs/common/src/services/search.service.ts index 880322c5e79..f7baabfbdd2 100644 --- a/libs/common/src/services/search.service.ts +++ b/libs/common/src/services/search.service.ts @@ -5,7 +5,6 @@ import { LogService } from "../abstractions/log.service"; import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service"; import { FieldType, UriMatchType } from "../enums"; import { SendView } from "../tools/send/models/view/send.view"; -import { CipherService } from "../vault/abstractions/cipher.service"; import { CipherType } from "../vault/enums/cipher-type"; import { CipherView } from "../vault/models/view/cipher.view"; @@ -19,11 +18,7 @@ export class SearchService implements SearchServiceAbstraction { private readonly defaultSearchableMinLength: number = 2; private searchableMinLength: number = this.defaultSearchableMinLength; - constructor( - private cipherService: CipherService, - private logService: LogService, - private i18nService: I18nService - ) { + constructor(private logService: LogService, private i18nService: I18nService) { this.i18nService.locale$.subscribe((locale) => { if (this.immediateSearchLocales.indexOf(locale) !== -1) { this.searchableMinLength = 1; @@ -55,7 +50,7 @@ export class SearchService implements SearchServiceAbstraction { return !notSearchable; } - async indexCiphers(indexedEntityId?: string, ciphers?: CipherView[]): Promise { + indexCiphers(ciphers: CipherView[], indexedEntityId?: string): void { if (this.indexing) { return; } @@ -94,7 +89,7 @@ export class SearchService implements SearchServiceAbstraction { extractor: (c: CipherView) => this.attachmentExtractor(c, true), }); builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId }); - ciphers = ciphers || (await this.cipherService.getAllDecrypted()); + ciphers = ciphers || []; ciphers.forEach((c) => builder.add(c)); this.index = builder.build(); @@ -106,7 +101,7 @@ export class SearchService implements SearchServiceAbstraction { async searchCiphers( query: string, filter: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[] = null, - ciphers: CipherView[] = null + ciphers: CipherView[] ): Promise { const results: CipherView[] = []; if (query != null) { @@ -117,7 +112,7 @@ export class SearchService implements SearchServiceAbstraction { } if (ciphers == null) { - ciphers = await this.cipherService.getAllDecrypted(); + ciphers = []; } if (filter != null && Array.isArray(filter) && filter.length > 0) { diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 3be54778d65..9fa70653cb6 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -49,7 +49,7 @@ describe("Cipher Service", () => { settingsService, apiService, i18nService, - () => searchService, + searchService, stateService, encryptService, cipherFileUploadService diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index c2a3a239e8a..9d475976718 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -53,7 +53,7 @@ export class CipherService implements CipherServiceAbstraction { private settingsService: SettingsService, private apiService: ApiService, private i18nService: I18nService, - private searchService: () => SearchService, + private searchService: SearchService, private stateService: StateService, private encryptService: EncryptService, private cipherFileUploadService: CipherFileUploadService @@ -68,9 +68,9 @@ export class CipherService implements CipherServiceAbstraction { await this.stateService.setDecryptedCiphers(value); if (this.searchService != null) { if (value == null) { - this.searchService().clearIndex(); + this.searchService.clearIndex(); } else { - this.searchService().indexCiphers(); + this.searchService.indexCiphers(value); } } } @@ -358,9 +358,9 @@ export class CipherService implements CipherServiceAbstraction { private async reindexCiphers() { const userId = await this.stateService.getUserId(); const reindexRequired = - this.searchService != null && (this.searchService().indexedEntityId ?? userId) !== userId; + this.searchService != null && (this.searchService.indexedEntityId ?? userId) !== userId; if (reindexRequired) { - await this.searchService().indexCiphers(userId, await this.getDecryptedCipherCache()); + this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId); } } From 3a4a79d0572cf5472a27ba5491446418f0ecc23f Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 10 Apr 2023 20:19:28 +0200 Subject: [PATCH 019/125] [PM-1691] Upgrade electron builder (#5038) * Upgrade electron builder * Fix after-sign * Allow singleArch * Add CSC_FOR_PULL_REQUEST --- .github/workflows/build-desktop.yml | 2 + apps/desktop/electron-builder.json | 1 + apps/desktop/scripts/after-sign.js | 2 +- package-lock.json | 1327 +++++++++++++-------------- package.json | 4 +- 5 files changed, 653 insertions(+), 683 deletions(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 586e0f6d6e3..73762e73f48 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -775,6 +775,7 @@ jobs: env: APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + CSC_FOR_PULL_REQUEST: true run: npm run pack:mac - name: Upload .zip artifact @@ -972,6 +973,7 @@ jobs: env: APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + CSC_FOR_PULL_REQUEST: true - name: Upload .pkg artifact uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index cca6dac2539..e890c357c2e 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -66,6 +66,7 @@ ], "CFBundleDevelopmentRegion": "en" }, + "singleArchFiles": "node_modules/@bitwarden/desktop-native/desktop_native.darwin-*.node", "target": ["dmg", "zip"] }, "win": { diff --git a/apps/desktop/scripts/after-sign.js b/apps/desktop/scripts/after-sign.js index fe82f735a16..4ef3023946a 100644 --- a/apps/desktop/scripts/after-sign.js +++ b/apps/desktop/scripts/after-sign.js @@ -2,8 +2,8 @@ require("dotenv").config(); const path = require("path"); +const { notarize } = require("@electron/notarize"); const { deepAssign } = require("builder-util"); -const { notarize } = require("electron-notarize"); const fse = require("fs-extra"); exports.default = run; diff --git a/package-lock.json b/package-lock.json index 9339edf4fbb..7f20d686a2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,8 @@ "@angular/compiler-cli": "^15.2.2", "@angular/elements": "^15.2.2", "@compodoc/compodoc": "^1.1.19", + "@electron/notarize": "^1.2.3", + "@electron/rebuild": "^3.2.10", "@fluffy-spoon/substitute": "^1.208.0", "@ngtools/webpack": "^15.2.2", "@storybook/addon-a11y": "^6.5.16", @@ -122,7 +124,7 @@ "css-loader": "^6.5.1", "del": "^6.0.0", "electron": "21.3.1", - "electron-builder": "22.11.10", + "electron-builder": "^23.6.0", "electron-log": "^4.4.8", "electron-notarize": "^1.2.2", "electron-rebuild": "^3.2.9", @@ -3810,17 +3812,272 @@ "node": ">= 4.0.0" } }, + "node_modules/@electron/notarize": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.3.tgz", + "integrity": "sha512-9oRzT56rKh5bspk3KpAVF8lPKHYQrBnRwcgiOeR0hdilVEQmszDaAu0IPCPrwwzJN0ugNs0rRboTreHMt/6mBQ==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.2.10.tgz", + "integrity": "sha512-SUBM6Mwi3yZaDFQjZzfGKpYTtOp9m60glounwX6tfGeVc/ZOl4jbquktUcyy7gYSLDWFLtKkftkY2xgMJZLQgg==", + "dev": true, + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "lzma-native": "^8.0.5", + "node-abi": "^3.0.0", + "node-api-version": "^0.1.4", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/src/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@electron/rebuild/node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@electron/rebuild/node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@electron/rebuild/node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/@electron/rebuild/node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/@electron/rebuild/node_modules/keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/@electron/rebuild/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@electron/rebuild/node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@electron/rebuild/node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@electron/universal": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.0.5.tgz", - "integrity": "sha512-zX9O6+jr2NMyAdSkwEUlyltiI4/EBLu2Ls/VD3pUQdi3cAYeYfdQnT2AJJ38HE4QxLccbU13LSpccw1IWlkyag==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", + "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", "dev": true, "dependencies": { "@malept/cross-spawn-promise": "^1.1.0", - "asar": "^3.0.3", + "asar": "^3.1.0", "debug": "^4.3.1", "dir-compare": "^2.4.0", - "fs-extra": "^9.0.1" + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" }, "engines": { "node": ">=8.6" @@ -16390,40 +16647,42 @@ } }, "node_modules/app-builder-bin": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-3.7.1.tgz", - "integrity": "sha512-ql93vEUq6WsstGXD+SBLSIQw6SNnhbDEM0swzgugytMxLp3rT24Ag/jcC80ZHxiPRTdew1niuR7P3/FCrDqIjw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", "dev": true }, "node_modules/app-builder-lib": { - "version": "22.11.10", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-22.11.10.tgz", - "integrity": "sha512-HhtQWAexnce/AVkBusMVaWD5ZtC5CvXu5KovZEo78kpBbFIXJJLyX8+eiLBNUEQuwQtY0vmqPRX3eAxNvfgosg==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.6.0.tgz", + "integrity": "sha512-dQYDuqm/rmy8GSCE6Xl/3ShJg6Ab4bZJMT8KaTKGzT436gl1DN4REP3FCWfXoh75qGTJ+u+WsdnnpO9Jl8nyMA==", "dev": true, "dependencies": { "@develar/schema-utils": "~2.6.5", - "@electron/universal": "1.0.5", + "@electron/universal": "1.2.1", "@malept/flatpak-bundler": "^0.4.0", "7zip-bin": "~5.1.1", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", - "builder-util": "22.11.10", - "builder-util-runtime": "8.7.9", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", "chromium-pickle-js": "^0.2.0", - "debug": "^4.3.2", - "ejs": "^3.1.6", - "electron-osx-sign": "^0.5.0", - "electron-publish": "22.11.10", - "fs-extra": "^10.0.0", - "hosted-git-info": "^4.0.2", + "debug": "^4.3.4", + "ejs": "^3.1.7", + "electron-osx-sign": "^0.6.0", + "electron-publish": "23.6.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", "is-ci": "^3.0.0", - "isbinaryfile": "^4.0.8", + "isbinaryfile": "^4.0.10", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "read-config-file": "6.2.0", "sanitize-filename": "^1.6.3", - "semver": "^7.3.5", + "semver": "^7.3.7", + "tar": "^6.1.11", "temp-file": "^3.4.0" }, "engines": { @@ -18524,20 +18783,23 @@ } }, "node_modules/builder-util": { - "version": "22.11.10", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-22.11.10.tgz", - "integrity": "sha512-IRN/eU8k5B0/OM4P9tTqYOkcY6xZLoezd+7STudXeTuRLpfkNNY6pR3BI0oTr0bYHJR61RvXRhWL0SdAszH9+Q==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.6.0.tgz", + "integrity": "sha512-QiQHweYsh8o+U/KNCZFSvISRnvRctb8m/2rB2I1JdByzvNKxPeFLlHFRPQRXab6aYeXc18j9LpsDLJ3sGQmWTQ==", "dev": true, "dependencies": { "@types/debug": "^4.1.6", "@types/fs-extra": "^9.0.11", "7zip-bin": "~5.1.1", - "app-builder-bin": "3.7.1", + "app-builder-bin": "4.0.0", "bluebird-lst": "^1.0.9", - "builder-util-runtime": "8.7.9", + "builder-util-runtime": "9.1.1", "chalk": "^4.1.1", - "debug": "^4.3.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", "fs-extra": "^10.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", "is-ci": "^3.0.0", "js-yaml": "^4.1.0", "source-map-support": "^0.5.19", @@ -18546,24 +18808,47 @@ } }, "node_modules/builder-util-runtime": { - "version": "8.7.9", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.9.tgz", - "integrity": "sha512-HCOkbKncb6eZSjnw1XnGgiXs7CQzORZit8zPK8IMHpU5GnZi6htS6KQdSMXU4ekDprfW3JLTRBHK9uxU2x++/g==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", + "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", "dev": true, "dependencies": { - "debug": "^4.3.2", + "debug": "^4.3.4", "sax": "^1.2.4" }, "engines": { "node": ">=12.0.0" } }, + "node_modules/builder-util/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/builder-util/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/builder-util/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/builder-util/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -19809,59 +20094,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "node_modules/configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "dev": true, - "dependencies": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/configstore/node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/configstore/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/configstore/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/connect": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", @@ -20821,15 +21053,6 @@ "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==", "dev": true }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/css-loader": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", @@ -21535,20 +21758,20 @@ "dev": true }, "node_modules/dmg-builder": { - "version": "22.11.10", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-22.11.10.tgz", - "integrity": "sha512-Dc29j9jZxjBZhRutfL/M/H+YJuQQQEywByXkB9WkvFoWEShG7Ncd6/HQcP2TvXv56Bj3OkRbC2GIclFhaCcabg==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.6.0.tgz", + "integrity": "sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA==", "dev": true, "dependencies": { - "app-builder-lib": "22.11.10", - "builder-util": "22.11.10", - "builder-util-runtime": "8.7.9", + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", "fs-extra": "^10.0.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { - "dmg-license": "^1.0.9" + "dmg-license": "^1.0.11" } }, "node_modules/dmg-builder/node_modules/argparse": { @@ -21948,9 +22171,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/ejs": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", - "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", "dev": true, "dependencies": { "jake": "^10.8.5" @@ -21981,23 +22204,23 @@ } }, "node_modules/electron-builder": { - "version": "22.11.10", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-22.11.10.tgz", - "integrity": "sha512-zzk1YVR7b9LaTGiPI1oK8KK2gd1ruiyR5abvYa5z5k3qtrMrTpLysGg2GbeWUbr+5aZlQpcuHnm13CLtjSyMZA==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.6.0.tgz", + "integrity": "sha512-y8D4zO+HXGCNxFBV/JlyhFnoQ0Y0K7/sFH+XwIbj47pqaW8S6PGYQbjoObolKBR1ddQFPt4rwp4CnwMJrW3HAw==", "dev": true, "dependencies": { "@types/yargs": "^17.0.1", - "app-builder-lib": "22.11.10", - "builder-util": "22.11.10", - "builder-util-runtime": "8.7.9", + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", "chalk": "^4.1.1", - "dmg-builder": "22.11.10", + "dmg-builder": "23.6.0", "fs-extra": "^10.0.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", "read-config-file": "6.2.0", - "update-notifier": "^5.1.0", - "yargs": "^17.0.1" + "simple-update-notifier": "^1.0.7", + "yargs": "^17.5.1" }, "bin": { "electron-builder": "cli.js", @@ -22043,9 +22266,9 @@ } }, "node_modules/electron-osx-sign": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.5.0.tgz", - "integrity": "sha512-icoRLHzFz/qxzDh/N4Pi2z4yVHurlsCAYQvsCSG7fCedJ4UJXBS6PoQyGH71IfcqKupcKeK7HX/NkyfG+v6vlQ==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz", + "integrity": "sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg==", "deprecated": "Please use @electron/osx-sign moving forward. Be aware the API is slightly different", "dev": true, "dependencies": { @@ -22092,14 +22315,14 @@ "dev": true }, "node_modules/electron-publish": { - "version": "22.11.10", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-22.11.10.tgz", - "integrity": "sha512-EE5aH5/YDWP91hHuRpTzhHqnN5lJ9+Juz89SCzmDAhrUF0ggV7u3640eFk2eFwxg57GRMAepB7Dx8/BGScgZHg==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.6.0.tgz", + "integrity": "sha512-jPj3y+eIZQJF/+t5SLvsI5eS4mazCbNYqatv5JihbqOstIM13k0d1Z3vAWntvtt13Itl61SO6seicWdioOU5dg==", "dev": true, "dependencies": { "@types/fs-extra": "^9.0.11", - "builder-util": "22.11.10", - "builder-util-runtime": "8.7.9", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", "chalk": "^4.1.1", "fs-extra": "^10.0.0", "lazy-val": "^1.0.5", @@ -22382,19 +22605,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/electron-updater/node_modules/builder-util-runtime": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", - "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/electron-updater/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -22853,15 +23063,6 @@ "node": ">=6" } }, - "node_modules/escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -25868,30 +26069,6 @@ "node": ">=10.0" } }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "dev": true, - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-dirs/node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", @@ -26754,15 +26931,6 @@ "node": ">=0.10.0" } }, - "node_modules/has-yarn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -27651,15 +27819,6 @@ "node": ">=4" } }, - "node_modules/import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -28297,22 +28456,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dev": true, - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -28358,18 +28501,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-npm": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", - "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -28726,12 +28857,6 @@ "node": ">=8" } }, - "node_modules/is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", - "dev": true - }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -31328,18 +31453,6 @@ "node": ">= 0.10" } }, - "node_modules/latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", - "dev": true, - "dependencies": { - "package-json": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lazy-universal-dotenv": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lazy-universal-dotenv/-/lazy-universal-dotenv-3.0.1.tgz", @@ -35336,30 +35449,6 @@ "node": ">=6" } }, - "node_modules/package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", - "dev": true, - "dependencies": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/package-json/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/packageurl-js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.0.1.tgz", @@ -37085,18 +37174,6 @@ "node": ">=6" } }, - "node_modules/pupa": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", - "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", - "dev": true, - "dependencies": { - "escape-goat": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/qrious": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz", @@ -37918,30 +37995,6 @@ "node": ">=4" } }, - "node_modules/registry-auth-token": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz", - "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==", - "dev": true, - "dependencies": { - "rc": "1.2.8" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "dev": true, - "dependencies": { - "rc": "^1.2.8" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/regjsparser": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", @@ -39261,27 +39314,6 @@ "dev": true, "optional": true }, - "node_modules/semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", - "dev": true, - "dependencies": { - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/semver-diff/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/semver-greatest-satisfied-range": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", @@ -39830,6 +39862,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -42797,18 +42850,6 @@ "through2-filter": "^3.0.0" } }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/unist-builder": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz", @@ -43042,52 +43083,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/update-notifier": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", - "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", - "dev": true, - "dependencies": { - "boxen": "^5.0.0", - "chalk": "^4.1.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.4.0", - "is-npm": "^5.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.1.0", - "pupa": "^2.1.1", - "semver": "^7.3.4", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/yeoman/update-notifier?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "node_modules/update-notifier/node_modules/is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "dependencies": { - "ci-info": "^2.0.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -44671,15 +44666,6 @@ "default-browser-id": "^1.0.4" } }, - "node_modules/xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -47393,17 +47379,197 @@ } } }, + "@electron/notarize": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.3.tgz", + "integrity": "sha512-9oRzT56rKh5bspk3KpAVF8lPKHYQrBnRwcgiOeR0hdilVEQmszDaAu0IPCPrwwzJN0ugNs0rRboTreHMt/6mBQ==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "dependencies": { + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, + "@electron/rebuild": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.2.10.tgz", + "integrity": "sha512-SUBM6Mwi3yZaDFQjZzfGKpYTtOp9m60glounwX6tfGeVc/ZOl4jbquktUcyy7gYSLDWFLtKkftkY2xgMJZLQgg==", + "dev": true, + "requires": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "lzma-native": "^8.0.5", + "node-abi": "^3.0.0", + "node-api-version": "^0.1.4", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "dependencies": { + "@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true + }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true + }, + "responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + } + } + }, "@electron/universal": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.0.5.tgz", - "integrity": "sha512-zX9O6+jr2NMyAdSkwEUlyltiI4/EBLu2Ls/VD3pUQdi3cAYeYfdQnT2AJJ38HE4QxLccbU13LSpccw1IWlkyag==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", + "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", "dev": true, "requires": { "@malept/cross-spawn-promise": "^1.1.0", - "asar": "^3.0.3", + "asar": "^3.1.0", "debug": "^4.3.1", "dir-compare": "^2.4.0", - "fs-extra": "^9.0.1" + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" }, "dependencies": { "fs-extra": { @@ -57101,40 +57267,42 @@ "dev": true }, "app-builder-bin": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-3.7.1.tgz", - "integrity": "sha512-ql93vEUq6WsstGXD+SBLSIQw6SNnhbDEM0swzgugytMxLp3rT24Ag/jcC80ZHxiPRTdew1niuR7P3/FCrDqIjw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", "dev": true }, "app-builder-lib": { - "version": "22.11.10", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-22.11.10.tgz", - "integrity": "sha512-HhtQWAexnce/AVkBusMVaWD5ZtC5CvXu5KovZEo78kpBbFIXJJLyX8+eiLBNUEQuwQtY0vmqPRX3eAxNvfgosg==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.6.0.tgz", + "integrity": "sha512-dQYDuqm/rmy8GSCE6Xl/3ShJg6Ab4bZJMT8KaTKGzT436gl1DN4REP3FCWfXoh75qGTJ+u+WsdnnpO9Jl8nyMA==", "dev": true, "requires": { "@develar/schema-utils": "~2.6.5", - "@electron/universal": "1.0.5", + "@electron/universal": "1.2.1", "@malept/flatpak-bundler": "^0.4.0", "7zip-bin": "~5.1.1", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", - "builder-util": "22.11.10", - "builder-util-runtime": "8.7.9", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", "chromium-pickle-js": "^0.2.0", - "debug": "^4.3.2", - "ejs": "^3.1.6", - "electron-osx-sign": "^0.5.0", - "electron-publish": "22.11.10", - "fs-extra": "^10.0.0", - "hosted-git-info": "^4.0.2", + "debug": "^4.3.4", + "ejs": "^3.1.7", + "electron-osx-sign": "^0.6.0", + "electron-publish": "23.6.0", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", "is-ci": "^3.0.0", - "isbinaryfile": "^4.0.8", + "isbinaryfile": "^4.0.10", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "read-config-file": "6.2.0", "sanitize-filename": "^1.6.3", - "semver": "^7.3.5", + "semver": "^7.3.7", + "tar": "^6.1.11", "temp-file": "^3.4.0" }, "dependencies": { @@ -58776,20 +58944,23 @@ } }, "builder-util": { - "version": "22.11.10", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-22.11.10.tgz", - "integrity": "sha512-IRN/eU8k5B0/OM4P9tTqYOkcY6xZLoezd+7STudXeTuRLpfkNNY6pR3BI0oTr0bYHJR61RvXRhWL0SdAszH9+Q==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.6.0.tgz", + "integrity": "sha512-QiQHweYsh8o+U/KNCZFSvISRnvRctb8m/2rB2I1JdByzvNKxPeFLlHFRPQRXab6aYeXc18j9LpsDLJ3sGQmWTQ==", "dev": true, "requires": { "@types/debug": "^4.1.6", "@types/fs-extra": "^9.0.11", "7zip-bin": "~5.1.1", - "app-builder-bin": "3.7.1", + "app-builder-bin": "4.0.0", "bluebird-lst": "^1.0.9", - "builder-util-runtime": "8.7.9", + "builder-util-runtime": "9.1.1", "chalk": "^4.1.1", - "debug": "^4.3.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", "fs-extra": "^10.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", "is-ci": "^3.0.0", "js-yaml": "^4.1.0", "source-map-support": "^0.5.19", @@ -58797,12 +58968,29 @@ "temp-file": "^3.4.0" }, "dependencies": { + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -58815,12 +59003,12 @@ } }, "builder-util-runtime": { - "version": "8.7.9", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.9.tgz", - "integrity": "sha512-HCOkbKncb6eZSjnw1XnGgiXs7CQzORZit8zPK8IMHpU5GnZi6htS6KQdSMXU4ekDprfW3JLTRBHK9uxU2x++/g==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", + "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", "dev": true, "requires": { - "debug": "^4.3.2", + "debug": "^4.3.4", "sax": "^1.2.4" } }, @@ -59763,46 +59951,6 @@ } } }, - "configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, "connect": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", @@ -60568,12 +60716,6 @@ "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==", "dev": true }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true - }, "css-loader": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", @@ -61106,15 +61248,15 @@ "dev": true }, "dmg-builder": { - "version": "22.11.10", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-22.11.10.tgz", - "integrity": "sha512-Dc29j9jZxjBZhRutfL/M/H+YJuQQQEywByXkB9WkvFoWEShG7Ncd6/HQcP2TvXv56Bj3OkRbC2GIclFhaCcabg==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.6.0.tgz", + "integrity": "sha512-jFZvY1JohyHarIAlTbfQOk+HnceGjjAdFjVn3n8xlDWKsYNqbO4muca6qXEZTfGXeQMG7TYim6CeS5XKSfSsGA==", "dev": true, "requires": { - "app-builder-lib": "22.11.10", - "builder-util": "22.11.10", - "builder-util-runtime": "8.7.9", - "dmg-license": "^1.0.9", + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", + "dmg-license": "^1.0.11", "fs-extra": "^10.0.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" @@ -61444,9 +61586,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "ejs": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", - "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", "dev": true, "requires": { "jake": "^10.8.5" @@ -61464,23 +61606,23 @@ } }, "electron-builder": { - "version": "22.11.10", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-22.11.10.tgz", - "integrity": "sha512-zzk1YVR7b9LaTGiPI1oK8KK2gd1ruiyR5abvYa5z5k3qtrMrTpLysGg2GbeWUbr+5aZlQpcuHnm13CLtjSyMZA==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.6.0.tgz", + "integrity": "sha512-y8D4zO+HXGCNxFBV/JlyhFnoQ0Y0K7/sFH+XwIbj47pqaW8S6PGYQbjoObolKBR1ddQFPt4rwp4CnwMJrW3HAw==", "dev": true, "requires": { "@types/yargs": "^17.0.1", - "app-builder-lib": "22.11.10", - "builder-util": "22.11.10", - "builder-util-runtime": "8.7.9", + "app-builder-lib": "23.6.0", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", "chalk": "^4.1.1", - "dmg-builder": "22.11.10", + "dmg-builder": "23.6.0", "fs-extra": "^10.0.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", "read-config-file": "6.2.0", - "update-notifier": "^5.1.0", - "yargs": "^17.0.1" + "simple-update-notifier": "^1.0.7", + "yargs": "^17.5.1" } }, "electron-log": { @@ -61514,9 +61656,9 @@ } }, "electron-osx-sign": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.5.0.tgz", - "integrity": "sha512-icoRLHzFz/qxzDh/N4Pi2z4yVHurlsCAYQvsCSG7fCedJ4UJXBS6PoQyGH71IfcqKupcKeK7HX/NkyfG+v6vlQ==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.6.0.tgz", + "integrity": "sha512-+hiIEb2Xxk6eDKJ2FFlpofCnemCbjbT5jz+BKGpVBrRNT3kWTGs4DfNX6IzGwgi33hUcXF+kFs9JW+r6Wc1LRg==", "dev": true, "requires": { "bluebird": "^3.5.0", @@ -61554,14 +61696,14 @@ } }, "electron-publish": { - "version": "22.11.10", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-22.11.10.tgz", - "integrity": "sha512-EE5aH5/YDWP91hHuRpTzhHqnN5lJ9+Juz89SCzmDAhrUF0ggV7u3640eFk2eFwxg57GRMAepB7Dx8/BGScgZHg==", + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.6.0.tgz", + "integrity": "sha512-jPj3y+eIZQJF/+t5SLvsI5eS4mazCbNYqatv5JihbqOstIM13k0d1Z3vAWntvtt13Itl61SO6seicWdioOU5dg==", "dev": true, "requires": { "@types/fs-extra": "^9.0.11", - "builder-util": "22.11.10", - "builder-util-runtime": "8.7.9", + "builder-util": "23.6.0", + "builder-util-runtime": "9.1.1", "chalk": "^4.1.1", "fs-extra": "^10.0.0", "lazy-val": "^1.0.5", @@ -61769,16 +61911,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "builder-util-runtime": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz", - "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==", - "dev": true, - "requires": { - "debug": "^4.3.4", - "sax": "^1.2.4" - } - }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -62165,12 +62297,6 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, - "escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", - "dev": true - }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -64570,23 +64696,6 @@ "serialize-error": "^7.0.1" } }, - "global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "dev": true, - "requires": { - "ini": "2.0.0" - }, - "dependencies": { - "ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "dev": true - } - } - }, "global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", @@ -65263,12 +65372,6 @@ } } }, - "has-yarn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", - "dev": true - }, "hash-base": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", @@ -65924,12 +66027,6 @@ } } }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", - "dev": true - }, "import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -66382,16 +66479,6 @@ "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", "dev": true }, - "is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dev": true, - "requires": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - } - }, "is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -66422,12 +66509,6 @@ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", "dev": true }, - "is-npm": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", - "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", - "dev": true - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -66679,12 +66760,6 @@ "is-docker": "^2.0.0" } }, - "is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", - "dev": true - }, "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -68751,15 +68826,6 @@ "es6-weak-map": "^2.0.1" } }, - "latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", - "dev": true, - "requires": { - "package-json": "^6.3.0" - } - }, "lazy-universal-dotenv": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lazy-universal-dotenv/-/lazy-universal-dotenv-3.0.1.tgz", @@ -71887,26 +71953,6 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, - "package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", - "dev": true, - "requires": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, "packageurl-js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.0.1.tgz", @@ -73162,15 +73208,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" }, - "pupa": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", - "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", - "dev": true, - "requires": { - "escape-goat": "^2.0.0" - } - }, "qrious": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz", @@ -73829,24 +73866,6 @@ "unicode-match-property-value-ecmascript": "^2.1.0" } }, - "registry-auth-token": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz", - "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==", - "dev": true, - "requires": { - "rc": "1.2.8" - } - }, - "registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "dev": true, - "requires": { - "rc": "^1.2.8" - } - }, "regjsparser": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", @@ -74893,23 +74912,6 @@ "dev": true, "optional": true }, - "semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", - "dev": true, - "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, "semver-greatest-satisfied-range": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", @@ -75340,6 +75342,23 @@ } } }, + "simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -77704,15 +77723,6 @@ "through2-filter": "^3.0.0" } }, - "unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "requires": { - "crypto-random-string": "^2.0.0" - } - }, "unist-builder": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz", @@ -77873,45 +77883,6 @@ "picocolors": "^1.0.0" } }, - "update-notifier": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", - "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", - "dev": true, - "requires": { - "boxen": "^5.0.0", - "chalk": "^4.1.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.4.0", - "is-npm": "^5.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.1.0", - "pupa": "^2.1.1", - "semver": "^7.3.4", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - } - } - }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -79128,12 +79099,6 @@ "default-browser-id": "^1.0.4" } }, - "xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true - }, "xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", diff --git a/package.json b/package.json index 114066f2589..b0331a2afe6 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "@angular/compiler-cli": "^15.2.2", "@angular/elements": "^15.2.2", "@compodoc/compodoc": "^1.1.19", + "@electron/notarize": "^1.2.3", + "@electron/rebuild": "^3.2.10", "@fluffy-spoon/substitute": "^1.208.0", "@ngtools/webpack": "^15.2.2", "@storybook/addon-a11y": "^6.5.16", @@ -86,7 +88,7 @@ "css-loader": "^6.5.1", "del": "^6.0.0", "electron": "21.3.1", - "electron-builder": "22.11.10", + "electron-builder": "^23.6.0", "electron-log": "^4.4.8", "electron-notarize": "^1.2.2", "electron-rebuild": "^3.2.9", From c3e87a55d62d0f70869fe528bd087bcf15d65be1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 14:04:00 -0700 Subject: [PATCH 020/125] Bumped desktop version to 2023.3.3 (#5195) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 3d1c77c1188..f6036ace0f4 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2023.3.2", + "version": "2023.3.3", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 6215c7ad114..48d626491df 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2023.3.2", + "version": "2023.3.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2023.3.2", + "version": "2023.3.3", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index bc2a50fc11e..4279a9a65f0 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2023.3.2", + "version": "2023.3.3", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 7f20d686a2b..61c7bceb1ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -228,7 +228,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2023.3.2", + "version": "2023.3.3", "hasInstallScript": true, "license": "GPL-3.0" }, From 8d34bc9ad9992ca6c5ed54abca1d713044fab9a9 Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Mon, 10 Apr 2023 22:36:18 -0400 Subject: [PATCH 021/125] SM-631: Allow Admins to Create and Edit Unassigned Secrets (#5052) * SM-631: Allow admins to edit and create unassigned secrets * SM-631: Fix OrganizationService import and refactor project selection logic in getSecretView --- .../secrets/dialog/secret-dialog.component.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts index f05a45b2d0f..bb44b84bea3 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts @@ -5,6 +5,7 @@ import { lastValueFrom, Subject } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { DialogService } from "@bitwarden/components"; import { ProjectListView } from "../../models/view/project-list.view"; @@ -40,11 +41,10 @@ export class SecretDialogComponent implements OnInit { project: new FormControl("", [Validators.required]), }); + private destroy$ = new Subject(); private loading = true; projects: ProjectListView[]; - private destroy$ = new Subject(); - constructor( public dialogRef: DialogRef, @Inject(DIALOG_DATA) private data: SecretOperation, @@ -52,7 +52,8 @@ export class SecretDialogComponent implements OnInit { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private projectService: ProjectService, - private dialogService: DialogService + private dialogService: DialogService, + private organizationService: OrganizationService ) {} async ngOnInit() { @@ -67,6 +68,11 @@ export class SecretDialogComponent implements OnInit { this.formGroup.get("project").setValue(this.data.projectId); } + if (this.organizationService.get(this.data.organizationId)?.isAdmin) { + this.formGroup.get("project").removeValidators(Validators.required); + this.formGroup.get("project").updateValueAndValidity(); + } + this.projects = await this.projectService .getProjects(this.data.organizationId) .then((projects) => projects.sort((a, b) => a.name.localeCompare(b.name))); @@ -157,7 +163,10 @@ export class SecretDialogComponent implements OnInit { secretView.name = this.formGroup.value.name; secretView.value = this.formGroup.value.value; secretView.note = this.formGroup.value.notes; - secretView.projects = [this.projects.find((p) => p.id == this.formGroup.value.project)]; + + const project = this.projects.find((p) => p.id == this.formGroup.value.project); + secretView.projects = project != undefined ? [project] : []; + return secretView; } From 1f026da924f038e3856276699e13263225388baa Mon Sep 17 00:00:00 2001 From: Andre Herms <49164940+onjonjo@users.noreply.github.com> Date: Tue, 11 Apr 2023 16:45:24 +0200 Subject: [PATCH 022/125] [PM-1674] let node-ipc use individual pipes on Windows (#4020) * let node-ipc use individual pipes on Windows This fixes an issue with multiple users on Windows. There should be an individual Windows pipe used for communication between browser plugin and desktop for each user. However, the naming scheme used the same name for every user. This change adds some uniqueness with a hash over username and unique directory name. * Fix ipc socket root for native-messaging too * use only homedir as unique --- .../desktop/src/main/native-messaging.main.ts | 9 +++--- apps/desktop/src/proxy/ipc.ts | 29 +++++++++++++++++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index ae549ace384..daa0d9de12f 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -6,6 +6,7 @@ import * as util from "util"; import { ipcMain } from "electron"; import * as ipc from "node-ipc"; +import { getIpcSocketRoot } from "../proxy/ipc"; import { LogService } from "@bitwarden/common/abstractions/log.service"; @@ -25,11 +26,9 @@ export class NativeMessagingMain { async listen() { ipc.config.id = "bitwarden"; ipc.config.retry = 1500; - if (process.platform === "darwin") { - if (!existsSync(`${homedir()}/tmp`)) { - await fs.mkdir(`${homedir()}/tmp`); - } - ipc.config.socketRoot = `${homedir()}/tmp/`; + const ipcSocketRoot = getIpcSocketRoot(); + if (ipcSocketRoot != null) { + ipc.config.socketRoot = ipcSocketRoot; } ipc.serve(() => { diff --git a/apps/desktop/src/proxy/ipc.ts b/apps/desktop/src/proxy/ipc.ts index 228179a5e4c..0160d6bf294 100644 --- a/apps/desktop/src/proxy/ipc.ts +++ b/apps/desktop/src/proxy/ipc.ts @@ -1,13 +1,38 @@ /* eslint-disable no-console */ +import { createHash } from "crypto"; +import { existsSync, mkdirSync } from "fs"; import { homedir } from "os"; +import { join as path_join } from "path"; import * as ipc from "node-ipc"; +export function getIpcSocketRoot(): string | null { + let socketRoot = null; + + switch (process.platform) { + case "darwin": { + const ipcSocketRootDir = path_join(homedir(), "tmp"); + if (!existsSync(ipcSocketRootDir)) { + mkdirSync(ipcSocketRootDir); + } + socketRoot = ipcSocketRootDir + "/"; + break; + } + case "win32": { + // Let node-ipc use a unique IPC pipe //./pipe/xxxxxxxxxxxxxxxxx.app.bitwarden per user. + // Hashing prevents problems with reserved characters and file length limitations. + socketRoot = createHash("sha1").update(homedir()).digest("hex") + "."; + } + } + return socketRoot; +} + ipc.config.id = "proxy"; ipc.config.retry = 1500; ipc.config.logger = console.warn; // Stdout is used for native messaging -if (process.platform === "darwin") { - ipc.config.socketRoot = `${homedir()}/tmp/`; +const ipcSocketRoot = getIpcSocketRoot(); +if (ipcSocketRoot != null) { + ipc.config.socketRoot = ipcSocketRoot; } export default class IPC { From 7ac893ad7cb1e73495ef749ad3656f30cd66e396 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 11 Apr 2023 17:24:42 +0200 Subject: [PATCH 023/125] Fix format (#5198) --- apps/desktop/src/main/native-messaging.main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index daa0d9de12f..360e219e22a 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -6,10 +6,11 @@ import * as util from "util"; import { ipcMain } from "electron"; import * as ipc from "node-ipc"; -import { getIpcSocketRoot } from "../proxy/ipc"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { getIpcSocketRoot } from "../proxy/ipc"; + import { WindowMain } from "./window.main"; export class NativeMessagingMain { From 2722198191fc39cab094a07e618b5249a036e746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Tue, 11 Apr 2023 18:37:58 +0200 Subject: [PATCH 024/125] [DEVOPS-1260] Update workflows to use new CI only keyvault (#5157) * Use new CI Azure Key Vault * Change name * Fix * Fix --- .github/workflows/brew-bump-cli.yml | 4 ++-- .github/workflows/brew-bump-desktop.yml | 4 ++-- .github/workflows/build-browser.yml | 8 ++++---- .github/workflows/build-cli.yml | 4 ++-- .github/workflows/build-desktop.yml | 12 ++++++------ .github/workflows/build-web.yml | 17 +++++++++++------ .github/workflows/crowdin-pull.yml | 6 +++--- .github/workflows/release-cli.yml | 12 ++++++------ .github/workflows/release-desktop-beta.yml | 8 ++++---- .github/workflows/release-desktop.yml | 12 ++++++------ .github/workflows/release-web.yml | 2 +- .github/workflows/staged-rollout-desktop.yml | 6 +++--- .github/workflows/version-auto-bump.yml | 2 +- .github/workflows/version-bump.yml | 4 ++-- 14 files changed, 53 insertions(+), 48 deletions(-) diff --git a/.github/workflows/brew-bump-cli.yml b/.github/workflows/brew-bump-cli.yml index 88a8cdefe50..f06517cec62 100644 --- a/.github/workflows/brew-bump-cli.yml +++ b/.github/workflows/brew-bump-cli.yml @@ -19,13 +19,13 @@ jobs: - name: Login to Azure uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "brew-bump-workflow-pat" - name: Update Homebrew formula diff --git a/.github/workflows/brew-bump-desktop.yml b/.github/workflows/brew-bump-desktop.yml index 249c5a57d48..32999bba4ab 100644 --- a/.github/workflows/brew-bump-desktop.yml +++ b/.github/workflows/brew-bump-desktop.yml @@ -19,13 +19,13 @@ jobs: - name: Login to Azure uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "brew-bump-workflow-pat" - name: Update Homebrew cask diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 18e7f752bd9..cdb045e629a 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -350,13 +350,13 @@ jobs: - name: Login to Azure uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@471ae4aec27405f16c5b796e288f54262c406e5d with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "crowdin-api-token" - name: Upload Sources @@ -411,14 +411,14 @@ jobs: uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 if: failure() with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets if: failure() uses: bitwarden/gh-actions/get-keyvault-secrets@471ae4aec27405f16c5b796e288f54262c406e5d with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index e319be23e23..a3e08a7f1a3 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -399,14 +399,14 @@ jobs: uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 # v1.1 if: failure() with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets if: failure() uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 73762e73f48..8ddda489df6 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -298,13 +298,13 @@ jobs: - name: Login to Azure uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@471ae4aec27405f16c5b796e288f54262c406e5d with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "code-signing-vault-url, code-signing-client-id, code-signing-tenant-id, @@ -1186,13 +1186,13 @@ jobs: - name: Login to Azure uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@471ae4aec27405f16c5b796e288f54262c406e5d with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "crowdin-api-token" - name: Upload Sources @@ -1264,14 +1264,14 @@ jobs: uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 if: failure() with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets if: failure() uses: bitwarden/gh-actions/get-keyvault-secrets@471ae4aec27405f16c5b796e288f54262c406e5d with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index be59be4123e..7becb7dff75 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -228,11 +228,16 @@ jobs: working-directory: apps/web run: unzip web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip + - name: Login to Azure + uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 # v1.1 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + - name: Retrieve github PAT secrets id: retrieve-secret-pat uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Setup DCT @@ -240,7 +245,7 @@ jobs: id: setup-dct uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff with: - azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + azure-creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} azure-keyvault-name: "bitwarden-prod-kv" - name: Build Docker image @@ -282,13 +287,13 @@ jobs: - name: Login to Azure uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 # v1.1 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "crowdin-api-token" - name: Upload Sources @@ -342,14 +347,14 @@ jobs: uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 # v1.1 if: failure() with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets if: failure() uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index acb660177f7..cda1f39262a 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -28,13 +28,13 @@ jobs: - name: Login to Azure uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af + uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase" - name: Download translations diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index c01f73677d2..603459876bd 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -146,13 +146,13 @@ jobs: - name: Login to Azure uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 # v1.1 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "snapcraft-store-token" - name: Install Snap @@ -200,13 +200,13 @@ jobs: - name: Login to Azure uses: Azure/login@24848bc889cfc0a8313c2b3e378ac0d625b9bc16 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "cli-choco-api-key" - name: Setup Chocolatey @@ -259,13 +259,13 @@ jobs: - name: Login to Azure uses: Azure/login@24848bc889cfc0a8313c2b3e378ac0d625b9bc16 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "npm-api-key" - name: Download artifacts diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index fb456de1951..60ddf2f60cd 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -245,13 +245,13 @@ jobs: - name: Login to Azure uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "code-signing-vault-url, code-signing-client-id, code-signing-tenant-id, @@ -928,13 +928,13 @@ jobs: - name: Login to Azure uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, aws-electron-bucket-name, diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index f0fe2011734..b4fa338897d 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -106,13 +106,13 @@ jobs: - name: Login to Azure uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, aws-electron-bucket-name, @@ -259,13 +259,13 @@ jobs: - name: Login to Azure uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "snapcraft-store-token" - name: Install Snap @@ -323,13 +323,13 @@ jobs: - name: Login to Azure uses: Azure/login@24848bc889cfc0a8313c2b3e378ac0d625b9bc16 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "cli-choco-api-key" - name: Setup Chocolatey diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index cf9379a8201..598541f1262 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -72,7 +72,7 @@ jobs: id: setup-dct uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff with: - azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + azure-creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} azure-keyvault-name: "bitwarden-prod-kv" - name: Pull branch image diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index d2c842301a3..04c0b8472fd 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -22,13 +22,13 @@ jobs: - name: Login to Azure uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, aws-electron-bucket-name, @@ -106,7 +106,7 @@ jobs: run: | aws s3 cp latest.yml $AWS_S3_BUCKET_NAME/desktop/ \ --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - + aws s3 cp latest-linux.yml $AWS_S3_BUCKET_NAME/desktop/ \ --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml index 88cb606a194..4274022f8f2 100644 --- a/.github/workflows/version-auto-bump.yml +++ b/.github/workflows/version-auto-bump.yml @@ -47,7 +47,7 @@ jobs: - name: Bump version to ${{ needs.setup.outputs.version_number }} uses: ./.github/workflows/version-bump.yml secrets: - AZURE_PROD_KV_CREDENTIALS: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + AZURE_PROD_KV_CREDENTIALS: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} with: version_number: ${{ needs.setup.outputs.version_number }} client: "Desktop" diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 6d3bd96ea11..6afd2278552 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -45,13 +45,13 @@ jobs: - name: Login to Azure - Prod Subscription uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af with: - keyvault: "bitwarden-prod-kv" + keyvault: "bitwarden-ci" secrets: "github-gpg-private-key, github-gpg-private-key-passphrase" - name: Import GPG key From 3300a4cc15395c091fa70ac14a5ec34547950149 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Tue, 11 Apr 2023 14:59:20 -0400 Subject: [PATCH 025/125] [SM-612] Highlighting selected product properly (#5065) * Highlighting selected product properly * suggested changes by WIll --- .../product-switcher-content.component.html | 6 +++++- .../product-switcher-content.component.ts | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html index a9b5d311bdf..293c024ac49 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html @@ -12,8 +12,12 @@ diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts index 21f186aa6a1..95b4c04d5e6 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts @@ -1,5 +1,5 @@ import { Component, ViewChild } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { combineLatest, map } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -25,6 +25,11 @@ type ProductSwitcherItem = { * Route for items in the `otherProducts$` section */ marketingRoute?: string | any[]; + + /** + * Used to apply css styles to show when a button is selected + */ + isActive?: boolean; }; @Component({ @@ -56,12 +61,14 @@ export class ProductSwitcherContentComponent { icon: "bwi-lock", appRoute: "/vault", marketingRoute: "https://bitwarden.com/products/personal/", + isActive: !this.router.url.includes("/sm/"), }, sm: { name: "Secrets Manager Beta", icon: "bwi-cli", appRoute: ["/sm", smOrg?.id], marketingRoute: "https://bitwarden.com/products/secrets-manager/", + isActive: this.router.url.includes("/sm/"), }, orgs: { name: "Organizations", @@ -90,5 +97,9 @@ export class ProductSwitcherContentComponent { }) ); - constructor(private organizationService: OrganizationService, private route: ActivatedRoute) {} + constructor( + private organizationService: OrganizationService, + private route: ActivatedRoute, + private router: Router + ) {} } From e08dafcf5f3b032aedc929f7bab784f9105e8f6e Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Thu, 13 Apr 2023 14:30:45 +0100 Subject: [PATCH 026/125] [PM-1608] Changed safari extension save dialog (#5127) * PS-1608 - Changed safari extension save dialog * PM-1608 moved setActivationPolicy to constructor --- .../safari/SafariWebExtensionHandler.swift | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift index 5a12da3a02a..80ea214b4b0 100644 --- a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift +++ b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift @@ -8,6 +8,11 @@ let ServiceNameBiometric = ServiceName + "_biometric" class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { + override init() { + super.init(); + NSApplication.shared.setActivationPolicy(.accessory) + } + func beginRequest(with context: NSExtensionContext) { let item = context.inputItems[0] as! NSExtensionItem let message = item.userInfo?[SFExtensionMessageKey] as AnyObject? @@ -54,24 +59,24 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { guard let data = blobData else { return } + let panel = NSSavePanel() - panel.isFloatingPanel = true panel.canCreateDirectories = true panel.nameFieldStringValue = dlMsg.fileName - panel.begin { response in - if response == NSApplication.ModalResponse.OK { - if let url = panel.url { - do { - let fileManager = FileManager.default - if !fileManager.fileExists(atPath: url.absoluteString) { - fileManager.createFile(atPath: url.absoluteString, contents: Data(), - attributes: nil) - } - try data.write(to: url) - } catch { - print(error) - NSLog("ERROR in downloadFile, \(error)") + let response = panel.runModal(); + + if response == NSApplication.ModalResponse.OK { + if let url = panel.url { + do { + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: url.absoluteString) { + fileManager.createFile(atPath: url.absoluteString, contents: Data(), + attributes: nil) } + try data.write(to: url) + } catch { + print(error) + NSLog("ERROR in downloadFile, \(error)") } } } From 2d2226f20df07d53e2928c9562711174cf916cf4 Mon Sep 17 00:00:00 2001 From: mimartin12 <77340197+mimartin12@users.noreply.github.com> Date: Thu, 13 Apr 2023 09:24:09 -0600 Subject: [PATCH 027/125] [DEVOPS-1260] - Update KV names (#5209) --- .github/workflows/build-web.yml | 2 +- .github/workflows/release-web.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 7becb7dff75..49e0d419a01 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -246,7 +246,7 @@ jobs: uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff with: azure-creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - azure-keyvault-name: "bitwarden-prod-kv" + azure-keyvault-name: "bitwarden-ci" - name: Build Docker image uses: docker/build-push-action@c56af957549030174b10d6867f20e78cfd7debc5 diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index 598541f1262..223198c8053 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -73,7 +73,7 @@ jobs: uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff with: azure-creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - azure-keyvault-name: "bitwarden-prod-kv" + azure-keyvault-name: "bitwarden-ci" - name: Pull branch image run: | From 5f26e58538e60aa6c1187fdd0190008c4d68ee57 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Thu, 13 Apr 2023 13:23:19 -0400 Subject: [PATCH 028/125] placeholder text being cut off (#5100) --- .../projects/projects/projects.component.html | 2 +- .../src/app/secrets-manager/secrets/secrets.component.html | 2 +- .../service-accounts/service-accounts.component.html | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.html index bc0b331d7d0..29d03a7f5e9 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects/projects.component.html @@ -1,5 +1,5 @@ - + - + - + Date: Thu, 13 Apr 2023 20:48:29 +0200 Subject: [PATCH 029/125] [AC-974] [Technical Dependency] Refactor Vault Tables (#4967) * [EC-974] feat: scaffold new vault-items component * [EC-974] feat: add basic mocked data to story * [EC-974] feat: add initial table version * [EC-974] chore: split rows into separate components * [EC-974] chore: rename item row to cipher row * [EC-974] feat: create common vault item interface * [EC-974] feat: use cdk virtual scrolling * [EC-974] fix: tweak `itemSize` * [EC-974] chore: move vault-items component to app/vault folder * [EC-974] feat: initial support for extra column * [EC-974] feat: start adding org badge Having issues with modules import * [EC-974] feat: add working owner column on collections row * [EC-974] feat: add owner to ciphers * [EC-974] fix: org name badge bugs when reused * [EC-974] feat: fix and translate columns * [EC-974] feat: allow collections to be non-editable * [EC-974] feat: use data source * [EC-974] fix: remove profile name from vault items * [EC-974] feat: add events * [EC-974] feat: add support for copy event * [EC-974] feat: add support for collections column * [EC-974] feat: add support for group badges * [EC-974] chore: rename for consistency * [EC-974] feat: change story to use template * [EC-974] feat: add support for launching * [EC-974] feat: add support for attachements * [EC-974] feat: add stories for all use-cases * [EC-974] feat: add support for cloning * [EC-974] feat: add support for moving to organization * [EC-974] feat: add support for editing cipher collections * [EC-974] feat: add support for event logs * [EC-974] feat: add support for trash/delete/restore * [EC-974] feat: add support for editing collections * [EC-974] feat: add support for access and delete collections * [EC-974] feat: don't show menu if it's empty * [EC-974] feat: initial buggy implementation of selection * [EC-974] feat: implement bulk move * [EC-974] feat: add support for bulk moving to org * [EC-974] feat: add support for bulk restore * [EC-974] feat: add support for bulk delete * [EC-974] feat: add ability to disable the table * [EC-974] feat: create new filter function based on routed model * [EC-974] wip: start replacing vault items component * [EC-974] feat: add support for fetching ciphers * [EC-974] feat: hide trash by default * [EC-974] feat: add support for the rest of the data * [EC-974] feat: implement organization filtering using org badge * [EC-974] feat: fix navigation to "my vault" * [EC-974] feat: don't show bulk move options when filtering on org items * [EC-974] feat: prepare for disabling table * [EC-974] fix: add missing router link to collections * [EC-974] feat: connect all outputs * [EC-974] fix: list not properly refreshing after delete * [EC-974] feat: limit selection to top 500 items * [EC-974] feat: implement refresh tracker * [EC-974] feat: use refresh tracker to disable vault items * [EC-974] feat: add empty list message * [AC-974] feat: add initial load with spinner and fix empty -> show list bug * [EC-974] feat: replace action promise with simple loading boolean * [EC-974] feat: refactor individual vault header * [EC-974] feat: cache and make observables long lived * [EC-974] feat: implement searching * [EC-974] feat: add support for showing collections * [EC-974] feat: add ciphers to org vault list * [EC-974] feat: show group column * [EC-974] feat: tweak settings for org vault * [EC-974] feat: implement search using query params * [EC-974] feat: add support for events that are common with individual vault * [EC-974] feat: add support for all events * [EC-974] feat: add support for empty list message and no permission message * [EC-974] feat: always show table * [EC-974] feat: fix layout issues due to incorrect row height * [EC-974] feat: disable list if empty * [EC-974] feat: improve sync handling * [EC-974] feat: improve initial loading sequence * [EC-974] feat: improve initial load sequence in org vault * [EC-974] refactor: simplify and optimize data fetching * [EC-974] feat: use observables from org service * [EC-974] feat: refactor org vault header * [EC-974] fix: data not refreshing properly * [EC-974] fix: avoid collection double fetching * [EC-974] chore: clean up refresh tracker * [EC-974] chore: clean up old vault-items components * [EC-974] chore: clean up old code in vault component * [EC-974] fix: reduce rows in story The story ends up too big for chromatic. * [EC-974] docs: tweak and typo fixes of asyncToObservable docs comment * [EC-974] fix: `attachements` typo * [EC-974] chore: remove review question comment * [EC-974] chore: remove unused `securityCode` if statement * [EC-974] fix: use `takeUntill` for legacy dialogs * [EC-974] fix: use CollectionDialogTabType instead of custom strings * [EC-974] fix: copy implementation * [EC-974] fix: use `useTotp` to check for premium features * [EC-974] fix: use `tw-sr-only` * [EC-974] chore: remove unecessary eslint disable * [EC-974] fix: clarify vault item event naming * [EC-974] fix: remove `new` from `app-new-vault-items` * [EC-974] fix: collection row not disabled during loading * [EC-974] chore: simplify router links without path changes * [EC-974] feat: invert filter function to get rid of `cipherPassesFilter` * [EC-974] fix: move `NestingDelimiter` to collection view Nesting is currently only a presentational construct, and the concept does not exist in our domain. * [EC-974] fix: org vault header not updating when switching org * [EC-974] fix: table sizing jumping around * [EC-974] fix: list not refreshing after restoring item * [EC-974] fix: re-add missing unassigned collection * [EC-974] fix don't show new item button in unassigned collection * [EC-974] fix: navigations always leading to individual vault * [EC-974] fix: remove checkbox when collections are not editable * [EC-974] fix: null reference blocking collections from refreshing after delete * [EC-974] fix: don't show checbox for collections that user does not have permissions to delete * [EC-974] fix: navigate away from deleted folder * [EC-974] chore: clean up un-used output * [EC-974] fix: org badge changing color randomly * [EC-974] fix: lint issues after merge * [EC-974] fix: lower amount of ciphers in story chromatic doesn't like large snapshots * [EC-974] fix: "all collections" not taking `organizationId` filter into account * [EC-974] fix: make sure unassigned appears in table too * [EC-974] feat: add unassigned to storybook * [EC-974] fix: forced row height not being applied properly * [EC-974] fix: hopefully fix table jumping once and for all * [EC-974] fix: attachemnts getting hidden * [EC-974] feat: extract collection editable logic to parent component * [EC-974] feat: separately track editable items * [EC-974] feat: optimize permission checks * [EC-974] fix: bulk menu hidden on chrome :lolcry: * [EC-974] fix: don't show groups column if org doesnt use groups * [EC-974] feat: make entire row clickable * [EC-974] fix: typo resulting in non-editable collections --- .../vault-cipher-row.component.html | 147 ++++ .../vault-items/vault-cipher-row.component.ts | 94 +++ .../vault-collection-row.component.html | 75 ++ .../vault-collection-row.component.ts | 72 ++ .../vault-items/vault-item-event.ts | 17 + .../components/vault-items/vault-item.ts | 7 + .../vault-items/vault-items.component.html | 105 +++ .../vault-items/vault-items.component.ts | 179 +++++ .../vault-items/vault-items.module.ts | 33 + .../vault-items/vault-items.stories.ts | 316 ++++++++ .../organization-badge.module.ts | 2 +- .../organization-name-badge.component.html | 10 +- .../organization-name-badge.component.ts | 29 +- .../pipes/get-organization-name.pipe.ts | 2 +- .../abstractions/vault-filter.service.ts | 2 +- .../routed-vault-filter-bridge.service.ts | 7 +- .../services/vault-filter.service.spec.ts | 19 +- .../services/vault-filter.service.ts | 14 +- .../shared/models/filter-function.spec.ts | 227 ++++++ .../shared/models/filter-function.ts | 81 ++ .../routed-vault-filter-bridge.model.ts | 2 +- .../vault-header/vault-header.component.html | 47 +- .../vault-header/vault-header.component.ts | 109 ++- .../vault-items.component.html | 361 --------- .../individual-vault/vault-items.component.ts | 588 --------------- .../individual-vault/vault.component.html | 59 +- .../vault/individual-vault/vault.component.ts | 642 +++++++++++++--- .../vault/individual-vault/vault.module.ts | 5 +- .../collection-badge.module.ts | 2 +- .../group-badge/group-badge.module.ts | 2 +- .../vault-filter/vault-filter.service.ts | 51 +- .../vault-header/vault-header.component.html | 62 +- .../vault-header/vault-header.component.ts | 200 ++--- .../vault/org-vault/vault-items.component.ts | 333 --------- .../app/vault/org-vault/vault.component.html | 86 ++- .../app/vault/org-vault/vault.component.ts | 695 ++++++++++++++++-- .../src/app/vault/org-vault/vault.module.ts | 5 +- .../src/app/vault/utils/collection-utils.ts | 56 ++ .../models/view/collection.view.ts | 2 + libs/common/src/misc/serviceUtils.ts | 10 +- libs/common/src/misc/utils.ts | 12 + .../src/breadcrumbs/breadcrumb.component.ts | 4 + .../breadcrumbs/breadcrumbs.component.html | 3 + .../components/src/table/table.component.html | 2 +- libs/components/src/table/table.component.ts | 10 + 45 files changed, 3011 insertions(+), 1775 deletions(-) create mode 100644 apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html create mode 100644 apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts create mode 100644 apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html create mode 100644 apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts create mode 100644 apps/web/src/app/vault/components/vault-items/vault-item-event.ts create mode 100644 apps/web/src/app/vault/components/vault-items/vault-item.ts create mode 100644 apps/web/src/app/vault/components/vault-items/vault-items.component.html create mode 100644 apps/web/src/app/vault/components/vault-items/vault-items.component.ts create mode 100644 apps/web/src/app/vault/components/vault-items/vault-items.module.ts create mode 100644 apps/web/src/app/vault/components/vault-items/vault-items.stories.ts create mode 100644 apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts create mode 100644 apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts delete mode 100644 apps/web/src/app/vault/individual-vault/vault-items.component.html delete mode 100644 apps/web/src/app/vault/individual-vault/vault-items.component.ts delete mode 100644 apps/web/src/app/vault/org-vault/vault-items.component.ts create mode 100644 apps/web/src/app/vault/utils/collection-utils.ts diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html new file mode 100644 index 00000000000..97b3875a47c --- /dev/null +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -0,0 +1,147 @@ + + + + + + + +
+ + + + {{ "attachments" | i18n }} + + + {{ "attachmentsNeedFix" | i18n }} + + +
+
+ {{ cipher.subTitle }} + + + + + + + + + + + + + + + + +
+ + {{ "launch" | i18n }} + + + + + + + + + + + diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts new file mode 100644 index 00000000000..abed3b320be --- /dev/null +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.ts @@ -0,0 +1,94 @@ +import { Component, EventEmitter, HostBinding, HostListener, Input, Output } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view"; +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { VaultItemEvent } from "./vault-item-event"; +import { RowHeightClass } from "./vault-items.component"; + +@Component({ + selector: "tr[appVaultCipherRow]", + templateUrl: "vault-cipher-row.component.html", +}) +export class VaultCipherRowComponent { + protected RowHeightClass = RowHeightClass; + + @Input() disabled: boolean; + @Input() cipher: CipherView; + @Input() showOwner: boolean; + @Input() showCollections: boolean; + @Input() showGroups: boolean; + @Input() showPremiumFeatures: boolean; + @Input() useEvents: boolean; + @Input() cloneable: boolean; + @Input() organizations: Organization[]; + @Input() collections: CollectionView[]; + + @Output() onEvent = new EventEmitter(); + + @Input() checked: boolean; + @Output() checkedToggled = new EventEmitter(); + + protected CipherType = CipherType; + + constructor(private router: Router, private activatedRoute: ActivatedRoute) {} + + @HostBinding("class") + get classes() { + return [].concat(this.disabled ? [] : ["tw-cursor-pointer"]); + } + + protected get showTotpCopyButton() { + return ( + (this.cipher.login?.hasTotp ?? false) && + (this.cipher.organizationUseTotp || this.showPremiumFeatures) + ); + } + + protected get showFixOldAttachments() { + return this.cipher.hasOldAttachments && this.cipher.organizationId == null; + } + + @HostListener("click") + protected click() { + this.router.navigate([], { + queryParams: { cipherId: this.cipher.id }, + queryParamsHandling: "merge", + }); + } + + protected copy(field: "username" | "password" | "totp") { + this.onEvent.emit({ type: "copyField", item: this.cipher, field }); + } + + protected clone() { + this.onEvent.emit({ type: "clone", item: this.cipher }); + } + + protected moveToOrganization() { + this.onEvent.emit({ type: "moveToOrganization", items: [this.cipher] }); + } + + protected editCollections() { + this.onEvent.emit({ type: "viewCollections", item: this.cipher }); + } + + protected events() { + this.onEvent.emit({ type: "viewEvents", item: this.cipher }); + } + + protected restore() { + this.onEvent.emit({ type: "restore", items: [this.cipher] }); + } + + protected deleteCipher() { + this.onEvent.emit({ type: "delete", items: [{ cipher: this.cipher }] }); + } + + protected attachments() { + this.onEvent.emit({ type: "viewAttachments", item: this.cipher }); + } +} diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html new file mode 100644 index 00000000000..6a4a17db6d7 --- /dev/null +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts new file mode 100644 index 00000000000..085d4653503 --- /dev/null +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -0,0 +1,72 @@ +import { Component, EventEmitter, HostBinding, HostListener, Input, Output } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view"; + +import { CollectionAdminView, GroupView } from "../../../admin-console/organizations/core"; + +import { VaultItemEvent } from "./vault-item-event"; +import { RowHeightClass } from "./vault-items.component"; + +@Component({ + selector: "tr[appVaultCollectionRow]", + templateUrl: "vault-collection-row.component.html", +}) +export class VaultCollectionRowComponent { + protected RowHeightClass = RowHeightClass; + + @Input() disabled: boolean; + @Input() collection: CollectionView; + @Input() showOwner: boolean; + @Input() showCollections: boolean; + @Input() showGroups: boolean; + @Input() canEditCollection: boolean; + @Input() canDeleteCollection: boolean; + @Input() organizations: Organization[]; + @Input() groups: GroupView[]; + + @Output() onEvent = new EventEmitter(); + + @Input() checked: boolean; + @Output() checkedToggled = new EventEmitter(); + + constructor(private router: Router, private activatedRoute: ActivatedRoute) {} + + @HostBinding("class") + get classes() { + return [].concat(this.disabled ? [] : ["tw-cursor-pointer"]); + } + + get collectionGroups() { + if (!(this.collection instanceof CollectionAdminView)) { + return []; + } + + return this.collection.groups; + } + + get organization() { + return this.organizations.find((o) => o.id === this.collection.organizationId); + } + + @HostListener("click") + protected click() { + this.router.navigate([], { + queryParams: { collectionId: this.collection.id }, + queryParamsHandling: "merge", + }); + } + + protected edit() { + this.onEvent.next({ type: "edit", item: this.collection }); + } + + protected access() { + this.onEvent.next({ type: "viewAccess", item: this.collection }); + } + + protected deleteCollection() { + this.onEvent.next({ type: "delete", items: [{ collection: this.collection }] }); + } +} diff --git a/apps/web/src/app/vault/components/vault-items/vault-item-event.ts b/apps/web/src/app/vault/components/vault-items/vault-item-event.ts new file mode 100644 index 00000000000..2643fcd7b13 --- /dev/null +++ b/apps/web/src/app/vault/components/vault-items/vault-item-event.ts @@ -0,0 +1,17 @@ +import { CollectionView } from "@bitwarden/common/src/admin-console/models/view/collection.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { VaultItem } from "./vault-item"; + +export type VaultItemEvent = + | { type: "viewAttachments"; item: CipherView } + | { type: "viewCollections"; item: CipherView } + | { type: "viewAccess"; item: CollectionView } + | { type: "viewEvents"; item: CipherView } + | { type: "edit"; item: CollectionView } + | { type: "clone"; item: CipherView } + | { type: "restore"; items: CipherView[] } + | { type: "delete"; items: VaultItem[] } + | { type: "copyField"; item: CipherView; field: "username" | "password" | "totp" } + | { type: "moveToFolder"; items: CipherView[] } + | { type: "moveToOrganization"; items: CipherView[] }; diff --git a/apps/web/src/app/vault/components/vault-items/vault-item.ts b/apps/web/src/app/vault/components/vault-items/vault-item.ts new file mode 100644 index 00000000000..c8c91becdbe --- /dev/null +++ b/apps/web/src/app/vault/components/vault-items/vault-item.ts @@ -0,0 +1,7 @@ +import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +export interface VaultItem { + collection?: CollectionView; + cipher?: CipherView; +} diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html new file mode 100644 index 00000000000..7179a0dfc83 --- /dev/null +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -0,0 +1,105 @@ + + + + + + + + + {{ "name" | i18n }} + {{ "owner" | i18n }} + {{ "collections" | i18n }} + {{ "groups" | i18n }} + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts new file mode 100644 index 00000000000..4fcf0439920 --- /dev/null +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -0,0 +1,179 @@ +import { SelectionModel } from "@angular/cdk/collections"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { TableDataSource } from "@bitwarden/components"; + +import { CollectionAdminView, GroupView } from "../../../admin-console/organizations/core"; +import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; + +import { VaultItem } from "./vault-item"; +import { VaultItemEvent } from "./vault-item-event"; + +// Fixed manual row height required due to how cdk-virtual-scroll works +export const RowHeight = 65; +export const RowHeightClass = `tw-h-[65px]`; + +const MaxSelectionCount = 500; + +@Component({ + selector: "app-vault-items", + templateUrl: "vault-items.component.html", + // TODO: Improve change detection, see: https://bitwarden.atlassian.net/browse/TDL-220 + // changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VaultItemsComponent { + protected RowHeight = RowHeight; + + @Input() disabled: boolean; + @Input() showOwner: boolean; + @Input() showCollections: boolean; + @Input() showGroups: boolean; + @Input() useEvents: boolean; + @Input() editableCollections: boolean; + @Input() cloneableOrganizationCiphers: boolean; + @Input() showPremiumFeatures: boolean; + @Input() showBulkMove: boolean; + @Input() showBulkTrashOptions: boolean; + @Input() allOrganizations: Organization[] = []; + @Input() allCollections: CollectionView[] = []; + @Input() allGroups: GroupView[] = []; + + private _ciphers?: CipherView[] = []; + @Input() get ciphers(): CipherView[] { + return this._ciphers; + } + set ciphers(value: CipherView[] | undefined) { + this._ciphers = value ?? []; + this.refreshItems(); + } + + private _collections?: CollectionView[] = []; + @Input() get collections(): CollectionView[] { + return this._collections; + } + set collections(value: CollectionView[] | undefined) { + this._collections = value ?? []; + this.refreshItems(); + } + + @Output() onEvent = new EventEmitter(); + + protected editableItems: VaultItem[] = []; + protected dataSource = new TableDataSource(); + protected selection = new SelectionModel(true, [], true); + + get showExtraColumn() { + return this.showCollections || this.showGroups || this.showOwner; + } + + get isAllSelected() { + return this.editableItems + .slice(0, MaxSelectionCount) + .every((item) => this.selection.isSelected(item)); + } + + get isEmpty() { + return this.dataSource.data.length === 0; + } + + protected canEditCollection(collection: CollectionView): boolean { + // We currently don't support editing collections from individual vault + if (!(collection instanceof CollectionAdminView)) { + return false; + } + + // Only allow allow deletion if collection editing is enabled and not deleting "Unassigned" + if (!this.editableCollections || collection.id === Unassigned) { + return false; + } + + const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); + + // Otherwise, check if we can edit the specified collection + return ( + organization?.canEditAnyCollection || + (organization?.canEditAssignedCollections && collection.assigned) + ); + } + + protected canDeleteCollection(collection: CollectionView): boolean { + // We currently don't support editing collections from individual vault + if (!(collection instanceof CollectionAdminView)) { + return false; + } + + // Only allow allow deletion if collection editing is enabled and not deleting "Unassigned" + if (!this.editableCollections || collection.id === Unassigned) { + return false; + } + + const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); + + // Otherwise, check if we can delete the specified collection + return ( + organization?.canDeleteAnyCollection || + (organization?.canDeleteAssignedCollections && collection.assigned) + ); + } + + protected toggleAll() { + this.isAllSelected + ? this.selection.clear() + : this.selection.select(...this.editableItems.slice(0, MaxSelectionCount)); + } + + protected event(event: VaultItemEvent) { + this.onEvent.emit(event); + } + + protected bulkMoveToFolder() { + this.event({ + type: "moveToFolder", + items: this.selection.selected + .filter((item) => item.cipher !== undefined) + .map((item) => item.cipher), + }); + } + + protected bulkMoveToOrganization() { + this.event({ + type: "moveToOrganization", + items: this.selection.selected + .filter((item) => item.cipher !== undefined) + .map((item) => item.cipher), + }); + } + + protected bulkRestore() { + this.event({ + type: "restore", + items: this.selection.selected + .filter((item) => item.cipher !== undefined) + .map((item) => item.cipher), + }); + } + + protected bulkDelete() { + this.event({ + type: "delete", + items: this.selection.selected, + }); + } + + private refreshItems() { + const collections: VaultItem[] = this.collections.map((collection) => ({ collection })); + const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher })); + const items: VaultItem[] = [].concat(collections).concat(ciphers); + + this.selection.clear(); + this.editableItems = items.filter( + (item) => + item.cipher !== undefined || + (item.collection !== undefined && this.canDeleteCollection(item.collection)) + ); + this.dataSource.data = items; + } +} diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.module.ts b/apps/web/src/app/vault/components/vault-items/vault-items.module.ts new file mode 100644 index 00000000000..ac0d0fb1947 --- /dev/null +++ b/apps/web/src/app/vault/components/vault-items/vault-items.module.ts @@ -0,0 +1,33 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { TableModule } from "@bitwarden/components"; + +import { SharedModule } from "../../../shared/shared.module"; +import { OrganizationBadgeModule } from "../../individual-vault/organization-badge/organization-badge.module"; +import { PipesModule } from "../../individual-vault/pipes/pipes.module"; +import { CollectionBadgeModule } from "../../org-vault/collection-badge/collection-badge.module"; +import { GroupBadgeModule } from "../../org-vault/group-badge/group-badge.module"; + +import { VaultCipherRowComponent } from "./vault-cipher-row.component"; +import { VaultCollectionRowComponent } from "./vault-collection-row.component"; +import { VaultItemsComponent } from "./vault-items.component"; + +@NgModule({ + imports: [ + CommonModule, + RouterModule, + ScrollingModule, + SharedModule, + TableModule, + OrganizationBadgeModule, + CollectionBadgeModule, + GroupBadgeModule, + PipesModule, + ], + declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent], + exports: [VaultItemsComponent], +}) +export class VaultItemsModule {} diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts new file mode 100644 index 00000000000..5ce71b1eeb7 --- /dev/null +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -0,0 +1,316 @@ +import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { Meta, moduleMetadata, Story } from "@storybook/angular"; +import { BehaviorSubject } from "rxjs"; + +import { AvatarUpdateService } from "@bitwarden/common/abstractions/account/avatar-update.service"; +import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; +import { StateService } from "@bitwarden/common/abstractions/state.service"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; +import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key"; +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; +import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; + +import { + CollectionAccessSelectionView, + CollectionAdminView, + GroupView, +} from "../../../admin-console/organizations/core"; +import { PreloadedEnglishI18nModule } from "../../../tests/preloaded-english-i18n.module"; +import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; + +import { VaultItemsComponent } from "./vault-items.component"; +import { VaultItemsModule } from "./vault-items.module"; + +@Component({ + template: "", +}) +class EmptyComponent {} + +const organizations = [...new Array(3).keys()].map(createOrganization); +const groups = [...Array(3).keys()].map(createGroupView); +const collections = [...Array(5).keys()].map(createCollectionView); +const ciphers = [...Array(50).keys()].map((i) => createCipherView(i)); +const deletedCiphers = [...Array(15).keys()].map((i) => createCipherView(i, true)); +const organizationOnlyCiphers = ciphers.filter((c) => c.organizationId != undefined); +const deletedOrganizationOnlyCiphers = deletedCiphers.filter((c) => c.organizationId != undefined); + +export default { + title: "Web/Vault/Items", + component: VaultItemsComponent, + decorators: [ + moduleMetadata({ + imports: [ + VaultItemsModule, + PreloadedEnglishI18nModule, + RouterModule.forRoot([{ path: "**", component: EmptyComponent }], { useHash: true }), + ], + providers: [ + { + provide: EnvironmentService, + useValue: { + getIconsUrl() { + return ""; + }, + } as Partial, + }, + { + provide: StateService, + useValue: { + activeAccount$: new BehaviorSubject("1").asObservable(), + accounts$: new BehaviorSubject({ "1": { profile: { name: "Foo" } } }).asObservable(), + async getDisableFavicon() { + return false; + }, + } as Partial, + }, + { + provide: AvatarUpdateService, + useValue: { + async loadColorFromState() { + return "#FF0000"; + }, + } as Partial, + }, + { + provide: TokenService, + useValue: { + async getUserId() { + return "user-id"; + }, + async getName() { + return "name"; + }, + async getEmail() { + return "email"; + }, + } as Partial, + }, + ], + }), + ], + args: { + disabled: false, + allCollections: collections, + allGroups: groups, + allOrganizations: organizations, + }, + argTypes: { onEvent: { action: "onEvent" } }, +} as Meta; + +const Template: Story = (args: VaultItemsComponent) => ({ + props: args, +}); + +export const Individual = Template.bind({}); +Individual.args = { + ciphers, + collections: [], + showOwner: true, + showCollections: false, + showGroups: false, + showPremiumFeatures: true, + showBulkMove: true, + showBulkTrashOptions: false, + useEvents: false, + editableCollections: false, + cloneableOrganizationCiphers: false, +}; + +export const IndividualDisabled = Template.bind({}); +IndividualDisabled.args = { + ciphers, + collections: [], + disabled: true, + showOwner: true, + showCollections: false, + showGroups: false, + showPremiumFeatures: true, + showBulkMove: true, + showBulkTrashOptions: false, + useEvents: false, + editableCollections: false, + cloneableOrganizationCiphers: false, +}; + +export const IndividualTrash = Template.bind({}); +IndividualTrash.args = { + ciphers: deletedCiphers, + collections: [], + showOwner: true, + showCollections: false, + showGroups: false, + showPremiumFeatures: true, + showBulkMove: false, + showBulkTrashOptions: true, + useEvents: false, + editableCollections: false, + cloneableOrganizationCiphers: false, +}; + +export const IndividualTopLevelCollection = Template.bind({}); +IndividualTopLevelCollection.args = { + ciphers: [], + collections, + showOwner: true, + showCollections: false, + showGroups: false, + showPremiumFeatures: true, + showBulkMove: false, + showBulkTrashOptions: false, + useEvents: false, + editableCollections: false, + cloneableOrganizationCiphers: false, +}; + +export const IndividualSecondLevelCollection = Template.bind({}); +IndividualSecondLevelCollection.args = { + ciphers, + collections, + showOwner: true, + showCollections: false, + showGroups: false, + showPremiumFeatures: true, + showBulkMove: true, + showBulkTrashOptions: false, + useEvents: false, + editableCollections: false, + cloneableOrganizationCiphers: false, +}; + +export const OrganizationVault = Template.bind({}); +OrganizationVault.args = { + ciphers: organizationOnlyCiphers, + collections: [], + showOwner: false, + showCollections: true, + showGroups: false, + showPremiumFeatures: true, + showBulkMove: false, + showBulkTrashOptions: false, + useEvents: true, + editableCollections: true, + cloneableOrganizationCiphers: true, +}; + +export const OrganizationTrash = Template.bind({}); +OrganizationTrash.args = { + ciphers: deletedOrganizationOnlyCiphers, + collections: [], + showOwner: false, + showCollections: true, + showGroups: false, + showPremiumFeatures: true, + showBulkMove: false, + showBulkTrashOptions: true, + useEvents: true, + editableCollections: true, + cloneableOrganizationCiphers: true, +}; + +const unassignedCollection = new CollectionAdminView(); +unassignedCollection.id = Unassigned; +unassignedCollection.name = "Unassigned"; +export const OrganizationTopLevelCollection = Template.bind({}); +OrganizationTopLevelCollection.args = { + ciphers: [], + collections: collections.concat(unassignedCollection), + showOwner: false, + showCollections: false, + showGroups: true, + showPremiumFeatures: true, + showBulkMove: false, + showBulkTrashOptions: false, + useEvents: true, + editableCollections: true, + cloneableOrganizationCiphers: true, +}; + +export const OrganizationSecondLevelCollection = Template.bind({}); +OrganizationSecondLevelCollection.args = { + ciphers: organizationOnlyCiphers, + collections, + showOwner: false, + showCollections: false, + showGroups: true, + showPremiumFeatures: true, + showBulkMove: false, + showBulkTrashOptions: false, + useEvents: true, + editableCollections: true, + cloneableOrganizationCiphers: true, +}; + +function createCipherView(i: number, deleted = false): CipherView { + const organization = organizations[i % (organizations.length + 1)]; + const collection = collections[i % (collections.length + 1)]; + const view = new CipherView(); + view.id = `cipher-${i}`; + view.name = `Vault item ${i}`; + view.type = CipherType.Login; + view.organizationId = organization?.id; + view.deletedDate = deleted ? new Date() : undefined; + view.login = new LoginView(); + view.login.username = i % 10 === 0 ? undefined : `username-${i}`; + view.login.totp = i % 2 === 0 ? "I65VU7K5ZQL7WB4E" : undefined; + view.login.uris = [new LoginUriView()]; + view.login.uris[0].uri = "https://bitwarden.com"; + view.collectionIds = collection ? [collection.id] : []; + + if (i === 0) { + // Old attachment + const attachement = new AttachmentView(); + view.organizationId = null; + view.collectionIds = []; + view.attachments = [attachement]; + } else if (i % 5 === 0) { + const attachement = new AttachmentView(); + attachement.key = new SymmetricCryptoKey(new ArrayBuffer(32)); + view.attachments = [attachement]; + } + + return view; +} + +function createCollectionView(i: number): CollectionAdminView { + const organization = organizations[i % (organizations.length + 1)]; + const group = groups[i % (groups.length + 1)]; + const view = new CollectionAdminView(); + view.id = `collection-${i}`; + view.name = `Collection ${i}`; + view.organizationId = organization?.id; + + if (group !== undefined) { + view.groups = [ + new CollectionAccessSelectionView({ + id: group.id, + hidePasswords: false, + readOnly: false, + }), + ]; + } + + return view; +} + +function createGroupView(i: number): GroupView { + const organization = organizations[i % organizations.length]; + const view = new GroupView(); + view.id = `group-${i}`; + view.name = `Group ${i}`; + view.organizationId = organization.id; + return view; +} + +function createOrganization(i: number): Organization { + const organization = new Organization(); + organization.id = `organization-${i}`; + organization.name = `Organization ${i}`; + organization.type = OrganizationUserType.Owner; + return organization; +} diff --git a/apps/web/src/app/vault/individual-vault/organization-badge/organization-badge.module.ts b/apps/web/src/app/vault/individual-vault/organization-badge/organization-badge.module.ts index 88069740602..e585d165d42 100644 --- a/apps/web/src/app/vault/individual-vault/organization-badge/organization-badge.module.ts +++ b/apps/web/src/app/vault/individual-vault/organization-badge/organization-badge.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { SharedModule } from "../../../shared"; +import { SharedModule } from "../../../shared/shared.module"; import { OrganizationNameBadgeComponent } from "./organization-name-badge.component"; diff --git a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.html b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.html index d19df048534..a4745c07203 100644 --- a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.html +++ b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.html @@ -1,11 +1,13 @@ - - diff --git a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts index faaf01b309c..702aff82463 100644 --- a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts +++ b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts @@ -1,20 +1,23 @@ -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { Component, Input, OnChanges } from "@angular/core"; import { AvatarUpdateService } from "@bitwarden/common/abstractions/account/avatar-update.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { Utils } from "@bitwarden/common/misc/utils"; +import { Unassigned } from "../vault-filter/shared/models/routed-vault-filter.model"; + @Component({ selector: "app-org-badge", templateUrl: "organization-name-badge.component.html", }) -export class OrganizationNameBadgeComponent implements OnInit { +export class OrganizationNameBadgeComponent implements OnChanges { + @Input() organizationId?: string; @Input() organizationName: string; - @Input() profileName: string; - - @Output() onOrganizationClicked = new EventEmitter(); + @Input() disabled: boolean; + // Need a separate variable or we get weird behavior when used as part of cdk virtual scrolling + name: string; color: string; textColor: string; isMe: boolean; @@ -25,12 +28,13 @@ export class OrganizationNameBadgeComponent implements OnInit { private tokenService: TokenService ) {} - async ngOnInit(): Promise { - if (this.organizationName == null || this.organizationName === "") { - this.organizationName = this.i18nService.t("me"); - this.isMe = true; - } + // ngOnChanges is required since this component might be reused as part of + // cdk virtual scrolling + async ngOnChanges() { + this.isMe = this.organizationName == null || this.organizationName === ""; + if (this.isMe) { + this.name = this.i18nService.t("me"); this.color = await this.avatarService.loadColorFromState(); if (this.color == null) { const userId = await this.tokenService.getUserId(); @@ -43,12 +47,13 @@ export class OrganizationNameBadgeComponent implements OnInit { } } } else { + this.name = this.organizationName; this.color = Utils.stringToColor(this.organizationName.toUpperCase()); } this.textColor = Utils.pickTextColorBasedOnBgColor(this.color, 135, true) + "!important"; } - emitOnOrganizationClicked() { - this.onOrganizationClicked.emit(); + get organizationIdLink() { + return this.organizationId ?? Unassigned; } } diff --git a/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts b/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts index 0ec8ae5afba..4d6c0b7d8d7 100644 --- a/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts +++ b/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts @@ -8,7 +8,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga }) export class GetOrgNameFromIdPipe implements PipeTransform { transform(value: string, organizations: Organization[]) { - const orgName = organizations.find((o) => o.id === value)?.name; + const orgName = organizations?.find((o) => o.id === value)?.name; return orgName; } } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts index 23b7541c67a..3a03a338e3a 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts @@ -20,7 +20,7 @@ export abstract class VaultFilterService { folderTree$: Observable>; collectionTree$: Observable>; cipherTypeTree$: Observable>; - reloadCollections: () => Promise; + reloadCollections: (collections: CollectionView[]) => void; getCollectionNodeFromTree: (id: string) => Promise>; setCollapsedFilterNodes: (collapsedFilterNodes: Set) => Promise; expandOrgFilter: () => Promise; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts index a532ecff780..cce5a5e24a8 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts @@ -151,7 +151,12 @@ function createLegacyFilterForEndUser( legacyFilter.selectedFolderNode = ServiceUtils.getTreeNodeObject(folderTree, filter.folderId); } - if (filter.organizationId !== undefined) { + if (filter.organizationId !== undefined && filter.organizationId === Unassigned) { + legacyFilter.selectedOrganizationNode = ServiceUtils.getTreeNodeObject( + organizationTree, + "MyVault" + ); + } else if (filter.organizationId !== undefined && filter.organizationId !== Unassigned) { legacyFilter.selectedOrganizationNode = ServiceUtils.getTreeNodeObject( organizationTree, filter.organizationId diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts index 45fa73ed26b..c56d780f067 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts @@ -3,7 +3,6 @@ import { firstValueFrom, ReplaySubject, take } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; -import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -23,7 +22,6 @@ describe("vault filter service", () => { let organizationService: MockProxy; let folderService: MockProxy; let cipherService: MockProxy; - let collectionService: MockProxy; let policyService: MockProxy; let i18nService: MockProxy; let organizations: ReplaySubject; @@ -34,7 +32,6 @@ describe("vault filter service", () => { organizationService = mock(); folderService = mock(); cipherService = mock(); - collectionService = mock(); policyService = mock(); i18nService = mock(); i18nService.collator = new Intl.Collator("en-US"); @@ -50,7 +47,6 @@ describe("vault filter service", () => { organizationService, folderService, cipherService, - collectionService, policyService, i18nService ); @@ -177,8 +173,7 @@ describe("vault filter service", () => { createCollectionView("1", "collection 1", "org test id"), createCollectionView("2", "collection 2", "non matching org id"), ]; - collectionService.getAllDecrypted.mockResolvedValue(storedCollections); - vaultFilterService.reloadCollections(); + vaultFilterService.reloadCollections(storedCollections); await expect(firstValueFrom(vaultFilterService.filteredCollections$)).resolves.toEqual([ createCollectionView("1", "collection 1", "org test id"), @@ -193,8 +188,7 @@ describe("vault filter service", () => { createCollectionView("id-2", "Collection 1/Collection 2", "org test id"), createCollectionView("id-3", "Collection 1/Collection 3", "org test id"), ]; - collectionService.getAllDecrypted.mockResolvedValue(storedCollections); - vaultFilterService.reloadCollections(); + vaultFilterService.reloadCollections(storedCollections); const result = await firstValueFrom(vaultFilterService.collectionTree$); @@ -207,8 +201,7 @@ describe("vault filter service", () => { createCollectionView("id-1", "Collection 1", "org test id"), createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"), ]; - collectionService.getAllDecrypted.mockResolvedValue(storedCollections); - vaultFilterService.reloadCollections(); + vaultFilterService.reloadCollections(storedCollections); const result = await firstValueFrom(vaultFilterService.collectionTree$); @@ -224,8 +217,7 @@ describe("vault filter service", () => { createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"), createCollectionView("id-4", "Collection 1/Collection 4", "org test id"), ]; - collectionService.getAllDecrypted.mockResolvedValue(storedCollections); - vaultFilterService.reloadCollections(); + vaultFilterService.reloadCollections(storedCollections); const result = await firstValueFrom(vaultFilterService.collectionTree$); @@ -243,8 +235,7 @@ describe("vault filter service", () => { createCollectionView("id-1", "Collection 1", "org test id"), createCollectionView("id-3", "Collection 1/Collection 2/Collection 3", "org test id"), ]; - collectionService.getAllDecrypted.mockResolvedValue(storedCollections); - vaultFilterService.reloadCollections(); + vaultFilterService.reloadCollections(storedCollections); const result = await firstValueFrom(vaultFilterService.collectionTree$); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index ab0b63b669f..e4f605caf8d 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -1,6 +1,7 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject, + combineLatest, combineLatestWith, firstValueFrom, map, @@ -12,7 +13,6 @@ import { import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; -import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service"; import { isNotProviderUser, OrganizationService, @@ -67,8 +67,10 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { // TODO: Remove once collections is refactored with observables // replace with collection service observable private collectionViews$ = new ReplaySubject(1); - filteredCollections$: Observable = this.collectionViews$.pipe( - combineLatestWith(this._organizationFilter), + filteredCollections$: Observable = combineLatest([ + this.collectionViews$, + this._organizationFilter, + ]).pipe( switchMap(([collections, org]) => { return this.filterCollections(collections, org); }) @@ -84,14 +86,12 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { protected organizationService: OrganizationService, protected folderService: FolderService, protected cipherService: CipherService, - protected collectionService: CollectionService, protected policyService: PolicyService, protected i18nService: I18nService ) {} - // TODO: Remove once collections is refactored with observables - async reloadCollections() { - this.collectionViews$.next(await this.collectionService.getAllDecrypted()); + async reloadCollections(collections: CollectionView[]) { + this.collectionViews$.next(collections); } async getCollectionNodeFromTree(id: string) { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts new file mode 100644 index 00000000000..adb0d5d4f80 --- /dev/null +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts @@ -0,0 +1,227 @@ +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { createFilterFunction } from "./filter-function"; +import { Unassigned, All } from "./routed-vault-filter.model"; + +describe("createFilter", () => { + describe("given a generic cipher", () => { + it("should return true when no filter is applied", () => { + const cipher = createCipher(); + const filterFunction = createFilterFunction({}); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); + }); + + describe("given a favorite cipher", () => { + const cipher = createCipher({ favorite: true }); + + it("should return true when filtering for favorites", () => { + const filterFunction = createFilterFunction({ type: "favorites" }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); + + it("should return false when filtering for trash", () => { + const filterFunction = createFilterFunction({ type: "trash" }); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + }); + + describe("given a deleted cipher", () => { + const cipher = createCipher({ deletedDate: new Date() }); + + it("should return true when filtering for trash", () => { + const filterFunction = createFilterFunction({ type: "trash" }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); + + it("should return false when filtering for favorites", () => { + const filterFunction = createFilterFunction({ type: "favorites" }); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + + it("should return false when type is not specified in filter", () => { + const filterFunction = createFilterFunction({}); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + }); + + describe("given a cipher with type", () => { + it("should return true when filter matches cipher type", () => { + const cipher = createCipher({ type: CipherType.Identity }); + const filterFunction = createFilterFunction({ type: "identity" }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); + + it("should return false when filter does not match cipher type", () => { + const cipher = createCipher({ type: CipherType.Card }); + const filterFunction = createFilterFunction({ type: "favorites" }); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + }); + + describe("given a cipher with folder id", () => { + it("should return true when filter matches folder id", () => { + const cipher = createCipher({ folderId: "folderId" }); + const filterFunction = createFilterFunction({ folderId: "folderId" }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); + + it("should return false when filter does not match folder id", () => { + const cipher = createCipher({ folderId: "folderId" }); + const filterFunction = createFilterFunction({ folderId: "differentFolderId" }); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + }); + + describe("given a cipher without folder", () => { + const cipher = createCipher({ folderId: null }); + + it("should return true when filtering on unassigned folder", () => { + const filterFunction = createFilterFunction({ folderId: Unassigned }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); + }); + + describe("given an organizational cipher (with organization and collections)", () => { + const cipher = createCipher({ + organizationId: "organizationId", + collectionIds: ["collectionId", "anotherId"], + }); + + it("should return true when filter matches collection id", () => { + const filterFunction = createFilterFunction({ + collectionId: "collectionId", + organizationId: "organizationId", + }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); + + it("should return false when filter does not match collection id", () => { + const filterFunction = createFilterFunction({ + collectionId: "nonMatchingCollectionId", + organizationId: "organizationId", + }); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + + it("should return false when filter does not match organization id", () => { + const filterFunction = createFilterFunction({ + organizationId: "nonMatchingOrganizationId", + }); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + + it("should return false when filtering for my vault only", () => { + const filterFunction = createFilterFunction({ organizationId: Unassigned }); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + + it("should return false when filtering by All Collections", () => { + const filterFunction = createFilterFunction({ collectionId: All }); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + }); + + describe("given an unassigned organizational cipher (with organization, without collection)", () => { + const cipher = createCipher({ organizationId: "organizationId", collectionIds: [] }); + + it("should return true when filtering for unassigned collection", () => { + const filterFunction = createFilterFunction({ collectionId: Unassigned }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); + + it("should return true when filter matches organization id", () => { + const filterFunction = createFilterFunction({ organizationId: "organizationId" }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); + }); + + describe("given an individual cipher (without organization or collection)", () => { + const cipher = createCipher({ organizationId: null, collectionIds: [] }); + + it("should return false when filtering for unassigned collection", () => { + const filterFunction = createFilterFunction({ collectionId: Unassigned }); + + const result = filterFunction(cipher); + + expect(result).toBe(false); + }); + + it("should return true when filtering for my vault only", () => { + const cipher = createCipher({ organizationId: null }); + const filterFunction = createFilterFunction({ organizationId: Unassigned }); + + const result = filterFunction(cipher); + + expect(result).toBe(true); + }); + }); +}); + +function createCipher(options: Partial = {}) { + const cipher = new CipherView(); + + cipher.favorite = options.favorite ?? false; + cipher.deletedDate = options.deletedDate; + cipher.type = options.type; + cipher.folderId = options.folderId; + cipher.collectionIds = options.collectionIds; + cipher.organizationId = options.organizationId; + + return cipher; +} diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts new file mode 100644 index 00000000000..c8d21e314d6 --- /dev/null +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts @@ -0,0 +1,81 @@ +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { All, RoutedVaultFilterModel, Unassigned } from "./routed-vault-filter.model"; + +export type FilterFunction = (cipher: CipherView) => boolean; + +export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunction { + return (cipher) => { + if (filter.type === "favorites" && !cipher.favorite) { + return false; + } + if (filter.type === "card" && cipher.type !== CipherType.Card) { + return false; + } + if (filter.type === "identity" && cipher.type !== CipherType.Identity) { + return false; + } + if (filter.type === "login" && cipher.type !== CipherType.Login) { + return false; + } + if (filter.type === "note" && cipher.type !== CipherType.SecureNote) { + return false; + } + if (filter.type === "trash" && !cipher.isDeleted) { + return false; + } + // Hide trash unless explicitly selected + if (filter.type !== "trash" && cipher.isDeleted) { + return false; + } + // No folder + if (filter.folderId === Unassigned && cipher.folderId !== null) { + return false; + } + // Folder + if ( + filter.folderId !== undefined && + filter.folderId !== All && + filter.folderId !== Unassigned && + cipher.folderId !== filter.folderId + ) { + return false; + } + // All collections (top level) + if (filter.collectionId === All) { + return false; + } + // Unassigned + if ( + filter.collectionId === Unassigned && + (cipher.organizationId == null || + (cipher.collectionIds != null && cipher.collectionIds.length > 0)) + ) { + return false; + } + // Collection + if ( + filter.collectionId !== undefined && + filter.collectionId !== All && + filter.collectionId !== Unassigned && + (cipher.collectionIds == null || !cipher.collectionIds.includes(filter.collectionId)) + ) { + return false; + } + // My Vault + if (filter.organizationId === Unassigned && cipher.organizationId != null) { + return false; + } + // Organization + else if ( + filter.organizationId !== undefined && + filter.organizationId !== Unassigned && + cipher.organizationId !== filter.organizationId + ) { + return false; + } + + return true; + }; +} diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts index c2a25b6751f..07a901c8eba 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts @@ -53,7 +53,7 @@ export class RoutedVaultFilterBridge implements VaultFilter { set selectedOrganizationNode(value: TreeNode) { this.bridgeService.navigate({ ...this.routedFilter, - organizationId: value.node.id, + organizationId: value?.node.id === "MyVault" ? Unassigned : value?.node.id, folderId: undefined, collectionId: undefined, }); diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html index d1280139464..1697dd7216b 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.html @@ -1,40 +1,45 @@
- + - - - {{ activeOrganizationId | orgNameFromId : (organizations$ | async) }} - {{ "vault" | i18n | lowercase }} - - {{ collection.node.name }} + {{ activeOrganizationId | orgNameFromId : organizations }} {{ "vault" | i18n | lowercase }} + + + {{ collection.name }} + +

{{ title }} - - - - {{ "loading" | i18n }} - + + + {{ "loading" | i18n }}

-
+
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ "attachments" | i18n }} - - - {{ "attachmentsNeedFix" | i18n }} - - -
- {{ c.subTitle }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - {{ "loading" | i18n }} - - - - -

{{ "noPermissionToViewAllCollectionItems" | i18n }}

-
- -

{{ "noItemsInList" | i18n }}

- -
-
-
- diff --git a/apps/web/src/app/vault/individual-vault/vault-items.component.ts b/apps/web/src/app/vault/individual-vault/vault-items.component.ts deleted file mode 100644 index 98f52bf5ca3..00000000000 --- a/apps/web/src/app/vault/individual-vault/vault-items.component.ts +++ /dev/null @@ -1,588 +0,0 @@ -import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core"; -import { lastValueFrom } from "rxjs"; - -import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; -import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; -import { StateService } from "@bitwarden/common/abstractions/state.service"; -import { TotpService } from "@bitwarden/common/abstractions/totp.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view"; -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { EventType } from "@bitwarden/common/enums"; -import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; -import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { DialogService, Icons } from "@bitwarden/components"; - -import { CollectionAdminView, GroupView } from "../../admin-console/organizations/core"; - -import { - BulkDeleteDialogResult, - openBulkDeleteDialog, -} from "./bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component"; -import { - BulkMoveDialogResult, - openBulkMoveDialog, -} from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component"; -import { - BulkRestoreDialogResult, - openBulkRestoreDialog, -} from "./bulk-action-dialogs/bulk-restore-dialog/bulk-restore-dialog.component"; -import { - BulkShareDialogResult, - openBulkShareDialog, -} from "./bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component"; -import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service"; -import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model"; -import { CollectionFilter } from "./vault-filter/shared/models/vault-filter.type"; - -const MaxCheckedCount = 500; - -export type VaultItemRow = (CipherView | TreeNode) & { checked?: boolean }; - -@Component({ - selector: "app-vault-items", - templateUrl: "vault-items.component.html", -}) -export class VaultItemsComponent extends BaseVaultItemsComponent implements OnDestroy { - @Output() activeFilterChanged = new EventEmitter(); - @Output() onAttachmentsClicked = new EventEmitter(); - @Output() onShareClicked = new EventEmitter(); - @Output() onEditCipherCollectionsClicked = new EventEmitter(); - @Output() onCloneClicked = new EventEmitter(); - @Output() onOrganzationBadgeClicked = new EventEmitter(); - - private _activeFilter: VaultFilter; - @Input() get activeFilter(): VaultFilter { - return this._activeFilter; - } - set activeFilter(value: VaultFilter) { - this._activeFilter = value; - this.reload(this.activeFilter.buildFilter(), this.activeFilter.isDeleted); - } - - cipherType = CipherType; - actionPromise: Promise; - userHasPremiumAccess = false; - organizations: Organization[] = []; - profileName: string; - noItemIcon = Icons.Search; - groups: GroupView[] = []; - - protected pageSizeLimit = 200; - protected isAllChecked = false; - protected didScroll = false; - protected currentPagedCiphersCount = 0; - protected currentPagedCollectionsCount = 0; - protected refreshing = false; - - protected pagedCiphers: CipherView[] = []; - protected pagedCollections: TreeNode[] = []; - protected searchedCollections: TreeNode[] = []; - - get showAddNew() { - return !this.activeFilter.isDeleted; - } - - get collections(): TreeNode[] { - return this.activeFilter?.selectedCollectionNode?.children ?? []; - } - - get filteredCollections(): TreeNode[] { - if (this.isPaging()) { - return this.pagedCollections; - } - - if (this.searchService.isSearchable(this.searchText)) { - return this.searchedCollections; - } - - return this.collections; - } - - get filteredCiphers(): CipherView[] { - return this.isPaging() ? this.pagedCiphers : this.ciphers; - } - - constructor( - searchService: SearchService, - protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, - protected vaultFilterService: VaultFilterService, - cipherService: CipherService, - protected eventCollectionService: EventCollectionService, - protected totpService: TotpService, - protected stateService: StateService, - protected passwordRepromptService: PasswordRepromptService, - protected dialogService: DialogService, - protected logService: LogService, - private searchPipe: SearchPipe, - private organizationService: OrganizationService, - private tokenService: TokenService - ) { - super(searchService, cipherService); - } - - ngOnDestroy() { - this.checkAll(false); - } - - async applyFilter(filter: (cipher: CipherView) => boolean = null) { - this.checkAll(false); - this.isAllChecked = false; - this.pagedCollections = []; - if (!this.refreshing && this.isPaging()) { - this.currentPagedCollectionsCount = 0; - this.currentPagedCiphersCount = 0; - } - await super.applyFilter(filter); - } - - // load() is called after the page loads and the first sync has completed. - // Do not use ngOnInit() for anything that requires sync data. - async load(filter: (cipher: CipherView) => boolean = null, deleted = false) { - await super.load(filter, deleted); - this.updateSearchedCollections(this.collections); - this.profileName = await this.tokenService.getName(); - this.organizations = await this.organizationService.getAll(); - this.userHasPremiumAccess = await this.stateService.getCanAccessPremium(); - } - - async refresh() { - try { - this.refreshing = true; - await this.reload(this.filter, this.deleted); - } finally { - this.refreshing = false; - } - } - - loadMore() { - // If we have less rows than the page size, we don't need to page anything - if (this.ciphers.length + (this.collections?.length || 0) <= this.pageSizeLimit) { - return; - } - - let pageSpaceLeft = this.pageSizeLimit; - if ( - this.refreshing && - this.pagedCiphers.length + this.pagedCollections.length === 0 && - this.currentPagedCiphersCount + this.currentPagedCollectionsCount > this.pageSizeLimit - ) { - // When we refresh, we want to load the previous amount of items, not restart the paging - pageSpaceLeft = this.currentPagedCiphersCount + this.currentPagedCollectionsCount; - } - // if there are still collections to show - if (this.collections?.length > this.pagedCollections.length) { - const collectionsToAdd = this.collections.slice( - this.pagedCollections.length, - this.currentPagedCollectionsCount + pageSpaceLeft - ); - this.pagedCollections = this.pagedCollections.concat(collectionsToAdd); - // set the current count to the new count of paged collections - this.currentPagedCollectionsCount = this.pagedCollections.length; - // subtract the available page size by the amount of collections we just added, default to 0 if negative - pageSpaceLeft = - collectionsToAdd.length > pageSpaceLeft ? 0 : pageSpaceLeft - collectionsToAdd.length; - } - // if we have room left to show ciphers and we have ciphers to show - if (pageSpaceLeft > 0 && this.ciphers.length > this.pagedCiphers.length) { - this.pagedCiphers = this.pagedCiphers.concat( - this.ciphers.slice(this.pagedCiphers.length, this.currentPagedCiphersCount + pageSpaceLeft) - ); - // set the current count to the new count of paged ciphers - this.currentPagedCiphersCount = this.pagedCiphers.length; - } - // set a flag if we actually loaded the second page while paging - this.didScroll = this.pagedCiphers.length + this.pagedCollections.length > this.pageSizeLimit; - } - - isPaging() { - const searching = this.isSearching(); - if (searching && this.didScroll) { - this.resetPaging(); - } - const totalRows = - this.ciphers.length + (this.activeFilter?.selectedCollectionNode?.children.length || 0); - return !searching && totalRows > this.pageSizeLimit; - } - - async resetPaging() { - this.pagedCollections = []; - this.pagedCiphers = []; - this.loadMore(); - } - - async doSearch(indexedCiphers?: CipherView[]) { - indexedCiphers = indexedCiphers ?? (await this.cipherService.getAllDecrypted()); - this.ciphers = await this.searchService.searchCiphers( - this.searchText, - [this.filter, this.deletedFilter], - indexedCiphers - ); - this.updateSearchedCollections(this.collections); - this.resetPaging(); - } - - launch(uri: string) { - this.platformUtilsService.launchUri(uri); - } - - async attachments(c: CipherView) { - if (!(await this.repromptCipher(c))) { - return; - } - this.onAttachmentsClicked.emit(c); - } - - async share(c: CipherView) { - if (!(await this.repromptCipher(c))) { - return; - } - this.onShareClicked.emit(c); - } - - editCipherCollections(c: CipherView) { - this.onEditCipherCollectionsClicked.emit(c); - } - - async clone(c: CipherView) { - if (!(await this.repromptCipher(c))) { - return; - } - this.onCloneClicked.emit(c); - } - - async deleteCipher(c: CipherView): Promise { - if (!(await this.repromptCipher(c))) { - return; - } - if (this.actionPromise != null) { - return; - } - const permanent = c.isDeleted; - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t( - permanent ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation" - ), - this.i18nService.t(permanent ? "permanentlyDeleteItem" : "deleteItem"), - this.i18nService.t("yes"), - this.i18nService.t("no"), - "warning" - ); - if (!confirmed) { - return false; - } - - try { - this.actionPromise = this.deleteCipherWithServer(c.id, permanent); - await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem") - ); - this.refresh(); - } catch (e) { - this.logService.error(e); - } - this.actionPromise = null; - } - - async bulkDelete() { - if (!(await this.repromptCipher())) { - return; - } - - const selectedIds = this.selectedCipherIds; - if (selectedIds.length === 0) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("nothingSelected") - ); - return; - } - - const dialog = openBulkDeleteDialog(this.dialogService, { - data: { permanent: this.deleted, cipherIds: selectedIds }, - }); - - const result = await lastValueFrom(dialog.closed); - if (result === BulkDeleteDialogResult.Deleted) { - this.actionPromise = this.refresh(); - await this.actionPromise; - this.actionPromise = null; - } - } - - async restore(c: CipherView): Promise { - if (this.actionPromise != null || !c.isDeleted) { - return; - } - const confirmed = await this.platformUtilsService.showDialog( - this.i18nService.t("restoreItemConfirmation"), - this.i18nService.t("restoreItem"), - this.i18nService.t("yes"), - this.i18nService.t("no"), - "warning" - ); - if (!confirmed) { - return false; - } - - try { - this.actionPromise = this.cipherService.restoreWithServer(c.id); - await this.actionPromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); - this.refresh(); - } catch (e) { - this.logService.error(e); - } - this.actionPromise = null; - } - - async bulkRestore() { - if (!(await this.repromptCipher())) { - return; - } - - const selectedCipherIds = this.selectedCipherIds; - if (selectedCipherIds.length === 0) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("nothingSelected") - ); - return; - } - - const dialog = openBulkRestoreDialog(this.dialogService, { - data: { cipherIds: selectedCipherIds }, - }); - - const result = await lastValueFrom(dialog.closed); - if (result === BulkRestoreDialogResult.Restored) { - this.actionPromise = this.refresh(); - await this.actionPromise; - this.actionPromise = null; - } - } - - async bulkShare() { - if (!(await this.repromptCipher())) { - return; - } - - const selectedCiphers = this.selectedCiphers; - if (selectedCiphers.length === 0) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("nothingSelected") - ); - return; - } - - const dialog = openBulkShareDialog(this.dialogService, { data: { ciphers: selectedCiphers } }); - - const result = await lastValueFrom(dialog.closed); - if (result === BulkShareDialogResult.Shared) { - this.actionPromise = this.refresh(); - await this.actionPromise; - this.actionPromise = null; - } - } - - async bulkMove() { - if (!(await this.repromptCipher())) { - return; - } - - const selectedCipherIds = this.selectedCipherIds; - if (selectedCipherIds.length === 0) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("nothingSelected") - ); - return; - } - - const dialog = openBulkMoveDialog(this.dialogService, { - data: { cipherIds: selectedCipherIds }, - }); - - const result = await lastValueFrom(dialog.closed); - if (result === BulkMoveDialogResult.Moved) { - this.actionPromise = this.refresh(); - await this.actionPromise; - this.actionPromise = null; - } - } - - async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) { - if ( - this.passwordRepromptService.protectedFields().includes(aType) && - !(await this.repromptCipher(cipher)) - ) { - return; - } - - if (value == null || (aType === "TOTP" && !this.displayTotpCopyButton(cipher))) { - return; - } else if (value === cipher.login.totp) { - value = await this.totpService.getCode(value); - } - - if (!cipher.viewPassword) { - return; - } - - this.platformUtilsService.copyToClipboard(value, { window: window }); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)) - ); - - if (typeI18nKey === "password" || typeI18nKey === "verificationCodeTotp") { - this.eventCollectionService.collect( - EventType.Cipher_ClientToggledHiddenFieldVisible, - cipher.id - ); - } else if (typeI18nKey === "securityCode") { - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id); - } - } - - navigateCollection(node: TreeNode) { - const filter = this.activeFilter; - filter.selectedCollectionNode = node; - this.activeFilterChanged.emit(filter); - } - - checkAll(select: boolean) { - if (select) { - this.checkAll(false); - } - const items: VaultItemRow[] = this.ciphers; - if (!items) { - return; - } - - const selectCount = select && items.length > MaxCheckedCount ? MaxCheckedCount : items.length; - for (let i = 0; i < selectCount; i++) { - this.checkRow(items[i], select); - } - } - - checkRow(item: VaultItemRow, select?: boolean) { - // Collections can't be managed in end user vault - if (!(item instanceof CipherView)) { - return; - } - item.checked = select ?? !item.checked; - } - - get selectedCiphers(): CipherView[] { - if (!this.ciphers) { - return []; - } - return this.ciphers.filter((c) => !!(c as VaultItemRow).checked); - } - - get selectedCipherIds(): string[] { - return this.selectedCiphers.map((c) => c.id); - } - - displayTotpCopyButton(cipher: CipherView) { - return ( - (cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || this.userHasPremiumAccess) - ); - } - - onOrganizationClicked(organizationId: string) { - this.onOrganzationBadgeClicked.emit(organizationId); - } - - events(c: CipherView) { - // TODO: This should be removed but is needed since we reuse the same template - } - - canDeleteCollection(c: CollectionAdminView): boolean { - // TODO: This should be removed but is needed since we reuse the same template - return false; // Always return false for non org vault - } - - async deleteCollection(collection: CollectionView): Promise { - // TODO: This should be removed but is needed since we reuse the same template - } - - canEditCollection(c: CollectionAdminView): boolean { - // TODO: This should be removed but is needed since we reuse the same template - return false; // Always return false for non org vault - } - - async editCollection(c: CollectionView, tab: "info" | "access"): Promise { - // TODO: This should be removed but is needed since we reuse the same template - } - - get showMissingCollectionPermissionMessage(): boolean { - // TODO: This should be removed but is needed since we reuse the same template - return false; // Always return false for non org vault - } - - /** - * @deprecated Block interaction using long running modal dialog instead - */ - protected get isProcessingAction() { - return this.actionPromise != null; - } - - protected updateSearchedCollections(collections: TreeNode[]) { - if (this.searchService.isSearchable(this.searchText)) { - this.searchedCollections = this.searchPipe.transform( - collections, - this.searchText, - (collection) => collection.node.name, - (collection) => collection.node.id - ); - } - } - - protected deleteCipherWithServer(id: string, permanent: boolean) { - return permanent - ? this.cipherService.deleteWithServer(id) - : this.cipherService.softDeleteWithServer(id); - } - - protected showFixOldAttachments(c: CipherView) { - return c.hasOldAttachments && c.organizationId == null; - } - - protected async repromptCipher(c?: CipherView) { - if (c) { - return ( - c.reprompt === CipherRepromptType.None || - (await this.passwordRepromptService.showPasswordPrompt()) - ); - } else { - const selectedCiphers = this.selectedCiphers; - const notProtected = !selectedCiphers.find( - (cipher) => cipher.reprompt !== CipherRepromptType.None - ); - - return notProtected || (await this.passwordRepromptService.showPasswordPrompt()); - } - } -} diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index 25f44118755..0e7333940d3 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -17,24 +17,61 @@
{{ trashCleanupWarning }} +
+ + {{ "loading" | i18n }} +
+
+ +

{{ "noItemsInList" | i18n }}

+ +
@@ -96,5 +133,5 @@ - + diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 2ce4988e383..28bfc954315 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -8,30 +8,69 @@ import { ViewContainerRef, } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { firstValueFrom, Subject } from "rxjs"; -import { first, switchMap, takeUntil } from "rxjs/operators"; +import { BehaviorSubject, combineLatest, firstValueFrom, lastValueFrom, Subject } from "rxjs"; +import { + concatMap, + debounceTime, + filter, + first, + map, + shareReplay, + switchMap, + takeUntil, + tap, +} from "rxjs/operators"; +import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; +import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; +import { TotpService } from "@bitwarden/common/abstractions/totp.service"; +import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { KdfType, DEFAULT_PBKDF2_ITERATIONS } from "@bitwarden/common/enums"; +import { KdfType, DEFAULT_PBKDF2_ITERATIONS, EventType } from "@bitwarden/common/enums"; import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils"; +import { Utils } from "@bitwarden/common/misc/utils"; import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService, Icons } from "@bitwarden/components"; import { UpdateKeyComponent } from "../../settings/update-key.component"; +import { VaultItemEvent } from "../components/vault-items/vault-item-event"; +import { getNestedCollectionTree } from "../utils/collection-utils"; import { AddEditComponent } from "./add-edit.component"; import { AttachmentsComponent } from "./attachments.component"; +import { + BulkDeleteDialogResult, + openBulkDeleteDialog, +} from "./bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component"; +import { + BulkMoveDialogResult, + openBulkMoveDialog, +} from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component"; +import { + BulkRestoreDialogResult, + openBulkRestoreDialog, +} from "./bulk-action-dialogs/bulk-restore-dialog/bulk-restore-dialog.component"; +import { + BulkShareDialogResult, + openBulkShareDialog, +} from "./bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component"; import { CollectionsComponent } from "./collections.component"; import { FolderAddEditComponent } from "./folder-add-edit.component"; import { ShareComponent } from "./share.component"; @@ -39,11 +78,17 @@ import { VaultFilterComponent } from "./vault-filter/components/vault-filter.com import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service"; import { RoutedVaultFilterBridgeService } from "./vault-filter/services/routed-vault-filter-bridge.service"; import { RoutedVaultFilterService } from "./vault-filter/services/routed-vault-filter.service"; +import { createFilterFunction } from "./vault-filter/shared/models/filter-function"; +import { + All, + RoutedVaultFilterModel, + Unassigned, +} from "./vault-filter/shared/models/routed-vault-filter.model"; import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model"; import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type"; -import { VaultItemsComponent } from "./vault-items.component"; const BroadcasterSubscriptionId = "VaultComponent"; +const SearchTextDebounceInterval = 200; @Component({ selector: "app-vault", @@ -52,7 +97,6 @@ const BroadcasterSubscriptionId = "VaultComponent"; }) export class VaultComponent implements OnInit, OnDestroy { @ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent; - @ViewChild(VaultItemsComponent, { static: true }) vaultItemsComponent: VaultItemsComponent; @ViewChild("attachments", { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef; @ViewChild("folderAddEdit", { read: ViewContainerRef, static: true }) @@ -60,7 +104,7 @@ export class VaultComponent implements OnInit, OnDestroy { @ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef; @ViewChild("share", { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef; - @ViewChild("collections", { read: ViewContainerRef, static: true }) + @ViewChild("collectionsModal", { read: ViewContainerRef, static: true }) collectionsModalRef: ViewContainerRef; @ViewChild("updateKeyTemplate", { read: ViewContainerRef, static: true }) updateKeyModalRef: ViewContainerRef; @@ -73,6 +117,23 @@ export class VaultComponent implements OnInit, OnDestroy { trashCleanupWarning: string = null; kdfIterations: number; activeFilter: VaultFilter = new VaultFilter(); + + protected noItemIcon = Icons.Search; + protected performingInitialLoad = true; + protected refreshing = false; + protected processingEvent = false; + protected filter: RoutedVaultFilterModel = {}; + protected showBulkMove: boolean; + protected canAccessPremium: boolean; + protected allCollections: CollectionView[]; + protected allOrganizations: Organization[]; + protected ciphers: CipherView[]; + protected collections: CollectionView[]; + protected isEmpty: boolean; + protected selectedCollection: TreeNode | undefined; + + private refresh$ = new BehaviorSubject(null); + private searchText$ = new Subject(); private destroy$ = new Subject(); constructor( @@ -82,6 +143,7 @@ export class VaultComponent implements OnInit, OnDestroy { private changeDetectorRef: ChangeDetectorRef, private i18nService: I18nService, private modalService: ModalService, + private dialogService: DialogService, private tokenService: TokenService, private cryptoService: CryptoService, private messagingService: MessagingService, @@ -91,47 +153,169 @@ export class VaultComponent implements OnInit, OnDestroy { private stateService: StateService, private organizationService: OrganizationService, private vaultFilterService: VaultFilterService, + private routedVaultFilterService: RoutedVaultFilterService, private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService, private cipherService: CipherService, - private passwordRepromptService: PasswordRepromptService + private passwordRepromptService: PasswordRepromptService, + private collectionService: CollectionService, + private logService: LogService, + private totpService: TotpService, + private eventCollectionService: EventCollectionService, + private searchService: SearchService, + private searchPipe: SearchPipe ) {} async ngOnInit() { - this.showVerifyEmail = !(await this.tokenService.getEmailVerified()); this.showBrowserOutdated = window.navigator.userAgent.indexOf("MSIE") !== -1; - // disable warning for March release -> add await this.isLowKdfIteration(); when ready - this.showLowKdf = false; this.trashCleanupWarning = this.i18nService.t( this.platformUtilsService.isSelfHost() ? "trashCleanupWarningSelfHosted" : "trashCleanupWarning" ); - this.route.queryParams + const firstSetup$ = this.route.queryParams.pipe( + first(), + switchMap(async (params: Params) => { + this.showVerifyEmail = !(await this.tokenService.getEmailVerified()); + // disable warning for March release -> add await this.isLowKdfIteration(); when ready + this.showLowKdf = false; + await this.syncService.fullSync(false); + + const canAccessPremium = await this.stateService.getCanAccessPremium(); + this.showPremiumCallout = + !this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost(); + this.showUpdateKey = !(await this.cryptoService.hasEncKey()); + + const cipherId = getCipherIdFromParams(params); + if (!cipherId) { + return; + } + const cipherView = new CipherView(); + cipherView.id = cipherId; + if (params.action === "clone") { + await this.cloneCipher(cipherView); + } else if (params.action === "edit") { + await this.editCipher(cipherView); + } + }), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "syncCompleted": + if (message.successfully) { + this.refresh(); + this.changeDetectorRef.detectChanges(); + } + break; + } + }); + }); + + this.routedVaultFilterBridgeService.activeFilter$ + .pipe(takeUntil(this.destroy$)) + .subscribe((activeFilter) => { + this.activeFilter = activeFilter; + }); + + const filter$ = this.routedVaultFilterService.filter$; + const canAccessPremium$ = Utils.asyncToObservable(() => + this.stateService.getCanAccessPremium() + ).pipe(shareReplay({ refCount: true, bufferSize: 1 })); + const allCollections$ = Utils.asyncToObservable(() => + this.collectionService.getAllDecrypted() + ).pipe(shareReplay({ refCount: true, bufferSize: 1 })); + const nestedCollections$ = allCollections$.pipe( + map((collections) => getNestedCollectionTree(collections)), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + this.searchText$ + .pipe(debounceTime(SearchTextDebounceInterval), takeUntil(this.destroy$)) + .subscribe((searchText) => + this.router.navigate([], { + queryParams: { search: Utils.isNullOrEmpty(searchText) ? null : searchText }, + queryParamsHandling: "merge", + replaceUrl: true, + }) + ); + + const querySearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search)); + + const ciphers$ = combineLatest([ + Utils.asyncToObservable(() => this.cipherService.getAllDecrypted()), + filter$, + querySearchText$, + ]).pipe( + filter(([ciphers, filter]) => ciphers != undefined && filter != undefined), + concatMap(async ([ciphers, filter, searchText]) => { + const filterFunction = createFilterFunction(filter); + + if (this.searchService.isSearchable(searchText)) { + return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers); + } + + return ciphers.filter(filterFunction); + }), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + const collections$ = combineLatest([nestedCollections$, filter$, querySearchText$]).pipe( + filter(([collections, filter]) => collections != undefined && filter != undefined), + map(([collections, filter, searchText]) => { + if (filter.collectionId === undefined || filter.collectionId === Unassigned) { + return []; + } + + let collectionsToReturn = []; + if (filter.organizationId !== undefined && filter.collectionId === All) { + collectionsToReturn = collections + .filter((c) => c.node.organizationId === filter.organizationId) + .map((c) => c.node); + } else if (filter.collectionId === All) { + collectionsToReturn = collections.map((c) => c.node); + } else { + const selectedCollection = ServiceUtils.getTreeNodeObjectFromList( + collections, + filter.collectionId + ); + collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; + } + + if (this.searchService.isSearchable(searchText)) { + collectionsToReturn = this.searchPipe.transform( + collectionsToReturn, + searchText, + (collection) => collection.name, + (collection) => collection.id + ); + } + + return collectionsToReturn; + }), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + const selectedCollection$ = combineLatest([nestedCollections$, filter$]).pipe( + filter(([collections, filter]) => collections != undefined && filter != undefined), + map(([collections, filter]) => { + if ( + filter.collectionId === undefined || + filter.collectionId === All || + filter.collectionId === Unassigned + ) { + return undefined; + } + + return ServiceUtils.getTreeNodeObjectFromList(collections, filter.collectionId); + }), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + firstSetup$ .pipe( - first(), - switchMap(async (params: Params) => { - await this.syncService.fullSync(false); - await this.vaultFilterService.reloadCollections(); - await this.vaultItemsComponent.reload(); - - const canAccessPremium = await this.stateService.getCanAccessPremium(); - this.showPremiumCallout = - !this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost(); - this.showUpdateKey = !(await this.cryptoService.hasEncKey()); - - const cipherId = getCipherIdFromParams(params); - if (!cipherId) { - return; - } - const cipherView = new CipherView(); - cipherView.id = cipherId; - if (params.action === "clone") { - await this.cloneCipher(cipherView); - } else if (params.action === "edit") { - await this.editCipher(cipherView); - } - }), switchMap(() => this.route.queryParams), switchMap(async (params) => { const cipherId = getCipherIdFromParams(params); @@ -155,27 +339,54 @@ export class VaultComponent implements OnInit, OnDestroy { ) .subscribe(); - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case "syncCompleted": - if (message.successfully) { - await Promise.all([ - this.vaultFilterService.reloadCollections(), - this.vaultItemsComponent.load(this.vaultItemsComponent.filter), - ]); - this.changeDetectorRef.detectChanges(); - } - break; - } - }); - }); + firstSetup$ + .pipe( + switchMap(() => this.refresh$), + tap(() => (this.refreshing = true)), + switchMap(() => + combineLatest([ + filter$, + canAccessPremium$, + allCollections$, + this.organizationService.organizations$, + ciphers$, + collections$, + selectedCollection$, + ]) + ), + takeUntil(this.destroy$) + ) + .subscribe( + ([ + filter, + canAccessPremium, + allCollections, + allOrganizations, + ciphers, + collections, + selectedCollection, + ]) => { + this.filter = filter; + this.canAccessPremium = canAccessPremium; + this.allCollections = allCollections; + this.allOrganizations = allOrganizations; + this.ciphers = ciphers; + this.collections = collections; + this.selectedCollection = selectedCollection; - this.routedVaultFilterBridgeService.activeFilter$ - .pipe(takeUntil(this.destroy$)) - .subscribe((activeFilter) => { - this.activeFilter = activeFilter; - }); + this.showBulkMove = + filter.type !== "trash" && + (filter.organizationId === undefined || filter.organizationId === Unassigned); + this.isEmpty = collections?.length === 0 && ciphers?.length === 0; + + // This is a temporary fix to avoid double fetching collections. + // TODO: Remove when implementing new VVR menu + this.vaultFilterService.reloadCollections(allCollections); + + this.performingInitialLoad = false; + this.refreshing = false; + } + ); } get isShowingCards() { @@ -198,6 +409,44 @@ export class VaultComponent implements OnInit, OnDestroy { this.destroy$.complete(); } + async onVaultItemsEvent(event: VaultItemEvent) { + this.processingEvent = true; + try { + if (event.type === "viewAttachments") { + await this.editCipherAttachments(event.item); + } else if (event.type === "viewCollections") { + await this.editCipherCollections(event.item); + } else if (event.type === "clone") { + await this.cloneCipher(event.item); + } else if (event.type === "restore") { + if (event.items.length === 1) { + await this.restore(event.items[0]); + } else { + await this.bulkRestore(event.items); + } + } else if (event.type === "delete") { + const ciphers = event.items.filter((i) => i.collection === undefined).map((i) => i.cipher); + if (ciphers.length === 1) { + await this.deleteCipher(ciphers[0]); + } else { + await this.bulkDelete(ciphers); + } + } else if (event.type === "moveToFolder") { + await this.bulkMove(event.items); + } else if (event.type === "moveToOrganization") { + if (event.items.length === 1) { + await this.shareCipher(event.items[0]); + } else { + await this.bulkShare(event.items); + } + } else if (event.type === "copyField") { + await this.copy(event.item, event.field); + } + } finally { + this.processingEvent = false; + } + } + async applyOrganizationFilter(orgId: string) { if (orgId == null) { orgId = "MyVault"; @@ -213,8 +462,7 @@ export class VaultComponent implements OnInit, OnDestroy { this.folderAddEditModalRef, (comp) => { comp.folderId = null; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onSavedFolder.subscribe(async () => { + comp.onSavedFolder.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); }); } @@ -227,12 +475,18 @@ export class VaultComponent implements OnInit, OnDestroy { this.folderAddEditModalRef, (comp) => { comp.folderId = folder.id; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onSavedFolder.subscribe(async () => { + comp.onSavedFolder.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); }); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onDeletedFolder.subscribe(async () => { + comp.onDeletedFolder.pipe(takeUntil(this.destroy$)).subscribe(() => { + // Navigate away if we deleted the colletion we were viewing + if (this.filter.folderId === folder.id) { + this.router.navigate([], { + queryParams: { folderId: null }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } modal.close(); }); } @@ -240,8 +494,7 @@ export class VaultComponent implements OnInit, OnDestroy { }; filterSearchText(searchText: string) { - this.vaultItemsComponent.searchText = searchText; - this.vaultItemsComponent.search(200); + this.searchText$.next(searchText); } async editCipherAttachments(cipher: CipherView) { @@ -265,19 +518,21 @@ export class VaultComponent implements OnInit, OnDestroy { this.attachmentsModalRef, (comp) => { comp.cipherId = cipher.id; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - comp.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true)); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - comp.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true)); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - comp.onReuploadedAttachment.subscribe(() => (madeAttachmentChanges = true)); + comp.onUploadedAttachment + .pipe(takeUntil(this.destroy$)) + .subscribe(() => (madeAttachmentChanges = true)); + comp.onDeletedAttachment + .pipe(takeUntil(this.destroy$)) + .subscribe(() => (madeAttachmentChanges = true)); + comp.onReuploadedAttachment + .pipe(takeUntil(this.destroy$)) + .subscribe(() => (madeAttachmentChanges = true)); } ); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - modal.onClosed.subscribe(async () => { + modal.onClosed.pipe(takeUntil(this.destroy$)).subscribe(() => { if (madeAttachmentChanges) { - await this.vaultItemsComponent.refresh(); + this.refresh(); } madeAttachmentChanges = false; }); @@ -289,10 +544,9 @@ export class VaultComponent implements OnInit, OnDestroy { this.shareModalRef, (comp) => { comp.cipherId = cipher.id; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onSharedCipher.subscribe(async () => { + comp.onSharedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); - await this.vaultItemsComponent.refresh(); + this.refresh(); }); } ); @@ -304,10 +558,9 @@ export class VaultComponent implements OnInit, OnDestroy { this.collectionsModalRef, (comp) => { comp.cipherId = cipher.id; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onSavedCollections.subscribe(async () => { + comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); - await this.vaultItemsComponent.refresh(); + this.refresh(); }); } ); @@ -351,20 +604,17 @@ export class VaultComponent implements OnInit, OnDestroy { this.cipherAddEditModalRef, (comp) => { comp.cipherId = id; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onSavedCipher.subscribe(async () => { + comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); - await this.vaultItemsComponent.refresh(); + this.refresh(); }); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onDeletedCipher.subscribe(async () => { + comp.onDeletedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); - await this.vaultItemsComponent.refresh(); + this.refresh(); }); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onRestoredCipher.subscribe(async () => { + comp.onRestoredCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); - await this.vaultItemsComponent.refresh(); + this.refresh(); }); } ); @@ -381,6 +631,216 @@ export class VaultComponent implements OnInit, OnDestroy { component.cloneMode = true; } + async restore(c: CipherView): Promise { + if (!(await this.repromptCipher([c]))) { + return; + } + + if (!c.isDeleted) { + return; + } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("restoreItemConfirmation"), + this.i18nService.t("restoreItem"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; + } + + try { + await this.cipherService.restoreWithServer(c.id); + this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); + this.refresh(); + } catch (e) { + this.logService.error(e); + } + } + + async bulkRestore(ciphers: CipherView[]) { + if (!(await this.repromptCipher(ciphers))) { + return; + } + + const selectedCipherIds = ciphers.map((cipher) => cipher.id); + if (selectedCipherIds.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nothingSelected") + ); + return; + } + + const dialog = openBulkRestoreDialog(this.dialogService, { + data: { cipherIds: selectedCipherIds }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === BulkRestoreDialogResult.Restored) { + this.refresh(); + } + } + + async deleteCipher(c: CipherView): Promise { + if (!(await this.repromptCipher([c]))) { + return; + } + + const permanent = c.isDeleted; + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t( + permanent ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation" + ), + this.i18nService.t(permanent ? "permanentlyDeleteItem" : "deleteItem"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; + } + + try { + await this.deleteCipherWithServer(c.id, permanent); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem") + ); + this.refresh(); + } catch (e) { + this.logService.error(e); + } + } + + async bulkDelete(ciphers: CipherView[]) { + if (!(await this.repromptCipher(ciphers))) { + return; + } + + const selectedIds = ciphers.map((cipher) => cipher.id); + if (selectedIds.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nothingSelected") + ); + return; + } + const dialog = openBulkDeleteDialog(this.dialogService, { + data: { permanent: this.filter.type === "trash", cipherIds: selectedIds }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === BulkDeleteDialogResult.Deleted) { + this.refresh(); + } + } + + async bulkMove(ciphers: CipherView[]) { + if (!(await this.repromptCipher(ciphers))) { + return; + } + + const selectedCipherIds = ciphers.map((cipher) => cipher.id); + if (selectedCipherIds.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nothingSelected") + ); + return; + } + + const dialog = openBulkMoveDialog(this.dialogService, { + data: { cipherIds: selectedCipherIds }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === BulkMoveDialogResult.Moved) { + this.refresh(); + } + } + + async copy(cipher: CipherView, field: "username" | "password" | "totp") { + let aType; + let value; + let typeI18nKey; + + if (field === "username") { + aType = "Username"; + value = cipher.login.username; + typeI18nKey = "username"; + } else if (field === "password") { + aType = "Password"; + value = cipher.login.password; + typeI18nKey = "password"; + } else if (field === "totp") { + aType = "TOTP"; + value = await this.totpService.getCode(cipher.login.totp); + typeI18nKey = "verificationCodeTotp"; + } else { + this.platformUtilsService.showToast("info", null, this.i18nService.t("unexpectedError")); + return; + } + + if ( + this.passwordRepromptService.protectedFields().includes(aType) && + !(await this.repromptCipher([cipher])) + ) { + return; + } + + if (!cipher.viewPassword) { + return; + } + + this.platformUtilsService.copyToClipboard(value, { window: window }); + this.platformUtilsService.showToast( + "info", + null, + this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)) + ); + + if (field === "password" || field === "totp") { + this.eventCollectionService.collect( + EventType.Cipher_ClientToggledHiddenFieldVisible, + cipher.id + ); + } + } + + async bulkShare(ciphers: CipherView[]) { + if (!(await this.repromptCipher(ciphers))) { + return; + } + + if (ciphers.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nothingSelected") + ); + return; + } + + const dialog = openBulkShareDialog(this.dialogService, { data: { ciphers } }); + + const result = await lastValueFrom(dialog.closed); + if (result === BulkShareDialogResult.Shared) { + this.refresh(); + } + } + + protected deleteCipherWithServer(id: string, permanent: boolean) { + return permanent + ? this.cipherService.deleteWithServer(id) + : this.cipherService.softDeleteWithServer(id); + } + async updateKey() { await this.modalService.openViewRef(UpdateKeyComponent, this.updateKeyModalRef); } @@ -391,6 +851,16 @@ export class VaultComponent implements OnInit, OnDestroy { return kdfType === KdfType.PBKDF2_SHA256 && kdfOptions.iterations < DEFAULT_PBKDF2_ITERATIONS; } + protected async repromptCipher(ciphers: CipherView[]) { + const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None); + + return notProtected || (await this.passwordRepromptService.showPasswordPrompt()); + } + + private refresh() { + this.refresh$.next(); + } + private go(queryParams: any = null) { if (queryParams == null) { queryParams = { diff --git a/apps/web/src/app/vault/individual-vault/vault.module.ts b/apps/web/src/app/vault/individual-vault/vault.module.ts index 97ae44a4617..5e3cc67bc06 100644 --- a/apps/web/src/app/vault/individual-vault/vault.module.ts +++ b/apps/web/src/app/vault/individual-vault/vault.module.ts @@ -3,6 +3,7 @@ import { NgModule } from "@angular/core"; import { BreadcrumbsModule } from "@bitwarden/components"; import { LooseComponentsModule, SharedModule } from "../../shared"; +import { VaultItemsModule } from "../components/vault-items/vault-items.module"; import { CollectionBadgeModule } from "../org-vault/collection-badge/collection-badge.module"; import { GroupBadgeModule } from "../org-vault/group-badge/group-badge.module"; @@ -11,7 +12,6 @@ import { OrganizationBadgeModule } from "./organization-badge/organization-badge import { PipesModule } from "./pipes/pipes.module"; import { VaultFilterModule } from "./vault-filter/vault-filter.module"; import { VaultHeaderComponent } from "./vault-header/vault-header.component"; -import { VaultItemsComponent } from "./vault-items.component"; import { VaultRoutingModule } from "./vault-routing.module"; import { VaultComponent } from "./vault.component"; @@ -27,8 +27,9 @@ import { VaultComponent } from "./vault.component"; LooseComponentsModule, BulkDialogsModule, BreadcrumbsModule, + VaultItemsModule, ], - declarations: [VaultComponent, VaultItemsComponent, VaultHeaderComponent], + declarations: [VaultComponent, VaultHeaderComponent], exports: [VaultComponent], }) export class VaultModule {} diff --git a/apps/web/src/app/vault/org-vault/collection-badge/collection-badge.module.ts b/apps/web/src/app/vault/org-vault/collection-badge/collection-badge.module.ts index 6e29b016a5b..44c27e57c8d 100644 --- a/apps/web/src/app/vault/org-vault/collection-badge/collection-badge.module.ts +++ b/apps/web/src/app/vault/org-vault/collection-badge/collection-badge.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { SharedModule } from "../../../shared"; +import { SharedModule } from "../../../shared/shared.module"; import { PipesModule } from "../../individual-vault/pipes/pipes.module"; import { CollectionNameBadgeComponent } from "./collection-name.badge.component"; diff --git a/apps/web/src/app/vault/org-vault/group-badge/group-badge.module.ts b/apps/web/src/app/vault/org-vault/group-badge/group-badge.module.ts index 5839e7e17b4..26ce689ed86 100644 --- a/apps/web/src/app/vault/org-vault/group-badge/group-badge.module.ts +++ b/apps/web/src/app/vault/org-vault/group-badge/group-badge.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { SharedModule } from "../../../shared"; +import { SharedModule } from "../../../shared/shared.module"; import { PipesModule } from "../../individual-vault/pipes/pipes.module"; import { GroupNameBadgeComponent } from "./group-name-badge.component"; diff --git a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts index 7456fbdd5e3..4918ef82bfb 100644 --- a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts +++ b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts @@ -1,21 +1,18 @@ import { Injectable, OnDestroy } from "@angular/core"; -import { filter, map, Observable, ReplaySubject, Subject, switchMap, takeUntil } from "rxjs"; +import { map, Observable, ReplaySubject, Subject } from "rxjs"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { StateService } from "@bitwarden/common/abstractions/state.service"; -import { CollectionService } from "@bitwarden/common/admin-console/abstractions/collection.service"; -import { - canAccessVaultTab, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { CollectionAdminView } from "../../../admin-console/organizations/core"; -import { CollectionAdminService } from "../../../admin-console/organizations/core/services/collection-admin.service"; +import { + CollectionAdminService, + CollectionAdminView, +} from "../../../admin-console/organizations/core"; +import { StateService } from "../../../core"; import { VaultFilterService as BaseVaultFilterService } from "../../individual-vault/vault-filter/services/vault-filter.service"; import { CollectionFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.type"; @@ -35,7 +32,6 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest organizationService: OrganizationService, folderService: FolderService, cipherService: CipherService, - collectionService: CollectionService, policyService: PolicyService, i18nService: I18nService, protected collectionAdminService: CollectionAdminService @@ -45,42 +41,13 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest organizationService, folderService, cipherService, - collectionService, policyService, i18nService ); - this.loadSubscriptions(); } - protected loadSubscriptions() { - this._organizationFilter - .pipe( - filter((org) => org != null), - switchMap((org) => { - return this.loadCollections(org); - }), - takeUntil(this.destroy$) - ) - .subscribe((collections) => { - this._collections.next(collections); - }); - } - - async reloadCollections() { - this._collections.next(await this.loadCollections(this._organizationFilter.getValue())); - } - - protected async loadCollections(org: Organization): Promise { - let collections: CollectionAdminView[] = []; - if (canAccessVaultTab(org)) { - collections = await this.collectionAdminService.getAll(org.id); - - const noneCollection = new CollectionAdminView(); - noneCollection.name = this.i18nService.t("unassigned"); - noneCollection.organizationId = org.id; - collections.push(noneCollection); - } - return collections; + async reloadCollections(collections: CollectionAdminView[]) { + this._collections.next(collections); } ngOnDestroy() { diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html index 8648639a853..7855acec388 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html @@ -1,29 +1,33 @@
- + - - - {{ activeOrganizationId | orgNameFromId : (organizations$ | async) }} - {{ "vault" | i18n | lowercase }} - - {{ collection.node.name }} + {{ organization.name }} {{ "vault" | i18n | lowercase }} + + + {{ collection.name }} + +

{{ title }} - + - - - - {{ "loading" | i18n }} - + + + {{ "loading" | i18n }}

-
+
{{ trashCleanupWarning }} - - + +
+ +

{{ "noPermissionToViewAllCollectionItems" | i18n }}

+
+
+ +

{{ "noItemsInList" | i18n }}

+ +
+
+ + {{ "loading" | i18n }} +
+ + + +
- - - - diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index b207d1f38e5..7a8bdb7c70a 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -8,35 +8,85 @@ import { ViewContainerRef, } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { combineLatest, firstValueFrom, Subject } from "rxjs"; -import { first, switchMap, takeUntil } from "rxjs/operators"; +import { BehaviorSubject, combineLatest, firstValueFrom, lastValueFrom, Subject } from "rxjs"; +import { + concatMap, + debounceTime, + distinctUntilChanged, + filter, + first, + map, + shareReplay, + switchMap, + takeUntil, + tap, +} from "rxjs/operators"; +import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { TotpService } from "@bitwarden/common/abstractions/totp.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view"; +import { EventType } from "@bitwarden/common/enums"; +import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils"; +import { Utils } from "@bitwarden/common/misc/utils"; +import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { PasswordRepromptService } from "@bitwarden/common/vault/abstractions/password-reprompt.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, Icons } from "@bitwarden/components"; +import { + CollectionAdminService, + CollectionAdminView, + GroupService, + GroupView, +} from "../../admin-console/organizations/core"; import { EntityEventsComponent } from "../../admin-console/organizations/manage/entity-events.component"; +import { + CollectionDialogResult, + CollectionDialogTabType, + openCollectionDialog, +} from "../../admin-console/organizations/shared"; import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model"; +import { VaultItemEvent } from "../components/vault-items/vault-item-event"; +import { + BulkDeleteDialogResult, + openBulkDeleteDialog, +} from "../individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component"; +import { + BulkRestoreDialogResult, + openBulkRestoreDialog, +} from "../individual-vault/bulk-action-dialogs/bulk-restore-dialog/bulk-restore-dialog.component"; import { RoutedVaultFilterBridgeService } from "../individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; import { RoutedVaultFilterService } from "../individual-vault/vault-filter/services/routed-vault-filter.service"; +import { createFilterFunction } from "../individual-vault/vault-filter/shared/models/filter-function"; +import { + All, + RoutedVaultFilterModel, + Unassigned, +} from "../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; +import { getNestedCollectionTree } from "../utils/collection-utils"; import { AddEditComponent } from "./add-edit.component"; import { AttachmentsComponent } from "./attachments.component"; import { CollectionsComponent } from "./collections.component"; import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; -import { VaultItemsComponent } from "./vault-items.component"; const BroadcasterSubscriptionId = "OrgVaultComponent"; +const SearchTextDebounceInterval = 200; @Component({ selector: "app-org-vault", @@ -44,21 +94,38 @@ const BroadcasterSubscriptionId = "OrgVaultComponent"; providers: [RoutedVaultFilterService, RoutedVaultFilterBridgeService], }) export class VaultComponent implements OnInit, OnDestroy { + protected Unassigned = Unassigned; + @ViewChild("vaultFilter", { static: true }) vaultFilterComponent: VaultFilterComponent; - @ViewChild(VaultItemsComponent, { static: true }) vaultItemsComponent: VaultItemsComponent; @ViewChild("attachments", { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef; @ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef; - @ViewChild("collections", { read: ViewContainerRef, static: true }) + @ViewChild("collectionsModal", { read: ViewContainerRef, static: true }) collectionsModalRef: ViewContainerRef; @ViewChild("eventsTemplate", { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef; - organization: Organization; trashCleanupWarning: string = null; activeFilter: VaultFilter = new VaultFilter(); + + protected noItemIcon = Icons.Search; + protected performingInitialLoad = true; + protected refreshing = false; + protected processingEvent = false; + protected filter: RoutedVaultFilterModel = {}; + protected organization: Organization; + protected allCollections: CollectionAdminView[]; + protected allGroups: GroupView[]; + protected ciphers: CipherView[]; + protected collections: CollectionAdminView[]; + protected selectedCollection: TreeNode | undefined; + protected isEmpty: boolean; + protected showMissingCollectionPermissionMessage: boolean; + + private refresh$ = new BehaviorSubject(null); + private searchText$ = new Subject(); private destroy$ = new Subject(); constructor( @@ -66,6 +133,7 @@ export class VaultComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, protected vaultFilterService: VaultFilterService, private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService, + private routedVaultFilterService: RoutedVaultFilterService, private router: Router, private changeDetectorRef: ChangeDetectorRef, private syncService: SyncService, @@ -77,7 +145,15 @@ export class VaultComponent implements OnInit, OnDestroy { private ngZone: NgZone, private platformUtilsService: PlatformUtilsService, private cipherService: CipherService, - private passwordRepromptService: PasswordRepromptService + private passwordRepromptService: PasswordRepromptService, + private collectionAdminService: CollectionAdminService, + private searchService: SearchService, + private searchPipe: SearchPipe, + private groupService: GroupService, + private logService: LogService, + private eventCollectionService: EventCollectionService, + private totpService: TotpService, + private apiService: ApiService ) {} async ngOnInit() { @@ -87,25 +163,203 @@ export class VaultComponent implements OnInit, OnDestroy { : "trashCleanupWarning" ); - this.route.parent.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { - this.organization = this.organizationService.get(params.organizationId); + const filter$ = this.routedVaultFilterService.filter$; + const organizationId$ = filter$.pipe( + map((filter) => filter.organizationId), + filter((filter) => filter !== undefined), + distinctUntilChanged() + ); + + const organization$ = organizationId$.pipe( + switchMap((organizationId) => this.organizationService.get$(organizationId)), + takeUntil(this.destroy$), + shareReplay({ refCount: false, bufferSize: 1 }) + ); + + const firstSetup$ = combineLatest([organization$, this.route.queryParams]).pipe( + first(), + switchMap(async ([organization]) => { + this.organization = organization; + + if (!organization.canUseAdminCollections) { + await this.syncService.fullSync(false); + } + + return undefined; + }), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case "syncCompleted": + if (message.successfully) { + this.refresh(); + this.changeDetectorRef.detectChanges(); + } + break; + } + }); }); - this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => { - this.vaultItemsComponent.searchText = this.vaultFilterComponent.searchText = qParams.search; - }); + this.routedVaultFilterBridgeService.activeFilter$ + .pipe(takeUntil(this.destroy$)) + .subscribe((activeFilter) => { + this.activeFilter = activeFilter; + }); - // verifies that the organization has been set - combineLatest([this.route.queryParams, this.route.parent.params]) + this.searchText$ + .pipe(debounceTime(SearchTextDebounceInterval), takeUntil(this.destroy$)) + .subscribe((searchText) => + this.router.navigate([], { + queryParams: { search: Utils.isNullOrEmpty(searchText) ? null : searchText }, + queryParamsHandling: "merge", + replaceUrl: true, + }) + ); + + const querySearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search)); + + const allCollectionsWithoutUnassigned$ = organizationId$.pipe( + switchMap((orgId) => this.collectionAdminService.getAll(orgId)), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + const allCollections$ = combineLatest([organizationId$, allCollectionsWithoutUnassigned$]).pipe( + map(([organizationId, allCollections]) => { + const noneCollection = new CollectionAdminView(); + noneCollection.name = this.i18nService.t("unassigned"); + noneCollection.id = Unassigned; + noneCollection.organizationId = organizationId; + return allCollections.concat(noneCollection); + }) + ); + + const allGroups$ = organizationId$.pipe( + switchMap((organizationId) => this.groupService.getAll(organizationId)), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + const allCiphers$ = organization$.pipe( + concatMap(async (organization) => { + let ciphers; + if (organization.canEditAnyCollection) { + ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id); + } else { + ciphers = (await this.cipherService.getAllDecrypted()).filter( + (c) => c.organizationId === organization.id + ); + } + await this.searchService.indexCiphers(ciphers, organization.id); + return ciphers; + }) + ); + + const ciphers$ = combineLatest([allCiphers$, filter$, querySearchText$]).pipe( + filter(([ciphers, filter]) => ciphers != undefined && filter != undefined), + concatMap(async ([ciphers, filter, searchText]) => { + if (filter.collectionId === undefined && filter.type === undefined) { + return []; + } + + const filterFunction = createFilterFunction(filter); + + if (this.searchService.isSearchable(searchText)) { + return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers); + } + + return ciphers.filter(filterFunction); + }), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + const nestedCollections$ = allCollections$.pipe( + map((collections) => getNestedCollectionTree(collections)), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + const collections$ = combineLatest([nestedCollections$, filter$, querySearchText$]).pipe( + filter(([collections, filter]) => collections != undefined && filter != undefined), + map(([collections, filter, searchText]) => { + if ( + filter.collectionId === Unassigned || + (filter.collectionId === undefined && filter.type !== undefined) + ) { + return []; + } + + let collectionsToReturn = []; + if (filter.collectionId === undefined || filter.collectionId === All) { + collectionsToReturn = collections.map((c) => c.node); + } else { + const selectedCollection = ServiceUtils.getTreeNodeObjectFromList( + collections, + filter.collectionId + ); + collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; + } + + if (this.searchService.isSearchable(searchText)) { + collectionsToReturn = this.searchPipe.transform( + collectionsToReturn, + searchText, + (collection) => collection.name, + (collection) => collection.id + ); + } + + return collectionsToReturn; + }), + takeUntil(this.destroy$), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + const selectedCollection$ = combineLatest([nestedCollections$, filter$]).pipe( + filter(([collections, filter]) => collections != undefined && filter != undefined), + map(([collections, filter]) => { + if ( + filter.collectionId === undefined || + filter.collectionId === All || + filter.collectionId === Unassigned + ) { + return undefined; + } + + return ServiceUtils.getTreeNodeObjectFromList(collections, filter.collectionId); + }), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + const showMissingCollectionPermissionMessage$ = combineLatest([ + filter$, + selectedCollection$, + organization$, + ]).pipe( + map(([filter, collection, organization]) => { + return ( + // Filtering by unassigned, show message if not admin + (filter.collectionId === Unassigned && !organization.canUseAdminCollections) || + // Filtering by a collection, so show message if user is not assigned + (collection != undefined && + !collection.node.assigned && + !organization.canUseAdminCollections) + ); + }), + shareReplay({ refCount: true, bufferSize: 1 }) + ); + + firstSetup$ .pipe( - switchMap(async ([qParams]) => { + switchMap(() => combineLatest([this.route.queryParams, organization$])), + switchMap(async ([qParams, organization]) => { const cipherId = getCipherIdFromParams(qParams); if (!cipherId) { return; } if ( // Handle users with implicit collection access since they use the admin endpoint - this.organization.canUseAdminCollections || + organization.canUseAdminCollections || (await this.cipherService.get(cipherId)) != null ) { this.editCipherId(cipherId); @@ -125,30 +379,58 @@ export class VaultComponent implements OnInit, OnDestroy { ) .subscribe(); - if (!this.organization.canUseAdminCollections) { - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case "syncCompleted": - if (message.successfully) { - await Promise.all([ - this.vaultFilterService.reloadCollections(), - this.vaultItemsComponent.refresh(), - ]); - this.changeDetectorRef.detectChanges(); - } - break; - } - }); - }); - await this.syncService.fullSync(false); - } + firstSetup$ + .pipe( + switchMap(() => this.refresh$), + tap(() => (this.refreshing = true)), + switchMap(() => + combineLatest([ + organization$, + filter$, + allCollections$, + allGroups$, + ciphers$, + collections$, + selectedCollection$, + showMissingCollectionPermissionMessage$, + ]) + ), + takeUntil(this.destroy$) + ) + .subscribe( + ([ + organization, + filter, + allCollections, + allGroups, + ciphers, + collections, + selectedCollection, + showMissingCollectionPermissionMessage, + ]) => { + this.organization = organization; + this.filter = filter; + this.allCollections = allCollections; + this.allGroups = allGroups; + this.ciphers = ciphers; + this.collections = collections; + this.selectedCollection = selectedCollection; + this.showMissingCollectionPermissionMessage = showMissingCollectionPermissionMessage; - this.routedVaultFilterBridgeService.activeFilter$ - .pipe(takeUntil(this.destroy$)) - .subscribe((activeFilter) => { - this.activeFilter = activeFilter; - }); + this.isEmpty = collections?.length === 0 && ciphers?.length === 0; + + // This is a temporary fix to avoid double fetching collections. + // TODO: Remove when implementing new VVR menu + this.vaultFilterService.reloadCollections(allCollections); + + this.refreshing = false; + this.performingInitialLoad = false; + } + ); + } + + get loading() { + return this.refreshing || this.processingEvent; } ngOnDestroy() { @@ -157,15 +439,50 @@ export class VaultComponent implements OnInit, OnDestroy { this.destroy$.complete(); } - async refreshItems() { - this.vaultItemsComponent.actionPromise = this.vaultItemsComponent.refresh(); - await this.vaultItemsComponent.actionPromise; - this.vaultItemsComponent.actionPromise = null; + async onVaultItemsEvent(event: VaultItemEvent) { + this.processingEvent = true; + + try { + if (event.type === "viewAttachments") { + await this.editCipherAttachments(event.item); + } else if (event.type === "viewCollections") { + await this.editCipherCollections(event.item); + } else if (event.type === "clone") { + await this.cloneCipher(event.item); + } else if (event.type === "restore") { + if (event.items.length === 1) { + await this.restore(event.items[0]); + } else { + await this.bulkRestore(event.items); + } + } else if (event.type === "delete") { + const ciphers = event.items.filter((i) => i.collection === undefined).map((i) => i.cipher); + const collections = event.items + .filter((i) => i.cipher === undefined) + .map((i) => i.collection); + if (ciphers.length === 1 && collections.length === 0) { + await this.deleteCipher(ciphers[0]); + } else if (ciphers.length === 0 && collections.length === 1) { + await this.deleteCollection(collections[0]); + } else { + await this.bulkDelete(ciphers, collections, this.organization); + } + } else if (event.type === "copyField") { + await this.copy(event.item, event.field); + } else if (event.type === "edit") { + await this.editCollection(event.item, CollectionDialogTabType.Info); + } else if (event.type === "viewAccess") { + await this.editCollection(event.item, CollectionDialogTabType.Access); + } else if (event.type === "viewEvents") { + await this.viewEvents(event.item); + } + } finally { + this.processingEvent = false; + } } filterSearchText(searchText: string) { - this.vaultItemsComponent.searchText = searchText; - this.vaultItemsComponent.search(200); + this.searchText$.next(searchText); } async editCipherAttachments(cipher: CipherView) { @@ -182,17 +499,18 @@ export class VaultComponent implements OnInit, OnDestroy { (comp) => { comp.organization = this.organization; comp.cipherId = cipher.id; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - comp.onUploadedAttachment.subscribe(() => (madeAttachmentChanges = true)); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - comp.onDeletedAttachment.subscribe(() => (madeAttachmentChanges = true)); + comp.onUploadedAttachment + .pipe(takeUntil(this.destroy$)) + .subscribe(() => (madeAttachmentChanges = true)); + comp.onDeletedAttachment + .pipe(takeUntil(this.destroy$)) + .subscribe(() => (madeAttachmentChanges = true)); } ); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - modal.onClosed.subscribe(async () => { + modal.onClosed.pipe(takeUntil(this.destroy$)).subscribe(() => { if (madeAttachmentChanges) { - await this.vaultItemsComponent.refresh(); + this.refresh(); } madeAttachmentChanges = false; }); @@ -208,10 +526,9 @@ export class VaultComponent implements OnInit, OnDestroy { comp.collections = currCollections.filter((c) => !c.readOnly && c.id != null); comp.organization = this.organization; comp.cipherId = cipher.id; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onSavedCollections.subscribe(async () => { + comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); - await this.vaultItemsComponent.refresh(); + this.refresh(); }); } ); @@ -258,20 +575,17 @@ export class VaultComponent implements OnInit, OnDestroy { comp.organization = this.organization; comp.organizationId = this.organization.id; comp.cipherId = cipherId; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onSavedCipher.subscribe(async () => { + comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); - await this.vaultItemsComponent.refresh(); + this.refresh(); }); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onDeletedCipher.subscribe(async () => { + comp.onDeletedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); - await this.vaultItemsComponent.refresh(); + this.refresh(); }); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - comp.onRestoredCipher.subscribe(async () => { + comp.onRestoredCipher.pipe(takeUntil(this.destroy$)).subscribe(() => { modal.close(); - await this.vaultItemsComponent.refresh(); + this.refresh(); }); }; @@ -305,6 +619,241 @@ export class VaultComponent implements OnInit, OnDestroy { }); } + async restore(c: CipherView): Promise { + if (!(await this.repromptCipher([c]))) { + return; + } + + if (!c.isDeleted) { + return; + } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("restoreItemConfirmation"), + this.i18nService.t("restoreItem"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; + } + + try { + await this.cipherService.restoreWithServer(c.id); + this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); + this.refresh(); + } catch (e) { + this.logService.error(e); + } + } + + async bulkRestore(ciphers: CipherView[]) { + if (!(await this.repromptCipher(ciphers))) { + return; + } + + const selectedCipherIds = ciphers.map((cipher) => cipher.id); + if (selectedCipherIds.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nothingSelected") + ); + return; + } + + const dialog = openBulkRestoreDialog(this.dialogService, { + data: { cipherIds: selectedCipherIds }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === BulkRestoreDialogResult.Restored) { + this.refresh(); + } + } + + async deleteCipher(c: CipherView): Promise { + if (!(await this.repromptCipher([c]))) { + return; + } + + const permanent = c.isDeleted; + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t( + permanent ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation" + ), + this.i18nService.t(permanent ? "permanentlyDeleteItem" : "deleteItem"), + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return false; + } + + try { + await this.deleteCipherWithServer(c.id, permanent); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t(permanent ? "permanentlyDeletedItem" : "deletedItem") + ); + this.refresh(); + } catch (e) { + this.logService.error(e); + } + } + + async deleteCollection(collection: CollectionView): Promise { + if ( + !this.organization.canDeleteAssignedCollections && + !this.organization.canDeleteAnyCollection + ) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("missingPermissions") + ); + return; + } + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t("deleteCollectionConfirmation"), + collection.name, + this.i18nService.t("yes"), + this.i18nService.t("no"), + "warning" + ); + if (!confirmed) { + return; + } + try { + await this.apiService.deleteCollection(this.organization?.id, collection.id); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("deletedCollectionId", collection.name) + ); + + // Navigate away if we deleted the colletion we were viewing + if (this.selectedCollection?.node.id === collection.id) { + this.router.navigate([], { + queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } + + this.refresh(); + } catch (e) { + this.logService.error(e); + } + } + + async bulkDelete( + ciphers: CipherView[], + collections: CollectionView[], + organization: Organization + ) { + if (!(await this.repromptCipher(ciphers))) { + return; + } + + if (ciphers.length === 0 && collections.length === 0) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + this.i18nService.t("nothingSelected") + ); + return; + } + const dialog = openBulkDeleteDialog(this.dialogService, { + data: { + permanent: this.filter.type === "trash", + cipherIds: ciphers.map((c) => c.id), + collectionIds: collections.map((c) => c.id), + organization, + }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === BulkDeleteDialogResult.Deleted) { + this.refresh(); + } + } + + async copy(cipher: CipherView, field: "username" | "password" | "totp") { + let aType; + let value; + let typeI18nKey; + + if (field === "username") { + aType = "Username"; + value = cipher.login.username; + typeI18nKey = "username"; + } else if (field === "password") { + aType = "Password"; + value = cipher.login.password; + typeI18nKey = "password"; + } else if (field === "totp") { + aType = "TOTP"; + value = await this.totpService.getCode(cipher.login.totp); + typeI18nKey = "verificationCodeTotp"; + } else { + this.platformUtilsService.showToast("info", null, this.i18nService.t("unexpectedError")); + return; + } + + if ( + this.passwordRepromptService.protectedFields().includes(aType) && + !(await this.repromptCipher([cipher])) + ) { + return; + } + + if (!cipher.viewPassword) { + return; + } + + this.platformUtilsService.copyToClipboard(value, { window: window }); + this.platformUtilsService.showToast( + "info", + null, + this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)) + ); + + if (field === "password" || field === "totp") { + this.eventCollectionService.collect( + EventType.Cipher_ClientToggledHiddenFieldVisible, + cipher.id + ); + } + } + + async addCollection(): Promise { + const dialog = openCollectionDialog(this.dialogService, { + data: { + organizationId: this.organization?.id, + parentCollectionId: this.selectedCollection?.node.id, + }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) { + this.refresh(); + } + } + + async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise { + const dialog = openCollectionDialog(this.dialogService, { + data: { collectionId: c?.id, organizationId: this.organization?.id, initialTab: tab }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) { + this.refresh(); + } + } + async viewEvents(cipher: CipherView) { await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => { comp.name = cipher.name; @@ -315,6 +864,22 @@ export class VaultComponent implements OnInit, OnDestroy { }); } + protected deleteCipherWithServer(id: string, permanent: boolean) { + return permanent + ? this.cipherService.deleteWithServer(id) + : this.cipherService.softDeleteWithServer(id); + } + + protected async repromptCipher(ciphers: CipherView[]) { + const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None); + + return notProtected || (await this.passwordRepromptService.showPasswordPrompt()); + } + + private refresh() { + this.refresh$.next(); + } + private go(queryParams: any = null) { if (queryParams == null) { queryParams = { diff --git a/apps/web/src/app/vault/org-vault/vault.module.ts b/apps/web/src/app/vault/org-vault/vault.module.ts index d50498a937e..070898b06c1 100644 --- a/apps/web/src/app/vault/org-vault/vault.module.ts +++ b/apps/web/src/app/vault/org-vault/vault.module.ts @@ -6,12 +6,12 @@ import { LooseComponentsModule } from "../../shared/loose-components.module"; import { SharedModule } from "../../shared/shared.module"; import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; +import { VaultItemsModule } from "../components/vault-items/vault-items.module"; import { CollectionBadgeModule } from "./collection-badge/collection-badge.module"; import { GroupBadgeModule } from "./group-badge/group-badge.module"; import { VaultFilterModule } from "./vault-filter/vault-filter.module"; import { VaultHeaderComponent } from "./vault-header/vault-header.component"; -import { VaultItemsComponent } from "./vault-items.component"; import { VaultRoutingModule } from "./vault-routing.module"; import { VaultComponent } from "./vault.component"; @@ -26,8 +26,9 @@ import { VaultComponent } from "./vault.component"; OrganizationBadgeModule, PipesModule, BreadcrumbsModule, + VaultItemsModule, ], - declarations: [VaultComponent, VaultItemsComponent, VaultHeaderComponent], + declarations: [VaultComponent, VaultHeaderComponent], exports: [VaultComponent], }) export class VaultModule {} diff --git a/apps/web/src/app/vault/utils/collection-utils.ts b/apps/web/src/app/vault/utils/collection-utils.ts new file mode 100644 index 00000000000..8e839398977 --- /dev/null +++ b/apps/web/src/app/vault/utils/collection-utils.ts @@ -0,0 +1,56 @@ +import { + CollectionView, + NestingDelimiter, +} from "@bitwarden/common/admin-console/models/view/collection.view"; +import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils"; +import { TreeNode } from "@bitwarden/common/models/domain/tree-node"; + +import { CollectionAdminView } from "../../admin-console/organizations/core"; + +export function getNestedCollectionTree( + collections: CollectionAdminView[] +): TreeNode[]; +export function getNestedCollectionTree(collections: CollectionView[]): TreeNode[]; +export function getNestedCollectionTree( + collections: (CollectionView | CollectionAdminView)[] +): TreeNode[] { + // Collections need to be cloned because ServiceUtils.nestedTraverse actively + // modifies the names of collections. + // These changes risk affecting collections store in StateService. + const clonedCollections = collections.map(cloneCollection); + + const nodes: TreeNode[] = []; + clonedCollections.forEach((collection) => { + const parts = + collection.name != null + ? collection.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) + : []; + ServiceUtils.nestedTraverse(nodes, 0, parts, collection, null, NestingDelimiter); + }); + return nodes; +} + +function cloneCollection(collection: CollectionView): CollectionView; +function cloneCollection(collection: CollectionAdminView): CollectionAdminView; +function cloneCollection( + collection: CollectionView | CollectionAdminView +): CollectionView | CollectionAdminView { + let cloned; + + if (collection instanceof CollectionAdminView) { + cloned = new CollectionAdminView(); + cloned.groups = [...collection.groups]; + cloned.users = [...collection.users]; + cloned.assigned = collection.assigned; + } else { + cloned = new CollectionView(); + } + + cloned.id = collection.id; + cloned.externalId = collection.externalId; + cloned.hidePasswords = collection.hidePasswords; + cloned.name = collection.name; + cloned.organizationId = collection.organizationId; + cloned.readOnly = collection.readOnly; + return cloned; +} diff --git a/libs/common/src/admin-console/models/view/collection.view.ts b/libs/common/src/admin-console/models/view/collection.view.ts index 24c53055d25..52644035c0c 100644 --- a/libs/common/src/admin-console/models/view/collection.view.ts +++ b/libs/common/src/admin-console/models/view/collection.view.ts @@ -3,6 +3,8 @@ import { View } from "../../../models/view/view"; import { Collection } from "../domain/collection"; import { CollectionAccessDetailsResponse } from "../response/collection.response"; +export const NestingDelimiter = "/"; + export class CollectionView implements View, ITreeNodeObject { id: string = null; organizationId: string = null; diff --git a/libs/common/src/misc/serviceUtils.ts b/libs/common/src/misc/serviceUtils.ts index eb035a77ba3..7fe07f7253c 100644 --- a/libs/common/src/misc/serviceUtils.ts +++ b/libs/common/src/misc/serviceUtils.ts @@ -94,14 +94,14 @@ export class ServiceUtils { /** * Searches an array of tree nodes for a node with a matching `id` - * @param {TreeNode} nodeTree - An array of TreeNode branches that will be searched + * @param {TreeNode} nodeTree - An array of TreeNode branches that will be searched * @param {string} id - The id of the node to be found - * @returns {TreeNode} The node with a matching `id` + * @returns {TreeNode} The node with a matching `id` */ - static getTreeNodeObjectFromList( - nodeTree: TreeNode[], + static getTreeNodeObjectFromList( + nodeTree: TreeNode[], id: string - ): TreeNode { + ): TreeNode { for (let i = 0; i < nodeTree.length; i++) { if (nodeTree[i].node.id === id) { return nodeTree[i]; diff --git a/libs/common/src/misc/utils.ts b/libs/common/src/misc/utils.ts index 5fdcaf1f713..4c30822cc8a 100644 --- a/libs/common/src/misc/utils.ts +++ b/libs/common/src/misc/utils.ts @@ -1,6 +1,7 @@ /* eslint-disable no-useless-escape */ import * as path from "path"; +import { Observable, of, switchMap } from "rxjs"; import { getHostname, parse } from "tldts"; import { Merge } from "type-fest"; @@ -526,6 +527,17 @@ export class Utils { return new Promise((resolve) => setTimeout(resolve, ms)); } + /** + * Generate an observable from a function that returns a promise. + * Similar to the rxjs function {@link from} with one big exception: + * {@link from} will not re-execute the function when observers resubscribe. + * {@link Util.asyncToObservable} will execute `generator` for every + * subscribe, making it ideal if the value ever needs to be refreshed. + * */ + static asyncToObservable(generator: () => Promise): Observable { + return of(undefined).pipe(switchMap(() => generator())); + } + private static isAppleMobile(win: Window) { return ( win.navigator.userAgent.match(/iPhone/i) != null || diff --git a/libs/components/src/breadcrumbs/breadcrumb.component.ts b/libs/components/src/breadcrumbs/breadcrumb.component.ts index 060154b4f69..82803f25153 100644 --- a/libs/components/src/breadcrumbs/breadcrumb.component.ts +++ b/libs/components/src/breadcrumbs/breadcrumb.component.ts @@ -1,4 +1,5 @@ import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core"; +import { QueryParamsHandling } from "@angular/router"; @Component({ selector: "bit-breadcrumb", @@ -14,6 +15,9 @@ export class BreadcrumbComponent { @Input() queryParams?: Record = {}; + @Input() + queryParamsHandling?: QueryParamsHandling; + @Output() click = new EventEmitter(); diff --git a/libs/components/src/breadcrumbs/breadcrumbs.component.html b/libs/components/src/breadcrumbs/breadcrumbs.component.html index b75bf4ac519..502bb0bb8e7 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.component.html +++ b/libs/components/src/breadcrumbs/breadcrumbs.component.html @@ -6,6 +6,7 @@ class="tw-my-2 tw-inline-block" [routerLink]="breadcrumb.route" [queryParams]="breadcrumb.queryParams" + [queryParamsHandling]="breadcrumb.queryParamsHandling" > @@ -43,6 +44,7 @@ linkType="primary" [routerLink]="breadcrumb.route" [queryParams]="breadcrumb.queryParams" + [queryParamsHandling]="breadcrumb.queryParamsHandling" > @@ -64,6 +66,7 @@ class="tw-my-2 tw-inline-block" [routerLink]="breadcrumb.route" [queryParams]="breadcrumb.queryParams" + [queryParamsHandling]="breadcrumb.queryParamsHandling" > diff --git a/libs/components/src/table/table.component.html b/libs/components/src/table/table.component.html index 49201e101e4..b7ae6743101 100644 --- a/libs/components/src/table/table.component.html +++ b/libs/components/src/table/table.component.html @@ -1,6 +1,6 @@ - +
diff --git a/libs/components/src/table/table.component.ts b/libs/components/src/table/table.component.ts index 40a2da70927..9f36d0a70fc 100644 --- a/libs/components/src/table/table.component.ts +++ b/libs/components/src/table/table.component.ts @@ -26,6 +26,7 @@ export class TableBodyDirective { }) export class TableComponent implements OnDestroy, AfterContentChecked { @Input() dataSource: TableDataSource; + @Input() layout: "auto" | "fixed" = "auto"; @ContentChild(TableBodyDirective) templateVariable: TableBodyDirective; @@ -33,6 +34,15 @@ export class TableComponent implements OnDestroy, AfterContentChecked { private _initialized = false; + get tableClass() { + return [ + "tw-w-full", + "tw-leading-normal", + "tw-text-main", + this.layout === "auto" ? "tw-table-auto" : "tw-table-fixed", + ]; + } + ngAfterContentChecked(): void { if (!this._initialized && isDataSource(this.dataSource)) { this._initialized = true; From b3d4d9898e0eb196e872501246c1856ccdcbf993 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Thu, 13 Apr 2023 15:59:31 -0400 Subject: [PATCH 030/125] [SG-1026 / PM-1125] - Document / Improve Form Detection in Notification Bar (#4798) * SG-1026 - Documenting / slight refactoring of notification-bar - WIP * SG-1026 - More documentation WIP * SG-1026 - Continued documentation of notification bar + testing theories for specific sites as part of research to identify areas for possible improvement + added types where appropriate. * SG-1026 - getSubmitButton docs * SG-1026 - Autofill Service tweak - On account creation (ex: talkshoe.com), even if the pageDetails contained a valid form to watch, the loadPasswordFields method parameter for fillNewPassword being false for inputs with autoCompleteType of "new-password" would cause the account creation form to not be watched (null form data returned to notification bar). Setting this to true will help capture more account creations in the above specified scenario. * SG-1026 - Additional documentation / comment clean up * SG-1026 - Remove unused pageDetails array * SG-1026 - These changes address form detection issues for the password change form on talkshoe.com: (1) Update autofill.service getFormsWithPasswordFields(...) method to group autofill.js found password type fields under a single form in a very specific scenario where the most likely case is that it is a password change form with poorly designed mark up in a SPA (2) Notification bar - when listening to a form, we must use both the loginButtonNames and the changePasswordButton names as we don't know what type of form we are listening to (3) Notification bar - on page change, we must empty out the watched forms array to prevent forms w/ the same opId being added to the array on SPA url change (4) Notification bar - getSubmitButton update - If we cannot find a submit button within a form, try going up one level to the parent element and searching again (+ added save to changePasswordButtonNames). (5) Notification bar - when listening to a form with a submit button, we can attach the formOpId to the button so we can only have DOM traversal in one location and retrieve the form off the button later on in the form submission logic. For now, I'm just adding it as a fallback, but it could be the primary approach with more testing. * SG-1026 - On first load of the notification-bar content script, we should start observing the DOM immediately so we properly catch rendered forms instead of waiting for a second. This was especially prevelant on refreshing the password change form page on talkshoe.com. * SG-1026 - Due to the previous, timeout based nature of the calls to collectPageDetailsIfNeeded (now handlePageChange), the mutation observer could get setup late and miss forms loading (ex: refreshing a password change page on talkshoe.com). DOM observation is now setup as fast as possible on page load for SPAs/Non SPAs and on change for SPAs by having the mutation observer itself detect page change and deterministically calling handlePageChange(). However, with these changes, page detail collection still only occurs after a minimum of ~1 second whether or not it was triggered from the mutation observer detecting forms being injected onto the page or the scheduleHandlePageChange running (which has a theoretical maximum time to page detail collection of ~1.999 seconds but this does require the mutation observer to miss the page change in a SPA which shouldn't happen). * SG-1026 - Identified issue with current form retrieval step in autofill service which prevents multi-step account creation forms from being returned to the notification-bar content script from the notification.background.ts script. * SG-1026 - Add logic to formSubmitted to try and successfully process multi-step login form (email then password on https://login.live.com/login.srf) with next button that gets swapped out for a true submit button in order to prompt for saving user credentials if not in Bitwarden. This logic works *sometimes* as the submit button page change often stops the submit button event listeners from being able to fire and send the login to the background script. However, that is a separate issue to be solved, and sometimes is better than never. This type of logic might be useful in solving the multi-step account creation form on https://signup.live.com/signup but that will require additional changes to the autofill service which current intercepts forms without passwords and prevents them from reaching the notification-bar.ts content script. * SG-1026 - Add note explaining the persistence of the content script * SG-1026 - Update stack overflow link to improve clarity. --------- Co-authored-by: Robyn MacCallum --- .../src/autofill/content/notification-bar.ts | 594 +++++++++++++++--- .../src/autofill/models/watched-form.ts | 8 + .../src/autofill/services/autofill.service.ts | 38 +- 3 files changed, 534 insertions(+), 106 deletions(-) create mode 100644 apps/browser/src/autofill/models/watched-form.ts diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index c031929988d..7edc730dece 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -1,15 +1,44 @@ import AddLoginRuntimeMessage from "../../background/models/addLoginRuntimeMessage"; import ChangePasswordRuntimeMessage from "../../background/models/changePasswordRuntimeMessage"; +import AutofillField from "../models/autofill-field"; +import { WatchedForm } from "../models/watched-form"; +import { FormData } from "../services/abstractions/autofill.service"; +interface HTMLElementWithFormOpId extends HTMLElement { + formOpId: string; +} + +/** + * @fileoverview This file contains the code for the Bitwarden Notification Bar content script. + * The notification bar is used to notify logged in users that they can + * save a new login, change a existing password on a password change screen, + * or update an existing login after detecting a different password on login. + * + * Note: content scripts are reloaded on non-SPA page change. + */ + +/* + * Run content script when the DOM is fully loaded + * + * The DOMContentLoaded event fires when the HTML document has been completely parsed, + * and all deferred scripts (