mirror of
https://github.com/bitwarden/browser
synced 2026-02-13 06:54:07 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -2504,6 +2504,14 @@
|
||||
"message": "Send saved",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendFilePopoutDialogText": {
|
||||
"message": "Pop out extension?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendFilePopoutDialogDesc": {
|
||||
"message": "To create a file Send, you need to pop out te extension to a new window.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendLinuxChromiumFileWarning": {
|
||||
"message": "In order to choose a file, open the extension in the sidebar (if possible) or pop out to a new window by clicking this banner."
|
||||
},
|
||||
@@ -2513,6 +2521,9 @@
|
||||
"sendSafariFileWarning": {
|
||||
"message": "In order to choose a file using Safari, pop out to a new window by clicking this banner."
|
||||
},
|
||||
"popOut": {
|
||||
"message": "Pop out"
|
||||
},
|
||||
"sendFileCalloutHeader": {
|
||||
"message": "Before you start"
|
||||
},
|
||||
@@ -4485,5 +4496,8 @@
|
||||
},
|
||||
"noEditPermissions": {
|
||||
"message": "You don't have permission to edit this item"
|
||||
},
|
||||
"authenticating": {
|
||||
"message": "Authenticating"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { generateDomainMatchPatterns, isInvalidResponseStatusCode } from "../utils";
|
||||
|
||||
import {
|
||||
ActiveFormSubmissionRequests,
|
||||
@@ -109,35 +110,11 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
*/
|
||||
private getSenderUrlMatchPatterns(sender: chrome.runtime.MessageSender) {
|
||||
return new Set([
|
||||
...this.generateMatchPatterns(sender.url),
|
||||
...this.generateMatchPatterns(sender.tab.url),
|
||||
...generateDomainMatchPatterns(sender.url),
|
||||
...generateDomainMatchPatterns(sender.tab.url),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the origin and subdomain match patterns for the URL.
|
||||
*
|
||||
* @param url - The URL of the tab
|
||||
*/
|
||||
private generateMatchPatterns(url: string): string[] {
|
||||
try {
|
||||
if (!url.startsWith("http")) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
|
||||
const originMatchPattern = `${new URL(url).origin}/*`;
|
||||
|
||||
const parsedUrl = new URL(url);
|
||||
const splitHost = parsedUrl.hostname.split(".");
|
||||
const domain = splitHost.slice(-2).join(".");
|
||||
const subDomainMatchPattern = `${parsedUrl.protocol}//*.${domain}/*`;
|
||||
|
||||
return [originMatchPattern, subDomainMatchPattern];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the login form data that was modified by the user in the content script. This data is
|
||||
* used to trigger the add login or change password notification when the form is submitted.
|
||||
@@ -329,7 +306,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
private handleOnCompletedRequestEvent = async (details: chrome.webRequest.WebResponseDetails) => {
|
||||
if (
|
||||
this.requestHostIsInvalid(details) ||
|
||||
this.isInvalidStatusCode(details.statusCode) ||
|
||||
isInvalidResponseStatusCode(details.statusCode) ||
|
||||
!this.activeFormSubmissionRequests.has(details.requestId)
|
||||
) {
|
||||
return;
|
||||
@@ -472,16 +449,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
this.setupWebRequestsListeners();
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the status code of the web response is invalid. An invalid status code is
|
||||
* any status code that is not in the 200-299 range.
|
||||
*
|
||||
* @param statusCode - The status code of the web response
|
||||
*/
|
||||
private isInvalidStatusCode = (statusCode: number) => {
|
||||
return statusCode < 200 || statusCode >= 300;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the host of the web request is invalid. An invalid host is any host that does not
|
||||
* start with "http" or a tab id that is less than 0.
|
||||
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
triggerPortOnDisconnectEvent,
|
||||
triggerPortOnMessageEvent,
|
||||
triggerWebNavigationOnCommittedEvent,
|
||||
triggerWebRequestOnCompletedEvent,
|
||||
} from "../spec/testing-utils";
|
||||
|
||||
import {
|
||||
@@ -3003,37 +3004,95 @@ describe("OverlayBackground", () => {
|
||||
expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code");
|
||||
});
|
||||
|
||||
it("triggers passkey authentication through mediated conditional UI", async () => {
|
||||
const fido2Credential = mock<Fido2CredentialView>({ credentialId: "credential-id" });
|
||||
const cipher1 = mock<CipherView>({
|
||||
id: "inline-menu-cipher-1",
|
||||
login: {
|
||||
username: "username1",
|
||||
password: "password1",
|
||||
fido2Credentials: [fido2Credential],
|
||||
},
|
||||
});
|
||||
overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher1]]);
|
||||
const pageDetailsForTab = {
|
||||
frameId: sender.frameId,
|
||||
tab: sender.tab,
|
||||
details: pageDetails,
|
||||
};
|
||||
overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([
|
||||
[sender.frameId, pageDetailsForTab],
|
||||
]);
|
||||
autofillService.isPasswordRepromptRequired.mockResolvedValue(false);
|
||||
jest.spyOn(fido2ActiveRequestManager, "getActiveRequest");
|
||||
describe("triggering passkey authentication", () => {
|
||||
let cipher1: CipherView;
|
||||
|
||||
sendPortMessage(listMessageConnectorSpy, {
|
||||
command: "fillAutofillInlineMenuCipher",
|
||||
inlineMenuCipherId: "inline-menu-cipher-1",
|
||||
usePasskey: true,
|
||||
portKey,
|
||||
beforeEach(() => {
|
||||
const fido2Credential = mock<Fido2CredentialView>({ credentialId: "credential-id" });
|
||||
cipher1 = mock<CipherView>({
|
||||
id: "inline-menu-cipher-1",
|
||||
login: {
|
||||
username: "username1",
|
||||
password: "password1",
|
||||
fido2Credentials: [fido2Credential],
|
||||
},
|
||||
});
|
||||
const pageDetailsForTab = {
|
||||
frameId: sender.frameId,
|
||||
tab: sender.tab,
|
||||
details: pageDetails,
|
||||
};
|
||||
overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher1]]);
|
||||
overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([
|
||||
[sender.frameId, pageDetailsForTab],
|
||||
]);
|
||||
autofillService.isPasswordRepromptRequired.mockResolvedValue(false);
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(fido2ActiveRequestManager.getActiveRequest).toHaveBeenCalledWith(sender.tab.id);
|
||||
it("logs an error if the authentication could not complete due to a missing FIDO2 request", async () => {
|
||||
jest.spyOn(logService, "error");
|
||||
|
||||
sendPortMessage(listMessageConnectorSpy, {
|
||||
command: "fillAutofillInlineMenuCipher",
|
||||
inlineMenuCipherId: "inline-menu-cipher-1",
|
||||
usePasskey: true,
|
||||
portKey,
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(logService.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when the FIDO2 request is present", () => {
|
||||
beforeEach(async () => {
|
||||
void fido2ActiveRequestManager.newActiveRequest(
|
||||
sender.tab.id,
|
||||
cipher1.login.fido2Credentials,
|
||||
new AbortController(),
|
||||
);
|
||||
});
|
||||
|
||||
it("aborts all active FIDO2 requests if the subsequent request after the authentication is invalid", async () => {
|
||||
jest.spyOn(fido2ActiveRequestManager, "removeActiveRequest");
|
||||
|
||||
sendPortMessage(listMessageConnectorSpy, {
|
||||
command: "fillAutofillInlineMenuCipher",
|
||||
inlineMenuCipherId: "inline-menu-cipher-1",
|
||||
usePasskey: true,
|
||||
portKey,
|
||||
});
|
||||
await flushPromises();
|
||||
triggerWebRequestOnCompletedEvent(
|
||||
mock<chrome.webRequest.WebResponseCacheDetails>({
|
||||
statusCode: 401,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(fido2ActiveRequestManager.removeActiveRequest).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("triggers a closure of the inline menu if the subsequent request after the authentication is valid", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
await initOverlayElementPorts();
|
||||
sendPortMessage(listMessageConnectorSpy, {
|
||||
command: "fillAutofillInlineMenuCipher",
|
||||
inlineMenuCipherId: "inline-menu-cipher-1",
|
||||
usePasskey: true,
|
||||
portKey,
|
||||
});
|
||||
triggerWebRequestOnCompletedEvent(
|
||||
mock<chrome.webRequest.WebResponseCacheDetails>({
|
||||
statusCode: 200,
|
||||
}),
|
||||
);
|
||||
jest.advanceTimersByTime(3100);
|
||||
|
||||
expect(listPortSpy.postMessage).toHaveBeenCalledWith({
|
||||
command: "triggerDelayedAutofillInlineMenuClosure",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -55,7 +55,11 @@ import {
|
||||
MAX_SUB_FRAME_DEPTH,
|
||||
} from "../enums/autofill-overlay.enum";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
import { generateRandomChars } from "../utils";
|
||||
import {
|
||||
generateDomainMatchPatterns,
|
||||
generateRandomChars,
|
||||
isInvalidResponseStatusCode,
|
||||
} from "../utils";
|
||||
|
||||
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
|
||||
import {
|
||||
@@ -151,7 +155,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
addEditCipherSubmitted: () => this.updateOverlayCiphers(),
|
||||
editedCipher: () => this.updateOverlayCiphers(),
|
||||
deletedCipher: () => this.updateOverlayCiphers(),
|
||||
fido2AbortRequest: ({ sender }) => this.abortFido2ActiveRequest(sender),
|
||||
fido2AbortRequest: ({ sender }) => this.abortFido2ActiveRequest(sender.tab.id),
|
||||
};
|
||||
private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = {
|
||||
triggerDelayedAutofillInlineMenuClosure: () => this.triggerDelayedInlineMenuClosure(),
|
||||
@@ -672,10 +676,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
/**
|
||||
* Aborts an active FIDO2 request for a given tab and updates the inline menu ciphers.
|
||||
*
|
||||
* @param sender - The sender of the message
|
||||
* @param tabId - The id of the tab to abort the request for
|
||||
*/
|
||||
private async abortFido2ActiveRequest(sender: chrome.runtime.MessageSender) {
|
||||
this.fido2ActiveRequestManager.removeActiveRequest(sender.tab.id);
|
||||
private async abortFido2ActiveRequest(tabId: number) {
|
||||
this.fido2ActiveRequestManager.removeActiveRequest(tabId);
|
||||
await this.updateOverlayCiphers(false);
|
||||
}
|
||||
|
||||
@@ -939,11 +943,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
|
||||
if (usePasskey && cipher.login?.hasFido2Credentials) {
|
||||
await this.authenticatePasskeyCredential(
|
||||
sender.tab.id,
|
||||
sender,
|
||||
cipher.login.fido2Credentials[0].credentialId,
|
||||
);
|
||||
this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher);
|
||||
this.closeInlineMenu(sender, { forceCloseInlineMenu: true });
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -969,11 +972,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
/**
|
||||
* Triggers a FIDO2 authentication from the inline menu using the passed credential ID.
|
||||
*
|
||||
* @param tabId - The tab ID to trigger the authentication for
|
||||
* @param sender - The sender of the port message
|
||||
* @param credentialId - The credential ID to authenticate
|
||||
*/
|
||||
async authenticatePasskeyCredential(tabId: number, credentialId: string) {
|
||||
const request = this.fido2ActiveRequestManager.getActiveRequest(tabId);
|
||||
async authenticatePasskeyCredential(sender: chrome.runtime.MessageSender, credentialId: string) {
|
||||
const request = this.fido2ActiveRequestManager.getActiveRequest(sender.tab.id);
|
||||
if (!request) {
|
||||
this.logService.error(
|
||||
"Could not complete passkey autofill due to missing active Fido2 request",
|
||||
@@ -981,9 +984,35 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.webRequest.onCompleted.addListener(this.handlePasskeyAuthenticationOnCompleted, {
|
||||
urls: generateDomainMatchPatterns(sender.tab.url),
|
||||
});
|
||||
request.subject.next({ type: Fido2ActiveRequestEvents.Continue, credentialId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the next web request that occurs after a passkey authentication has been completed.
|
||||
* Ensures that the inline menu closes after the request, and that the FIDO2 request is aborted
|
||||
* if the request is not successful.
|
||||
*
|
||||
* @param details - The web request details
|
||||
*/
|
||||
private handlePasskeyAuthenticationOnCompleted = (
|
||||
details: chrome.webRequest.WebResponseCacheDetails,
|
||||
) => {
|
||||
chrome.webRequest.onCompleted.removeListener(this.handlePasskeyAuthenticationOnCompleted);
|
||||
|
||||
if (isInvalidResponseStatusCode(details.statusCode)) {
|
||||
this.closeInlineMenu({ tab: { id: details.tabId } } as chrome.runtime.MessageSender, {
|
||||
forceCloseInlineMenu: true,
|
||||
});
|
||||
this.abortFido2ActiveRequest(details.tabId).catch((error) => this.logService.error(error));
|
||||
return;
|
||||
}
|
||||
|
||||
globalThis.setTimeout(() => this.triggerDelayedInlineMenuClosure(), 3000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the most recently used cipher at the top of the list of ciphers.
|
||||
*
|
||||
@@ -1587,6 +1616,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
passkeys: this.i18nService.translate("passkeys"),
|
||||
passwords: this.i18nService.translate("passwords"),
|
||||
logInWithPasskey: this.i18nService.translate("logInWithPasskeyAriaLabel"),
|
||||
authenticating: this.i18nService.translate("authenticating"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2131,6 +2131,44 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user fill cipher button event listeners filling a cipher displays an \`Authenticating\` loader when a passkey cipher is filled 1`] = `
|
||||
<div
|
||||
class="inline-menu-list-container theme_light"
|
||||
>
|
||||
<div
|
||||
class="passkey-authenticating-loader"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
clip-path="url(#a)"
|
||||
>
|
||||
<path
|
||||
d="M4.869 15.015a.588.588 0 1 0 0-1.177.588.588 0 0 0 0 1.177ZM8.252 16a.588.588 0 1 0 0-1.176.588.588 0 0 0 0 1.176Zm3.683-.911a.589.589 0 1 0 0-1.177.589.589 0 0 0 0 1.177ZM2.43 12.882a.693.693 0 1 0 0-1.387.693.693 0 0 0 0 1.387ZM1.318 9.738a.82.82 0 1 0 0-1.64.82.82 0 0 0 0 1.64Zm.69-3.578a.968.968 0 1 0 0-1.937.968.968 0 0 0 0 1.937ZM4.81 3.337a1.175 1.175 0 1 0 0-2.35 1.175 1.175 0 0 0 0 2.35Zm4.597-.676a1.33 1.33 0 1 0 0-2.661 1.33 1.33 0 0 0 0 2.66Zm4.543 2.954a1.553 1.553 0 1 0 0-3.105 1.553 1.553 0 0 0 0 3.105Z"
|
||||
fill="#5A6D91"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clippath
|
||||
id="a"
|
||||
>
|
||||
<path
|
||||
d="M0 0h16v16H0z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</clippath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AutofillInlineMenuList initAutofillInlineMenuList the locked inline menu for an unauthenticated user creates the views for the locked inline menu 1`] = `
|
||||
<div
|
||||
class="inline-menu-list-container theme_light"
|
||||
|
||||
@@ -230,21 +230,56 @@ describe("AutofillInlineMenuList", () => {
|
||||
postWindowMessage(createInitAutofillInlineMenuListMessageMock({ portKey }));
|
||||
});
|
||||
|
||||
it("allows the user to fill a cipher on click", () => {
|
||||
const fillCipherButton =
|
||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector(".fill-cipher-button");
|
||||
describe("filling a cipher", () => {
|
||||
it("allows the user to fill a cipher on click", () => {
|
||||
const fillCipherButton =
|
||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector(
|
||||
".fill-cipher-button",
|
||||
);
|
||||
|
||||
fillCipherButton.dispatchEvent(new Event("click"));
|
||||
fillCipherButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{
|
||||
command: "fillAutofillInlineMenuCipher",
|
||||
inlineMenuCipherId: "1",
|
||||
usePasskey: false,
|
||||
portKey,
|
||||
},
|
||||
"*",
|
||||
);
|
||||
expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
|
||||
{
|
||||
command: "fillAutofillInlineMenuCipher",
|
||||
inlineMenuCipherId: "1",
|
||||
usePasskey: false,
|
||||
portKey,
|
||||
},
|
||||
"*",
|
||||
);
|
||||
});
|
||||
|
||||
it("displays an `Authenticating` loader when a passkey cipher is filled", async () => {
|
||||
postWindowMessage(
|
||||
createInitAutofillInlineMenuListMessageMock({
|
||||
ciphers: [
|
||||
createAutofillOverlayCipherDataMock(1, {
|
||||
name: "https://example.com",
|
||||
login: {
|
||||
username: "username1",
|
||||
passkey: {
|
||||
rpName: "https://example.com",
|
||||
userName: "username1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
showPasskeysLabels: true,
|
||||
portKey,
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
const fillCipherButton =
|
||||
autofillInlineMenuList["inlineMenuListContainer"].querySelector(
|
||||
".fill-cipher-button",
|
||||
);
|
||||
|
||||
fillCipherButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it("allows the user to move keyboard focus to the next cipher element on ArrowDown", () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
plusIcon,
|
||||
viewCipherIcon,
|
||||
passkeyIcon,
|
||||
spinnerIcon,
|
||||
} from "../../../../utils/svg-icons";
|
||||
import {
|
||||
AutofillInlineMenuListWindowMessageHandlers,
|
||||
@@ -40,6 +41,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
private passkeysHeadingHeight: number;
|
||||
private lastPasskeysListItemHeight: number;
|
||||
private ciphersListHeight: number;
|
||||
private isPasskeyAuthInProgress = false;
|
||||
private readonly showCiphersPerPage = 6;
|
||||
private readonly headingBorderClass = "inline-menu-list-heading--bordered";
|
||||
private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers =
|
||||
@@ -156,15 +158,14 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
ciphers: InlineMenuCipherData[],
|
||||
showInlineMenuAccountCreation?: boolean,
|
||||
) {
|
||||
if (this.isPasskeyAuthInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ciphers = ciphers;
|
||||
this.currentCipherIndex = 0;
|
||||
this.showInlineMenuAccountCreation = showInlineMenuAccountCreation;
|
||||
if (this.inlineMenuListContainer) {
|
||||
this.inlineMenuListContainer.innerHTML = "";
|
||||
this.inlineMenuListContainer.classList.remove(
|
||||
"inline-menu-list-container--with-new-item-button",
|
||||
);
|
||||
}
|
||||
this.resetInlineMenuContainer();
|
||||
|
||||
if (!ciphers?.length) {
|
||||
this.buildNoResultsInlineMenuList();
|
||||
@@ -191,6 +192,18 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
this.newItemButtonElement.addEventListener(EVENTS.KEYUP, this.handleNewItemButtonKeyUpEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears and resets the inline menu list container.
|
||||
*/
|
||||
private resetInlineMenuContainer() {
|
||||
if (this.inlineMenuListContainer) {
|
||||
this.inlineMenuListContainer.innerHTML = "";
|
||||
this.inlineMenuListContainer.classList.remove(
|
||||
"inline-menu-list-container--with-new-item-button",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline menu view that is presented when no ciphers are found for a given page.
|
||||
* Facilitates the ability to add a new vault item from the inline menu.
|
||||
@@ -330,7 +343,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
this.ciphersList.addEventListener(
|
||||
EVENTS.SCROLL,
|
||||
this.useEventHandlersMemo(
|
||||
throttle(() => this.updatePasskeysHeadingsOnScroll(this.ciphersList.scrollTop), 50),
|
||||
throttle(this.handleThrottledOnScrollEvent, 50),
|
||||
UPDATE_PASSKEYS_HEADINGS_ON_SCROLL,
|
||||
),
|
||||
options,
|
||||
@@ -342,7 +355,10 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
* Handles updating the list of ciphers when the
|
||||
* user scrolls to the bottom of the list.
|
||||
*/
|
||||
private updateCiphersListOnScroll = () => {
|
||||
private updateCiphersListOnScroll = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.cipherListScrollIsDebounced) {
|
||||
return;
|
||||
}
|
||||
@@ -382,6 +398,18 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Throttled handler for updating the passkeys and login headings when the user scrolls the ciphers list.
|
||||
*
|
||||
* @param event - The scroll event.
|
||||
*/
|
||||
private handleThrottledOnScrollEvent = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.updatePasskeysHeadingsOnScroll(this.ciphersList.scrollTop);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the passkeys and login headings when the user scrolls the ciphers list.
|
||||
*
|
||||
@@ -596,16 +624,29 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
private handleFillCipherClickEvent = (cipher: InlineMenuCipherData) => {
|
||||
const usePasskey = !!cipher.login?.passkey;
|
||||
return this.useEventHandlersMemo(
|
||||
() =>
|
||||
this.postMessageToParent({
|
||||
command: "fillAutofillInlineMenuCipher",
|
||||
inlineMenuCipherId: cipher.id,
|
||||
usePasskey,
|
||||
}),
|
||||
() => this.triggerFillCipherClickEvent(cipher, usePasskey),
|
||||
`${cipher.id}-fill-cipher-button-click-handler-${usePasskey ? "passkey" : ""}`,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggers a fill of the currently selected cipher.
|
||||
*
|
||||
* @param cipher - The cipher to fill.
|
||||
* @param usePasskey - Whether the cipher uses a passkey.
|
||||
*/
|
||||
private triggerFillCipherClickEvent = (cipher: InlineMenuCipherData, usePasskey: boolean) => {
|
||||
if (usePasskey) {
|
||||
this.createPasskeyAuthenticatingLoader();
|
||||
}
|
||||
|
||||
this.postMessageToParent({
|
||||
command: "fillAutofillInlineMenuCipher",
|
||||
inlineMenuCipherId: cipher.id,
|
||||
usePasskey,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the keyup event for the fill cipher button. Facilitates
|
||||
* selecting the next/previous cipher item on ArrowDown/ArrowUp. Also
|
||||
@@ -889,6 +930,26 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
|
||||
return cipherDetailsElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an indicator for the user that the passkey is being authenticated.
|
||||
*/
|
||||
private createPasskeyAuthenticatingLoader() {
|
||||
this.isPasskeyAuthInProgress = true;
|
||||
this.resetInlineMenuContainer();
|
||||
|
||||
const passkeyAuthenticatingLoader = globalThis.document.createElement("div");
|
||||
passkeyAuthenticatingLoader.classList.add("passkey-authenticating-loader");
|
||||
passkeyAuthenticatingLoader.textContent = this.getTranslation("authenticating");
|
||||
passkeyAuthenticatingLoader.appendChild(buildSvgDomElement(spinnerIcon));
|
||||
|
||||
this.inlineMenuListContainer.appendChild(passkeyAuthenticatingLoader);
|
||||
|
||||
globalThis.setTimeout(() => {
|
||||
this.isPasskeyAuthInProgress = false;
|
||||
this.postMessageToParent({ command: "checkAutofillInlineMenuButtonFocused" });
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the subtitle text for a given cipher.
|
||||
*
|
||||
|
||||
@@ -15,6 +15,8 @@ body {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: $font-family-sans-serif;
|
||||
font-weight: 400;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("textColor");
|
||||
@@ -23,8 +25,6 @@ body {
|
||||
}
|
||||
|
||||
.inline-menu-list-message {
|
||||
font-family: $font-family-sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.5;
|
||||
width: 100%;
|
||||
@@ -393,3 +393,38 @@ body {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bwi-spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
.passkey-authenticating-loader {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 1rem 0.8rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("passkeysAuthenticating");
|
||||
}
|
||||
|
||||
svg {
|
||||
animation: bwi-spin 2s infinite linear;
|
||||
margin-left: 1rem;
|
||||
|
||||
path {
|
||||
@include themify($themes) {
|
||||
fill: themed("passkeysAuthenticating") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3017,9 +3017,11 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
const tabs = await BrowserApi.tabsQuery({});
|
||||
for (let index = 0; index < tabs.length; index++) {
|
||||
const tab = tabs[index];
|
||||
if (tab.url?.startsWith("http")) {
|
||||
if (tab?.id && tab.url?.startsWith("http")) {
|
||||
const frames = await BrowserApi.getAllFrameDetails(tab.id);
|
||||
frames.forEach((frame) => this.injectAutofillScripts(tab, frame.frameId, false));
|
||||
if (frames) {
|
||||
frames.forEach((frame) => this.injectAutofillScripts(tab, frame.frameId, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ $border-color: #ced4dc;
|
||||
$border-color-dark: #ddd;
|
||||
$border-radius: 3px;
|
||||
$focus-outline-color: #1252a3;
|
||||
$muted-blue: #5a6d91;
|
||||
|
||||
$brand-primary: #175ddc;
|
||||
|
||||
@@ -45,6 +46,7 @@ $themes: (
|
||||
focusOutlineColor: $focus-outline-color,
|
||||
successColor: $success-color-light,
|
||||
errorColor: $error-color-light,
|
||||
passkeysAuthenticating: $muted-blue,
|
||||
),
|
||||
dark: (
|
||||
textColor: #ffffff,
|
||||
@@ -60,6 +62,7 @@ $themes: (
|
||||
focusOutlineColor: lighten($focus-outline-color, 25%),
|
||||
successColor: $success-color-dark,
|
||||
errorColor: $error-color-dark,
|
||||
passkeysAuthenticating: #bac0ce,
|
||||
),
|
||||
nord: (
|
||||
textColor: $nord5,
|
||||
@@ -74,6 +77,7 @@ $themes: (
|
||||
borderColor: $nord0,
|
||||
focusOutlineColor: lighten($focus-outline-color, 25%),
|
||||
successColor: $success-color-dark,
|
||||
passkeysAuthenticating: $nord4,
|
||||
),
|
||||
solarizedDark: (
|
||||
textColor: $solarizedDarkBase2,
|
||||
@@ -89,6 +93,7 @@ $themes: (
|
||||
borderColor: $solarizedDarkBase2,
|
||||
focusOutlineColor: lighten($focus-outline-color, 15%),
|
||||
successColor: $success-color-dark,
|
||||
passkeysAuthenticating: $solarizedDarkBase2,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -426,3 +426,50 @@ export function getSubmitButtonKeywordsSet(element: HTMLElement): Set<string> {
|
||||
|
||||
return keywordsSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the origin and subdomain match patterns for the URL.
|
||||
*
|
||||
* @param url - The URL of the tab
|
||||
*/
|
||||
export function generateDomainMatchPatterns(url: string): string[] {
|
||||
try {
|
||||
const extensionUrlPattern =
|
||||
/^(chrome|chrome-extension|moz-extension|safari-web-extension):\/\/\/?/;
|
||||
if (extensionUrlPattern.test(url)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Add protocol to URL if it is missing to allow for parsing the hostname correctly
|
||||
const urlPattern = /^(https?|file):\/\/\/?/;
|
||||
if (!urlPattern.test(url)) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
|
||||
let protocolGlob = "*://";
|
||||
if (url.startsWith("file:///")) {
|
||||
protocolGlob = "*:///"; // File URLs require three slashes to be a valid match pattern
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(url);
|
||||
const originMatchPattern = `${protocolGlob}${parsedUrl.hostname}/*`;
|
||||
|
||||
const splitHost = parsedUrl.hostname.split(".");
|
||||
const domain = splitHost.slice(-2).join(".");
|
||||
const subDomainMatchPattern = `${protocolGlob}*.${domain}/*`;
|
||||
|
||||
return [originMatchPattern, subDomainMatchPattern];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the status code of the web response is invalid. An invalid status code is
|
||||
* any status code that is not in the 200-299 range.
|
||||
*
|
||||
* @param statusCode - The status code of the web response
|
||||
*/
|
||||
export function isInvalidResponseStatusCode(statusCode: number) {
|
||||
return statusCode < 200 || statusCode >= 300;
|
||||
}
|
||||
|
||||
@@ -27,3 +27,6 @@ export const passkeyIcon =
|
||||
|
||||
export const circleCheckIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g clip-path="url(#a)"><path fill="#017E45" d="M8 15.5a8.383 8.383 0 0 1-4.445-1.264A7.627 7.627 0 0 1 .61 10.87a7.063 7.063 0 0 1-.455-4.333 7.368 7.368 0 0 1 2.19-3.84A8.181 8.181 0 0 1 6.438.644a8.498 8.498 0 0 1 4.623.427 7.912 7.912 0 0 1 3.59 2.762A7.171 7.171 0 0 1 16 8c-.002 1.988-.846 3.895-2.345 5.3-1.5 1.406-3.534 2.198-5.655 2.2ZM8 1.437a7.337 7.337 0 0 0-3.889 1.106 6.672 6.672 0 0 0-2.578 2.945 6.182 6.182 0 0 0-.399 3.792 6.448 6.448 0 0 0 1.916 3.36 7.156 7.156 0 0 0 3.584 1.796 7.434 7.434 0 0 0 4.044-.374 6.924 6.924 0 0 0 3.142-2.417A6.275 6.275 0 0 0 15 8c-.002-1.74-.74-3.407-2.053-4.638C11.635 2.131 9.856 1.44 8 1.437Zm-1.351 9.905a.361.361 0 0 1-.245-.094l-2.257-2.07a.326.326 0 0 1-.103-.232c0-.043.009-.085.027-.125a.334.334 0 0 1 .076-.107.366.366 0 0 1 .246-.097c.093 0 .182.033.249.093l1.843 1.687a.166.166 0 0 0 .126.044.17.17 0 0 0 .066-.018.157.157 0 0 0 .052-.041l4.623-5.636a.34.34 0 0 1 .102-.088.375.375 0 0 1 .27-.038.34.34 0 0 1 .216.156.311.311 0 0 1-.033.37L6.93 11.21a.344.344 0 0 1-.112.09.376.376 0 0 1-.141.039l-.03.003h.001Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .5h16v15H0z"/></clipPath></defs></svg>';
|
||||
|
||||
export const spinnerIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><g clip-path="url(#a)"><path fill="#5A6D91" d="M4.869 15.015a.588.588 0 1 0 0-1.177.588.588 0 0 0 0 1.177ZM8.252 16a.588.588 0 1 0 0-1.176.588.588 0 0 0 0 1.176Zm3.683-.911a.589.589 0 1 0 0-1.177.589.589 0 0 0 0 1.177ZM2.43 12.882a.693.693 0 1 0 0-1.387.693.693 0 0 0 0 1.387ZM1.318 9.738a.82.82 0 1 0 0-1.64.82.82 0 0 0 0 1.64Zm.69-3.578a.968.968 0 1 0 0-1.937.968.968 0 0 0 0 1.937ZM4.81 3.337a1.175 1.175 0 1 0 0-2.35 1.175 1.175 0 0 0 0 2.35Zm4.597-.676a1.33 1.33 0 1 0 0-2.661 1.33 1.33 0 0 0 0 2.66Zm4.543 2.954a1.553 1.553 0 1 0 0-3.105 1.553 1.553 0 0 0 0 3.105Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>';
|
||||
|
||||
@@ -3,7 +3,9 @@ import { Component, importProvidersFrom } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import {
|
||||
AvatarModule,
|
||||
BadgeModule,
|
||||
@@ -318,6 +320,30 @@ export default {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PolicyService,
|
||||
useFactory: () => {
|
||||
return {
|
||||
policyAppliesToActiveUser$: () => {
|
||||
return {
|
||||
pipe: () => ({
|
||||
subscribe: () => ({}),
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SendService,
|
||||
useFactory: () => {
|
||||
return {
|
||||
sends$: () => {
|
||||
return { pipe: () => ({}) };
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
applicationConfig({
|
||||
|
||||
@@ -1,9 +1,41 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { filter, map, switchMap } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { LinkModule } from "@bitwarden/components";
|
||||
|
||||
const allNavButtons = [
|
||||
{
|
||||
label: "Vault",
|
||||
page: "/tabs/vault",
|
||||
iconKey: "lock",
|
||||
iconKeyActive: "lock-f",
|
||||
},
|
||||
{
|
||||
label: "Generator",
|
||||
page: "/tabs/generator",
|
||||
iconKey: "generate",
|
||||
iconKeyActive: "generate-f",
|
||||
},
|
||||
{
|
||||
label: "Send",
|
||||
page: "/tabs/send",
|
||||
iconKey: "send",
|
||||
iconKeyActive: "send-f",
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
page: "/tabs/settings",
|
||||
iconKey: "cog",
|
||||
iconKeyActive: "cog-f",
|
||||
},
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: "popup-tab-navigation",
|
||||
templateUrl: "popup-tab-navigation.component.html",
|
||||
@@ -14,30 +46,23 @@ import { LinkModule } from "@bitwarden/components";
|
||||
},
|
||||
})
|
||||
export class PopupTabNavigationComponent {
|
||||
navButtons = [
|
||||
{
|
||||
label: "Vault",
|
||||
page: "/tabs/vault",
|
||||
iconKey: "lock",
|
||||
iconKeyActive: "lock-f",
|
||||
},
|
||||
{
|
||||
label: "Generator",
|
||||
page: "/tabs/generator",
|
||||
iconKey: "generate",
|
||||
iconKeyActive: "generate-f",
|
||||
},
|
||||
{
|
||||
label: "Send",
|
||||
page: "/tabs/send",
|
||||
iconKey: "send",
|
||||
iconKeyActive: "send-f",
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
page: "/tabs/settings",
|
||||
iconKey: "cog",
|
||||
iconKeyActive: "cog-f",
|
||||
},
|
||||
];
|
||||
navButtons = allNavButtons;
|
||||
constructor(
|
||||
private policyService: PolicyService,
|
||||
private sendService: SendService,
|
||||
) {
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.DisableSend)
|
||||
.pipe(
|
||||
filter((policyAppliesToActiveUser) => policyAppliesToActiveUser),
|
||||
switchMap(() => this.sendService.sends$),
|
||||
map((sends) => sends.length > 1),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe((hasSends) => {
|
||||
this.navButtons = hasSends
|
||||
? allNavButtons
|
||||
: allNavButtons.filter((b) => b.page !== "/tabs/send");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
>
|
||||
</tools-send-form>
|
||||
|
||||
<send-file-popout-dialog-container [config]="config"></send-file-popout-dialog-container>
|
||||
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton type="submit" form="sendForm" buttonType="primary" #submitBtn>
|
||||
{{ "save" | i18n }}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src
|
||||
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
||||
import { SendFilePopoutDialogContainerComponent } from "../send-file-popout-dialog/send-file-popout-dialog-container.component";
|
||||
|
||||
/**
|
||||
* Helper class to parse query parameters for the AddEdit route.
|
||||
@@ -70,6 +71,7 @@ export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopupFooterComponent,
|
||||
SendFilePopoutDialogContainerComponent,
|
||||
SendFormModule,
|
||||
AsyncActionsModule,
|
||||
],
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { SendFormConfig } from "@bitwarden/send-ui";
|
||||
|
||||
import { FilePopoutUtilsService } from "../../services/file-popout-utils.service";
|
||||
|
||||
import { SendFilePopoutDialogComponent } from "./send-file-popout-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "send-file-popout-dialog-container",
|
||||
templateUrl: "./send-file-popout-dialog-container.component.html",
|
||||
standalone: true,
|
||||
imports: [JslibModule, CommonModule],
|
||||
})
|
||||
export class SendFilePopoutDialogContainerComponent implements OnInit {
|
||||
@Input() config: SendFormConfig;
|
||||
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private filePopoutUtilsService: FilePopoutUtilsService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.config.mode === "add" && this.filePopoutUtilsService.showFilePopoutMessage(window)) {
|
||||
this.dialogService.open(SendFilePopoutDialogComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<bit-simple-dialog dialogSize="default">
|
||||
<div bitDialogIcon>
|
||||
<i class="bwi bwi-info-circle bwi-2x tw-text-info" aria-hidden="true"></i>
|
||||
</div>
|
||||
<ng-container bitDialogContent>
|
||||
<div bitTypography="h3">
|
||||
{{ "sendFilePopoutDialogText" | i18n }}
|
||||
</div>
|
||||
<div bitTypography="body1">{{ "sendFilePopoutDialogDesc" | i18n }}</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button buttonType="primary" bitButton type="button" (click)="popOutWindow()">
|
||||
{{ "popOut" | i18n }}
|
||||
<i class="bwi bwi-popout tw-ml-1" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" type="button" (click)="close()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components";
|
||||
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
|
||||
@Component({
|
||||
selector: "send-file-popout-dialog",
|
||||
templateUrl: "./send-file-popout-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [JslibModule, CommonModule, DialogModule, ButtonModule, TypographyModule],
|
||||
})
|
||||
export class SendFilePopoutDialogComponent {
|
||||
constructor(private dialogService: DialogService) {}
|
||||
|
||||
async popOutWindow() {
|
||||
await BrowserPopupUtils.openCurrentPagePopout(window);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogService.closeAll();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="'send' | i18n">
|
||||
<ng-container slot="end">
|
||||
<tools-new-send-dropdown></tools-new-send-dropdown>
|
||||
|
||||
<tools-new-send-dropdown *ngIf="!sendsDisabled"></tools-new-send-dropdown>
|
||||
<app-pop-out></app-pop-out>
|
||||
<app-current-account></app-current-account>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<div slot="above-scroll-area" class="tw-p-4">
|
||||
<bit-callout *ngIf="sendsDisabled" [title]="'sendDisabled' | i18n">
|
||||
{{ "sendDisabledWarning" | i18n }}
|
||||
</bit-callout>
|
||||
<ng-container *ngIf="!sendsDisabled">
|
||||
<tools-send-search></tools-send-search>
|
||||
<app-send-list-filters></app-send-list-filters>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="listState === sendState.Empty"
|
||||
@@ -15,7 +23,7 @@
|
||||
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
|
||||
<ng-container slot="title">{{ "sendsNoItemsTitle" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "sendsNoItemsMessage" | i18n }}</ng-container>
|
||||
<tools-new-send-dropdown slot="button"></tools-new-send-dropdown>
|
||||
<tools-new-send-dropdown *ngIf="!sendsDisabled" slot="button"></tools-new-send-dropdown>
|
||||
</bit-no-items>
|
||||
</div>
|
||||
|
||||
@@ -31,9 +39,4 @@
|
||||
</div>
|
||||
<app-send-list-items-container [headerText]="title | i18n" [sends]="sends$ | async" />
|
||||
</ng-container>
|
||||
|
||||
<div slot="above-scroll-area" class="tw-p-4" *ngIf="listState !== sendState.Empty">
|
||||
<tools-send-search></tools-send-search>
|
||||
<app-send-list-filters></app-send-list-filters>
|
||||
</div>
|
||||
</popup-page>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { of, BehaviorSubject } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
@@ -46,6 +47,7 @@ describe("SendV2Component", () => {
|
||||
let sendListFiltersServiceFilters$: BehaviorSubject<{ sendType: SendType | null }>;
|
||||
let sendItemsServiceEmptyList$: BehaviorSubject<boolean>;
|
||||
let sendItemsServiceNoFilteredResults$: BehaviorSubject<boolean>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
sendListFiltersServiceFilters$ = new BehaviorSubject({ sendType: null });
|
||||
@@ -60,6 +62,9 @@ describe("SendV2Component", () => {
|
||||
latestSearchText$: of(""),
|
||||
});
|
||||
|
||||
policyService = mock<PolicyService>();
|
||||
policyService.policyAppliesToActiveUser$.mockReturnValue(of(true)); // Return `true` by default
|
||||
|
||||
sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder());
|
||||
|
||||
sendListFiltersService.filters$ = sendListFiltersServiceFilters$;
|
||||
@@ -104,6 +109,7 @@ describe("SendV2Component", () => {
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: SendListFiltersService, useValue: sendListFiltersService },
|
||||
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import { RouterLink } from "@angular/router";
|
||||
import { combineLatest } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components";
|
||||
import { ButtonModule, CalloutModule, Icons, NoItemsModule } from "@bitwarden/components";
|
||||
import {
|
||||
NoSendsIcon,
|
||||
NewSendDropdownComponent,
|
||||
@@ -31,6 +33,7 @@ export enum SendState {
|
||||
templateUrl: "send-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CalloutModule,
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopOutComponent,
|
||||
@@ -48,22 +51,20 @@ export enum SendState {
|
||||
})
|
||||
export class SendV2Component implements OnInit, OnDestroy {
|
||||
sendType = SendType;
|
||||
|
||||
sendState = SendState;
|
||||
|
||||
protected listState: SendState | null = null;
|
||||
|
||||
protected sends$ = this.sendItemsService.filteredAndSortedSends$;
|
||||
|
||||
protected title: string = "allSends";
|
||||
|
||||
protected noItemIcon = NoSendsIcon;
|
||||
|
||||
protected noResultsIcon = Icons.NoResults;
|
||||
|
||||
protected sendsDisabled = false;
|
||||
|
||||
constructor(
|
||||
protected sendItemsService: SendItemsService,
|
||||
protected sendListFiltersService: SendListFiltersService,
|
||||
private policyService: PolicyService,
|
||||
) {
|
||||
combineLatest([
|
||||
this.sendItemsService.emptyList$,
|
||||
@@ -90,6 +91,13 @@ export class SendV2Component implements OnInit, OnDestroy {
|
||||
|
||||
this.listState = null;
|
||||
});
|
||||
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.DisableSend)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((sendsDisabled) => {
|
||||
this.sendsDisabled = sendsDisabled;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class MoreFromBitwardenPageV2Component {
|
||||
private organizationService: OrganizationService,
|
||||
) {
|
||||
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||
this.familySponsorshipAvailable$ = this.organizationService.canManageSponsorships$;
|
||||
this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$;
|
||||
}
|
||||
|
||||
async openFreeBitwardenFamiliesPage() {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { EventType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
@@ -30,20 +31,19 @@ import {
|
||||
import { PremiumUpgradePromptService } from "../../../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view";
|
||||
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
|
||||
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { BrowserViewPasswordHistoryService } from "../../../services/browser-view-password-history.service";
|
||||
|
||||
import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup-page.component";
|
||||
import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-view-v2",
|
||||
templateUrl: "view-v2.component.html",
|
||||
standalone: true,
|
||||
providers: [
|
||||
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SearchModule,
|
||||
@@ -58,6 +58,10 @@ import { VaultPopupAutofillService } from "../../../services/vault-popup-autofil
|
||||
AsyncActionsModule,
|
||||
PopOutComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: BrowserViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
|
||||
],
|
||||
})
|
||||
export class ViewV2Component {
|
||||
headerText: string;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { BrowserViewPasswordHistoryService } from "./browser-view-password-history.service";
|
||||
|
||||
describe("BrowserViewPasswordHistoryService", () => {
|
||||
let service: BrowserViewPasswordHistoryService;
|
||||
let router: MockProxy<Router>;
|
||||
|
||||
beforeEach(async () => {
|
||||
router = mock<Router>();
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [BrowserViewPasswordHistoryService, { provide: Router, useValue: router }],
|
||||
}).compileComponents();
|
||||
|
||||
service = TestBed.inject(BrowserViewPasswordHistoryService);
|
||||
});
|
||||
|
||||
describe("viewPasswordHistory", () => {
|
||||
it("navigates to the password history screen", async () => {
|
||||
await service.viewPasswordHistory("test");
|
||||
expect(router.navigate).toHaveBeenCalledWith(["/cipher-password-history"], {
|
||||
queryParams: { cipherId: "test" },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
|
||||
/**
|
||||
* This class handles the premium upgrade process for the browser extension.
|
||||
*/
|
||||
export class BrowserViewPasswordHistoryService implements ViewPasswordHistoryService {
|
||||
private router = inject(Router);
|
||||
|
||||
/**
|
||||
* Navigates to the password history screen.
|
||||
*/
|
||||
async viewPasswordHistory(cipherId: string) {
|
||||
await this.router.navigate(["/cipher-password-history"], { queryParams: { cipherId } });
|
||||
}
|
||||
}
|
||||
93
apps/desktop/desktop_native/Cargo.lock
generated
93
apps/desktop/desktop_native/Cargo.lock
generated
@@ -85,9 +85,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7"
|
||||
checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
@@ -139,9 +139,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.2.4"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374"
|
||||
checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
@@ -154,7 +154,6 @@ dependencies = [
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -194,9 +193,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.81"
|
||||
version = "0.1.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
|
||||
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -290,9 +289,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.7.1"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
|
||||
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
@@ -305,9 +304,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.15"
|
||||
version = "1.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6"
|
||||
checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@@ -405,9 +404,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.13"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
|
||||
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -603,9 +602,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "error-code"
|
||||
version = "3.2.0"
|
||||
version = "3.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b"
|
||||
checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
@@ -1074,9 +1073,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "1.0.73"
|
||||
version = "1.0.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cd81b794fc1d6051acf8c4f3cb4f82833b0621272a232b4ff0cf3df1dbddb61"
|
||||
checksum = "967c485e00f0bf3b1bdbe510a38a4606919cf1d34d9a37ad41f25a81aa077abe"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"once_cell",
|
||||
@@ -1277,9 +1276,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.0"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
@@ -1339,9 +1338,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
@@ -1387,9 +1386,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.34.0"
|
||||
version = "0.36.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4"
|
||||
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -1435,9 +1434,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.3"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
|
||||
checksum = "62871f2d65009c0256aed1b9cfeeb8ac272833c404e13d53d400cd0dad7a2ac0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
@@ -1548,18 +1547,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.209"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.209"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1763,9 +1762,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.20"
|
||||
version = "0.22.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
|
||||
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -1838,21 +1837,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.13"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
@@ -1874,9 +1873,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.6"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993"
|
||||
checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
@@ -1888,9 +1887,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.5"
|
||||
version = "0.31.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943"
|
||||
checksum = "e3f45d1222915ef1fd2057220c1d9d9624b7654443ea35c3877f7a52bd0a5a2d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"rustix",
|
||||
@@ -1925,9 +1924,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-scanner"
|
||||
version = "0.31.4"
|
||||
version = "0.31.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6"
|
||||
checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
@@ -1936,9 +1935,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-sys"
|
||||
version = "0.31.4"
|
||||
version = "0.31.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148"
|
||||
checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"log",
|
||||
@@ -2176,9 +2175,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.18"
|
||||
version = "0.6.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
|
||||
checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
@@ -46,8 +46,6 @@
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
@@ -58,23 +56,17 @@
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"version": "1.4.15",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
@@ -82,27 +74,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||
"version": "1.0.10",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
@@ -116,8 +100,6 @@
|
||||
},
|
||||
"node_modules/@types/node-ipc": {
|
||||
"version": "9.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz",
|
||||
"integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -125,9 +107,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"version": "8.11.3",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -137,21 +117,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz",
|
||||
"integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==",
|
||||
"version": "8.3.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -159,8 +132,6 @@
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -174,14 +145,10 @@
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
@@ -194,8 +161,6 @@
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@@ -206,20 +171,14 @@
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
@@ -227,8 +186,6 @@
|
||||
},
|
||||
"node_modules/easy-stack": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz",
|
||||
"integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -236,14 +193,10 @@
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"version": "3.1.2",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -251,8 +204,6 @@
|
||||
},
|
||||
"node_modules/event-pubsub": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz",
|
||||
"integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==",
|
||||
"license": "Unlicense",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
@@ -260,8 +211,6 @@
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
@@ -269,8 +218,6 @@
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -278,8 +225,6 @@
|
||||
},
|
||||
"node_modules/js-message": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
|
||||
"integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
@@ -287,8 +232,6 @@
|
||||
},
|
||||
"node_modules/js-queue": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz",
|
||||
"integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"easy-stack": "^1.0.1"
|
||||
@@ -299,20 +242,14 @@
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/module-alias": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.3.tgz",
|
||||
"integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-ipc": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz",
|
||||
"integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-pubsub": "4.3.0",
|
||||
@@ -325,8 +262,6 @@
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -334,8 +269,6 @@
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -348,8 +281,6 @@
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -403,8 +334,6 @@
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -435,14 +364,10 @@
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@@ -458,8 +383,6 @@
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -467,8 +390,6 @@
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
@@ -485,8 +406,6 @@
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -494,8 +413,6 @@
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
|
||||
@@ -382,7 +382,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<small id="enableBrowserIntegrationHelp" class="help-block">{{
|
||||
"enableBrowserIntegrationDesc" | i18n
|
||||
"enableBrowserIntegrationDesc1" | i18n
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -1775,8 +1775,8 @@
|
||||
"enableBrowserIntegration": {
|
||||
"message": "Allow browser integration"
|
||||
},
|
||||
"enableBrowserIntegrationDesc": {
|
||||
"message": "Used for biometrics in browser."
|
||||
"enableBrowserIntegrationDesc1": {
|
||||
"message": "Used to allow biometric unlock in browsers that are not Safari."
|
||||
},
|
||||
"enableDuckDuckGoBrowserIntegration": {
|
||||
"message": "Allow DuckDuckGo browser integration"
|
||||
|
||||
@@ -66,33 +66,39 @@
|
||||
</bit-callout>
|
||||
</bit-section>
|
||||
<bit-section *ngIf="isSelfHost">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||
<form [formGroup]="licenseFormGroup" [bitSubmit]="submitPremiumLicense">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{
|
||||
licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n)
|
||||
}}
|
||||
</div>
|
||||
<input
|
||||
bitInput
|
||||
#fileSelector
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="onLicenseFileSelected($event)"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<ng-container *ngIf="!(useLicenseUploaderComponent$ | async)">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||
<form [formGroup]="licenseFormGroup" [bitSubmit]="submitPremiumLicense">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{
|
||||
licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n)
|
||||
}}
|
||||
</div>
|
||||
<input
|
||||
bitInput
|
||||
#fileSelector
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="onLicenseFileSelected($event)"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
<individual-self-hosting-license-uploader
|
||||
*ngIf="useLicenseUploaderComponent$ | async"
|
||||
(onLicenseFileUploaded)="onLicenseFileSelectedChanged()"
|
||||
/>
|
||||
</bit-section>
|
||||
<form *ngIf="!isSelfHost" [formGroup]="addOnFormGroup" [bitSubmit]="submitPayment">
|
||||
<bit-section>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { combineLatest, concatMap, from, Observable, of } from "rxjs";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -36,6 +38,10 @@ export class PremiumV2Component {
|
||||
protected cloudWebVaultURL: string;
|
||||
protected isSelfHost = false;
|
||||
|
||||
protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
|
||||
);
|
||||
|
||||
protected readonly familyPlanMaxUserCount = 6;
|
||||
protected readonly premiumPrice = 10;
|
||||
protected readonly storageGBPrice = 4;
|
||||
@@ -44,6 +50,7 @@ export class PremiumV2Component {
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
private environmentService: EnvironmentService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@@ -78,6 +85,9 @@ export class PremiumV2Component {
|
||||
finalizeUpgrade = async () => {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
};
|
||||
|
||||
postFinalizeUpgrade = async () => {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
@@ -119,6 +129,7 @@ export class PremiumV2Component {
|
||||
|
||||
await this.apiService.postAccountLicense(formData);
|
||||
await this.finalizeUpgrade();
|
||||
await this.postFinalizeUpgrade();
|
||||
};
|
||||
|
||||
submitPayment = async (): Promise<void> => {
|
||||
@@ -138,6 +149,7 @@ export class PremiumV2Component {
|
||||
|
||||
await this.apiService.postPremium(formData);
|
||||
await this.finalizeUpgrade();
|
||||
await this.postFinalizeUpgrade();
|
||||
};
|
||||
|
||||
protected get additionalStorageCost(): number {
|
||||
@@ -161,4 +173,8 @@ export class PremiumV2Component {
|
||||
protected get total(): number {
|
||||
return this.subtotal + this.estimatedTax;
|
||||
}
|
||||
|
||||
protected async onLicenseFileSelectedChanged(): Promise<void> {
|
||||
await this.postFinalizeUpgrade();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,32 +7,38 @@
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="createOrganization && selfHosted">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div class="tw-pt-2 tw-pb-1">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
|
||||
</div>
|
||||
<input
|
||||
#fileSelector
|
||||
bitInput
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="setSelectedFile($event)"
|
||||
accept="application/JSON"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<ng-container *ngIf="!(useLicenseUploaderComponent$ | async)">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div class="tw-pt-2 tw-pb-1">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
|
||||
</div>
|
||||
<input
|
||||
#fileSelector
|
||||
bitInput
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="setSelectedFile($event)"
|
||||
accept="application/JSON"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
<organization-self-hosting-license-uploader
|
||||
*ngIf="useLicenseUploaderComponent$ | async"
|
||||
(onLicenseFileUploaded)="onLicenseFileUploaded($event)"
|
||||
/>
|
||||
</ng-container>
|
||||
<form
|
||||
[formGroup]="formGroup"
|
||||
|
||||
@@ -117,6 +117,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
discount = 0;
|
||||
deprecateStripeSourcesAPI: boolean;
|
||||
|
||||
protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
|
||||
);
|
||||
|
||||
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
|
||||
|
||||
selfHostedForm = this.formBuilder.group({
|
||||
@@ -855,4 +859,30 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
private planIsEnabled(plan: PlanResponse) {
|
||||
return !plan.disabled && !plan.legacyYear;
|
||||
}
|
||||
|
||||
protected async onLicenseFileUploaded(organizationId: string): Promise<void> {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("organizationCreated"),
|
||||
message: this.i18nService.t("organizationReadyToGo"),
|
||||
});
|
||||
|
||||
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/organizations/" + organizationId]);
|
||||
}
|
||||
|
||||
if (this.isInTrialFlow) {
|
||||
this.onTrialBillingSuccess.emit({
|
||||
orgId: organizationId,
|
||||
subLabelText: this.billingSubLabelText(),
|
||||
});
|
||||
}
|
||||
|
||||
this.onSuccess.emit({ organizationId: organizationId });
|
||||
|
||||
// TODO: No one actually listening to this message?
|
||||
this.messagingService.send("organizationCreated", { organizationId: organizationId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import { OffboardingSurveyComponent } from "./offboarding-survey.component";
|
||||
import { PaymentV2Component } from "./payment/payment-v2.component";
|
||||
import { PaymentComponent } from "./payment/payment.component";
|
||||
import { PaymentMethodComponent } from "./payment-method.component";
|
||||
import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component";
|
||||
import { OrganizationSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/organization-self-hosting-license-uploader.component";
|
||||
import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component";
|
||||
import { TaxInfoComponent } from "./tax-info.component";
|
||||
import { UpdateLicenseDialogComponent } from "./update-license-dialog.component";
|
||||
@@ -40,6 +42,8 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
|
||||
OffboardingSurveyComponent,
|
||||
AdjustPaymentDialogV2Component,
|
||||
AdjustStorageDialogV2Component,
|
||||
IndividualSelfHostingLicenseUploaderComponent,
|
||||
OrganizationSelfHostingLicenseUploaderComponent,
|
||||
],
|
||||
exports: [
|
||||
SharedModule,
|
||||
@@ -53,6 +57,8 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
|
||||
OffboardingSurveyComponent,
|
||||
VerifyBankAccountComponent,
|
||||
PaymentV2Component,
|
||||
IndividualSelfHostingLicenseUploaderComponent,
|
||||
OrganizationSelfHostingLicenseUploaderComponent,
|
||||
],
|
||||
})
|
||||
export class BillingSharedModule {}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
||||
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { LicenseUploaderFormModel } from "./license-uploader-form-model";
|
||||
|
||||
/**
|
||||
* Shared implementation for processing license file uploads.
|
||||
* @remarks Requires self-hosting.
|
||||
*/
|
||||
export abstract class AbstractSelfHostingLicenseUploaderComponent {
|
||||
protected form: FormGroup;
|
||||
|
||||
protected constructor(
|
||||
protected readonly formBuilder: FormBuilder,
|
||||
protected readonly i18nService: I18nService,
|
||||
protected readonly platformUtilsService: PlatformUtilsService,
|
||||
protected readonly toastService: ToastService,
|
||||
protected readonly tokenService: TokenService,
|
||||
) {
|
||||
const isSelfHosted = this.platformUtilsService.isSelfHost();
|
||||
|
||||
if (!isSelfHosted) {
|
||||
throw new Error("This component should only be used in self-hosted environments");
|
||||
}
|
||||
|
||||
this.form = this.formBuilder.group({
|
||||
file: [null, [Validators.required]],
|
||||
});
|
||||
this.submit = this.submit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the submitted license upload form model.
|
||||
* @protected
|
||||
*/
|
||||
protected get formValue(): LicenseUploaderFormModel {
|
||||
return this.form.value as LicenseUploaderFormModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered when a different license file is selected.
|
||||
* @param event
|
||||
*/
|
||||
onLicenseFileSelectedChanged(event: Event): void {
|
||||
const element = event.target as HTMLInputElement;
|
||||
this.form.value.file = element.files.length > 0 ? element.files[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the license upload form.
|
||||
* @protected
|
||||
*/
|
||||
protected async submit(): Promise<void> {
|
||||
this.form.markAllAsTouched();
|
||||
|
||||
if (this.form.invalid) {
|
||||
return this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("selectFile"),
|
||||
});
|
||||
}
|
||||
|
||||
const emailVerified = await this.tokenService.getEmailVerified();
|
||||
if (!emailVerified) {
|
||||
return this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("verifyEmailFirst"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
abstract get description(): string;
|
||||
|
||||
abstract get hintFileName(): string;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-hosting-license-uploader/abstract-self-hosting-license-uploader.component";
|
||||
|
||||
/**
|
||||
* Processes license file uploads for individual plans.
|
||||
* @remarks Requires self-hosting.
|
||||
*/
|
||||
@Component({
|
||||
selector: "individual-self-hosting-license-uploader",
|
||||
templateUrl: "./self-hosting-license-uploader.component.html",
|
||||
})
|
||||
export class IndividualSelfHostingLicenseUploaderComponent extends AbstractSelfHostingLicenseUploaderComponent {
|
||||
/**
|
||||
* Emitted when a license file has been successfully uploaded & processed.
|
||||
*/
|
||||
@Output() onLicenseFileUploaded: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
protected readonly apiService: ApiService,
|
||||
protected readonly formBuilder: FormBuilder,
|
||||
protected readonly i18nService: I18nService,
|
||||
protected readonly platformUtilsService: PlatformUtilsService,
|
||||
protected readonly syncService: SyncService,
|
||||
protected readonly toastService: ToastService,
|
||||
protected readonly tokenService: TokenService,
|
||||
) {
|
||||
super(formBuilder, i18nService, platformUtilsService, toastService, tokenService);
|
||||
}
|
||||
|
||||
protected async submit(): Promise<void> {
|
||||
await super.submit();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("license", this.formValue.file);
|
||||
|
||||
await this.apiService.postAccountLicense(formData);
|
||||
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
this.onLicenseFileUploaded.emit();
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return "uploadLicenseFilePremium";
|
||||
}
|
||||
|
||||
get hintFileName(): string {
|
||||
return "bitwarden_premium_license.json";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface LicenseUploaderFormModel {
|
||||
file: File;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-hosting-license-uploader/abstract-self-hosting-license-uploader.component";
|
||||
|
||||
/**
|
||||
* Processes license file uploads for organizations.
|
||||
* @remarks Requires self-hosting.
|
||||
*/
|
||||
@Component({
|
||||
selector: "organization-self-hosting-license-uploader",
|
||||
templateUrl: "./self-hosting-license-uploader.component.html",
|
||||
})
|
||||
export class OrganizationSelfHostingLicenseUploaderComponent extends AbstractSelfHostingLicenseUploaderComponent {
|
||||
/**
|
||||
* Notifies the parent component of the `organizationId` the license was created for.
|
||||
*/
|
||||
@Output() onLicenseFileUploaded: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
constructor(
|
||||
protected readonly formBuilder: FormBuilder,
|
||||
protected readonly i18nService: I18nService,
|
||||
protected readonly platformUtilsService: PlatformUtilsService,
|
||||
protected readonly toastService: ToastService,
|
||||
protected readonly tokenService: TokenService,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly encryptService: EncryptService,
|
||||
private readonly cryptoService: CryptoService,
|
||||
private readonly organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private readonly syncService: SyncService,
|
||||
) {
|
||||
super(formBuilder, i18nService, platformUtilsService, toastService, tokenService);
|
||||
}
|
||||
|
||||
protected async submit(): Promise<void> {
|
||||
await super.submit();
|
||||
|
||||
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
|
||||
const key = orgKey[0].encryptedString;
|
||||
const collection = await this.encryptService.encrypt(
|
||||
this.i18nService.t("defaultCollection"),
|
||||
orgKey[1],
|
||||
);
|
||||
const collectionCt = collection.encryptedString;
|
||||
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append("license", this.formValue.file);
|
||||
fd.append("key", key);
|
||||
fd.append("collectionName", collectionCt);
|
||||
const response = await this.organizationApiService.createLicense(fd);
|
||||
const orgId = response.id;
|
||||
|
||||
await this.apiService.refreshIdentityToken();
|
||||
|
||||
// Org Keys live outside of the OrganizationLicense - add the keys to the org here
|
||||
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||
await this.organizationApiService.updateKeys(orgId, request);
|
||||
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
this.onLicenseFileUploaded.emit(orgId);
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return "uploadLicenseFileOrg";
|
||||
}
|
||||
|
||||
get hintFileName(): string {
|
||||
return "bitwarden_organization_license.json";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||
<form [formGroup]="form" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ description | i18n }}</bit-label>
|
||||
<div>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{ form.value.file ? form.value.file.name : ("noFileChosen" | i18n) }}
|
||||
</div>
|
||||
<input
|
||||
#fileSelector
|
||||
bitInput
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="onLicenseFileSelectedChanged($event)"
|
||||
accept="application/JSON"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: hintFileName }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
@@ -0,0 +1,40 @@
|
||||
<bit-dialog dialogSize="small" background="alt">
|
||||
<span bitDialogTitle>
|
||||
{{ "passwordHistory" | i18n }}
|
||||
</span>
|
||||
<ng-container bitDialogContent>
|
||||
<div *ngIf="history && history.length">
|
||||
<bit-item *ngFor="let h of history">
|
||||
<div class="tw-pl-3 tw-py-2">
|
||||
<bit-color-password
|
||||
class="tw-text-base"
|
||||
[password]="h.password"
|
||||
[showCount]="false"
|
||||
></bit-color-password>
|
||||
<div class="tw-text-sm tw-text-muted">{{ h.lastUsedDate | date: "medium" }}</div>
|
||||
</div>
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
aria-label="Copy"
|
||||
appStopClick
|
||||
(click)="copy(h.password)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
</div>
|
||||
<div class="no-items" *ngIf="!history || !history.length">
|
||||
<p>{{ "noPasswordsInList" | i18n }}</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton (click)="close()" buttonType="primary" type="button">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,131 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { OnInit, Inject, Component } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
ToastService,
|
||||
ItemModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
|
||||
/**
|
||||
* The parameters for the password history dialog.
|
||||
*/
|
||||
export interface ViewPasswordHistoryDialogParams {
|
||||
cipherId: CipherId;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dialog component that displays the password history for a cipher.
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-vault-password-history",
|
||||
templateUrl: "password-history.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, AsyncActionsModule, DialogModule, ItemModule, SharedModule],
|
||||
})
|
||||
export class PasswordHistoryComponent implements OnInit {
|
||||
/**
|
||||
* The ID of the cipher to display the password history for.
|
||||
*/
|
||||
cipherId: CipherId;
|
||||
|
||||
/**
|
||||
* The password history for the cipher.
|
||||
*/
|
||||
history: PasswordHistoryView[] = [];
|
||||
|
||||
/**
|
||||
* The constructor for the password history dialog component.
|
||||
* @param params The parameters passed to the password history dialog.
|
||||
* @param cipherService The cipher service - used to get the cipher to display the password history for.
|
||||
* @param platformUtilsService The platform utils service - used to copy passwords to the clipboard.
|
||||
* @param i18nService The i18n service - used to translate strings.
|
||||
* @param accountService The account service - used to get the active account to decrypt the cipher.
|
||||
* @param win The window object - used to copy passwords to the clipboard.
|
||||
* @param toastService The toast service - used to display feedback to the user when a password is copied.
|
||||
* @param dialogRef The dialog reference - used to close the dialog.
|
||||
**/
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) public params: ViewPasswordHistoryDialogParams,
|
||||
protected cipherService: CipherService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected i18nService: I18nService,
|
||||
protected accountService: AccountService,
|
||||
@Inject(WINDOW) private win: Window,
|
||||
protected toastService: ToastService,
|
||||
private dialogRef: DialogRef<PasswordHistoryComponent>,
|
||||
) {
|
||||
/**
|
||||
* Set the cipher ID from the parameters.
|
||||
*/
|
||||
this.cipherId = params.cipherId;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a password to the clipboard.
|
||||
* @param password The password to copy.
|
||||
*/
|
||||
copy(password: string) {
|
||||
const copyOptions = this.win != null ? { window: this.win } : undefined;
|
||||
this.platformUtilsService.copyToClipboard(password, copyOptions);
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: "",
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t("password")),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the password history dialog component.
|
||||
*/
|
||||
protected async init() {
|
||||
const cipher = await this.cipherService.get(this.cipherId);
|
||||
const activeAccount = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)),
|
||||
);
|
||||
|
||||
if (!activeAccount || !activeAccount.id) {
|
||||
throw new Error("Active account is not available.");
|
||||
}
|
||||
|
||||
const activeUserId = activeAccount.id as UserId;
|
||||
const decCipher = await cipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
|
||||
);
|
||||
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the password history dialog.
|
||||
*/
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed wrapper around the dialog service to open the password history dialog.
|
||||
*/
|
||||
export function openPasswordHistoryDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<ViewPasswordHistoryDialogParams>,
|
||||
) {
|
||||
return dialogService.open(PasswordHistoryComponent, config);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
@@ -22,6 +23,7 @@ import { PremiumUpgradePromptService } from "../../../../../../libs/common/src/v
|
||||
import { CipherViewComponent } from "../../../../../../libs/vault/src/cipher-view/cipher-view.component";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service";
|
||||
import { WebViewPasswordHistoryService } from "../services/web-view-password-history.service";
|
||||
|
||||
export interface ViewCipherDialogParams {
|
||||
cipher: CipherView;
|
||||
@@ -57,6 +59,7 @@ export interface ViewCipherDialogCloseResult {
|
||||
standalone: true,
|
||||
imports: [CipherViewComponent, CommonModule, AsyncActionsModule, DialogModule, SharedModule],
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Overlay } from "@angular/cdk/overlay";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { openPasswordHistoryDialog } from "../individual-vault/password-history.component";
|
||||
|
||||
import { WebViewPasswordHistoryService } from "./web-view-password-history.service";
|
||||
|
||||
jest.mock("../individual-vault/password-history.component", () => ({
|
||||
openPasswordHistoryDialog: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("WebViewPasswordHistoryService", () => {
|
||||
let service: WebViewPasswordHistoryService;
|
||||
let dialogService: DialogService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockDialogService = {
|
||||
open: jest.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
WebViewPasswordHistoryService,
|
||||
{ provide: DialogService, useValue: mockDialogService },
|
||||
Overlay,
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
service = TestBed.inject(WebViewPasswordHistoryService);
|
||||
dialogService = TestBed.inject(DialogService);
|
||||
});
|
||||
|
||||
describe("viewPasswordHistory", () => {
|
||||
it("calls openPasswordHistoryDialog with the correct parameters", async () => {
|
||||
const mockCipherId = "cipher-id" as CipherId;
|
||||
await service.viewPasswordHistory(mockCipherId);
|
||||
expect(openPasswordHistoryDialog).toHaveBeenCalledWith(dialogService, {
|
||||
data: { cipherId: mockCipherId },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ViewPasswordHistoryService } from "../../../../../../libs/common/src/vault/abstractions/view-password-history.service";
|
||||
import { openPasswordHistoryDialog } from "../individual-vault/password-history.component";
|
||||
|
||||
/**
|
||||
* This service is used to display the password history dialog in the web vault.
|
||||
*/
|
||||
@Injectable()
|
||||
export class WebViewPasswordHistoryService implements ViewPasswordHistoryService {
|
||||
constructor(private dialogService: DialogService) {}
|
||||
|
||||
/**
|
||||
* Opens the password history dialog for the given cipher ID.
|
||||
* @param cipherId The ID of the cipher to view the password history for.
|
||||
*/
|
||||
async viewPasswordHistory(cipherId: CipherId) {
|
||||
openPasswordHistoryDialog(this.dialogService, { data: { cipherId } });
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export class CollectionsComponent implements OnInit {
|
||||
if (this.organization.canEditAllCiphers) {
|
||||
return !!(c as any).checked;
|
||||
} else {
|
||||
return !!(c as any).checked && c.readOnly == null;
|
||||
return !!(c as any).checked && !c.readOnly;
|
||||
}
|
||||
})
|
||||
.map((c) => c.id);
|
||||
|
||||
@@ -117,6 +117,10 @@ export abstract class OrganizationService {
|
||||
* Emits true if the user can create or manage a Free Bitwarden Families sponsorship.
|
||||
*/
|
||||
canManageSponsorships$: Observable<boolean>;
|
||||
/**
|
||||
* Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available.
|
||||
*/
|
||||
familySponsorshipAvailable$: Observable<boolean>;
|
||||
hasOrganizations: () => Promise<boolean>;
|
||||
get$: (id: string) => Observable<Organization | undefined>;
|
||||
get: (id: string) => Promise<Organization>;
|
||||
|
||||
@@ -88,6 +88,10 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti
|
||||
mapToBooleanHasAnyOrganizations(),
|
||||
);
|
||||
|
||||
familySponsorshipAvailable$ = this.organizations$.pipe(
|
||||
map((orgs) => orgs.some((o) => o.familySponsorshipAvailable)),
|
||||
);
|
||||
|
||||
async hasOrganizations(): Promise<boolean> {
|
||||
return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations()));
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export enum FeatureFlag {
|
||||
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
||||
AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@@ -76,6 +77,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
|
||||
[FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
@@ -21,6 +21,7 @@ export class ProfileResponse extends BaseResponse {
|
||||
securityStamp: string;
|
||||
forcePasswordReset: boolean;
|
||||
usesKeyConnector: boolean;
|
||||
managedByOrganizationId?: string | null;
|
||||
organizations: ProfileOrganizationResponse[] = [];
|
||||
providers: ProfileProviderResponse[] = [];
|
||||
providerOrganizations: ProfileProviderOrganizationResponse[] = [];
|
||||
@@ -42,6 +43,7 @@ export class ProfileResponse extends BaseResponse {
|
||||
this.securityStamp = this.getResponseProperty("SecurityStamp");
|
||||
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset") ?? false;
|
||||
this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector") ?? false;
|
||||
this.managedByOrganizationId = this.getResponseProperty("ManagedByOrganizationId");
|
||||
|
||||
const organizations = this.getResponseProperty("Organizations");
|
||||
if (organizations != null) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { CipherId } from "../../types/guid";
|
||||
|
||||
/**
|
||||
* The ViewPasswordHistoryService is responsible for displaying the password history for a cipher.
|
||||
*/
|
||||
export abstract class ViewPasswordHistoryService {
|
||||
abstract viewPasswordHistory(cipherId?: CipherId): Promise<void>;
|
||||
}
|
||||
@@ -27,10 +27,8 @@
|
||||
</p>
|
||||
<a
|
||||
*ngIf="cipher.hasPasswordHistory && isLogin"
|
||||
bitLink
|
||||
class="tw-font-bold tw-no-underline"
|
||||
routerLink="/cipher-password-history"
|
||||
[queryParams]="{ cipherId: cipher.id }"
|
||||
class="tw-font-bold tw-no-underline tw-cursor-pointer"
|
||||
(click)="viewPasswordHistory()"
|
||||
bitTypography="body2"
|
||||
>
|
||||
{{ "passwordHistory" | i18n }}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Component, Input } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
@@ -31,7 +33,16 @@ import {
|
||||
export class ItemHistoryV2Component {
|
||||
@Input() cipher: CipherView;
|
||||
|
||||
constructor(private viewPasswordHistoryService: ViewPasswordHistoryService) {}
|
||||
|
||||
get isLogin() {
|
||||
return this.cipher.type === CipherType.Login;
|
||||
}
|
||||
|
||||
/**
|
||||
* View the password history for the cipher.
|
||||
*/
|
||||
async viewPasswordHistory() {
|
||||
await this.viewPasswordHistoryService.viewPasswordHistory(this.cipher?.id as CipherId);
|
||||
}
|
||||
}
|
||||
|
||||
950
package-lock.json
generated
950
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,7 @@
|
||||
"@storybook/manager-api": "8.2.9",
|
||||
"@storybook/theming": "8.2.9",
|
||||
"@types/argon2-browser": "1.18.4",
|
||||
"@types/chrome": "0.0.270",
|
||||
"@types/chrome": "0.0.272",
|
||||
"@types/firefox-webext-browser": "111.0.5",
|
||||
"@types/inquirer": "8.2.10",
|
||||
"@types/jest": "29.5.12",
|
||||
@@ -209,7 +209,8 @@
|
||||
"@storybook/angular": {
|
||||
"zone.js": "$zone.js"
|
||||
},
|
||||
"replacestream": "4.0.3"
|
||||
"replacestream": "4.0.3",
|
||||
"@types/minimatch": "3.0.5"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "prettier --cache --ignore-unknown --write",
|
||||
|
||||
Reference in New Issue
Block a user