+ | {{ i.date | date: "mediumDate" }} |
+
+
+
+ {{ "invoiceNumber" | i18n: i.number }}
+ |
+ {{ i.amount | currency: "$" }} |
+
{{ "paid" | i18n }}
diff --git a/apps/web/src/app/billing/shared/billing-history.component.ts b/apps/web/src/app/billing/shared/billing-history.component.ts
index ac16b3dc72a..541edc912c9 100644
--- a/apps/web/src/app/billing/shared/billing-history.component.ts
+++ b/apps/web/src/app/billing/shared/billing-history.component.ts
@@ -12,7 +12,10 @@ import {
})
export class BillingHistoryComponent {
@Input()
- invoices: BillingInvoiceResponse[];
+ openInvoices: BillingInvoiceResponse[];
+
+ @Input()
+ paidInvoices: BillingInvoiceResponse[];
@Input()
transactions: BillingTransactionResponse[];
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index a1ea607f4be..96afa9dd1aa 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -2616,8 +2616,11 @@
"invoices": {
"message": "Invoices"
},
- "noInvoices": {
- "message": "No invoices."
+ "noUnpaidInvoices": {
+ "message": "No unpaid invoices."
+ },
+ "noPaidInvoices": {
+ "message": "No paid invoices."
},
"paid": {
"message": "Paid",
diff --git a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts
index 4b67ce55c30..66f164ce50e 100644
--- a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts
+++ b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts
@@ -4,9 +4,6 @@ import {
} from "@bitwarden/common/billing/models/response/billing.response";
export class AccountBillingApiServiceAbstraction {
- getBillingInvoices: (id: string, startAfter?: string) => Promise;
- getBillingTransactions: (
- id: string,
- startAfter?: string,
- ) => Promise;
+ getBillingInvoices: (status?: string, startAfter?: string) => Promise;
+ getBillingTransactions: (startAfter?: string) => Promise;
}
diff --git a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts
index 4b3592bb6d3..639f1fdb7c7 100644
--- a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts
+++ b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts
@@ -4,7 +4,12 @@ import {
} from "@bitwarden/common/billing/models/response/billing.response";
export class OrganizationBillingApiServiceAbstraction {
- getBillingInvoices: (id: string, startAfter?: string) => Promise;
+ getBillingInvoices: (
+ id: string,
+ status?: string,
+ startAfter?: string,
+ ) => Promise;
+
getBillingTransactions: (
id: string,
startAfter?: string,
diff --git a/libs/common/src/billing/services/account/account-billing-api.service.ts b/libs/common/src/billing/services/account/account-billing-api.service.ts
index ddd5bad02e1..f94940ecef0 100644
--- a/libs/common/src/billing/services/account/account-billing-api.service.ts
+++ b/libs/common/src/billing/services/account/account-billing-api.service.ts
@@ -8,11 +8,25 @@ import {
export class AccountBillingApiService implements AccountBillingApiServiceAbstraction {
constructor(private apiService: ApiService) {}
- async getBillingInvoices(startAfter?: string): Promise {
- const queryParams = startAfter ? `?startAfter=${startAfter}` : "";
+ async getBillingInvoices(
+ status?: string,
+ startAfter?: string,
+ ): Promise {
+ const params = new URLSearchParams();
+
+ if (status) {
+ params.append("status", status);
+ }
+
+ if (startAfter) {
+ params.append("startAfter", startAfter);
+ }
+
+ const queryString = `?${params.toString()}`;
+
const r = await this.apiService.send(
"GET",
- `/accounts/billing/invoices${queryParams}`,
+ `/accounts/billing/invoices${queryString}`,
null,
true,
true,
diff --git a/libs/common/src/billing/services/organization/organization-billing-api.service.ts b/libs/common/src/billing/services/organization/organization-billing-api.service.ts
index acf12e83209..9bf1e6ee6d9 100644
--- a/libs/common/src/billing/services/organization/organization-billing-api.service.ts
+++ b/libs/common/src/billing/services/organization/organization-billing-api.service.ts
@@ -8,11 +8,26 @@ import {
export class OrganizationBillingApiService implements OrganizationBillingApiServiceAbstraction {
constructor(private apiService: ApiService) {}
- async getBillingInvoices(id: string, startAfter?: string): Promise {
- const queryParams = startAfter ? `?startAfter=${startAfter}` : "";
+ async getBillingInvoices(
+ id: string,
+ status?: string,
+ startAfter?: string,
+ ): Promise {
+ const params = new URLSearchParams();
+
+ if (status) {
+ params.append("status", status);
+ }
+
+ if (startAfter) {
+ params.append("startAfter", startAfter);
+ }
+
+ const queryString = `?${params.toString()}`;
+
const r = await this.apiService.send(
"GET",
- `/organizations/${id}/billing/invoices${queryParams}`,
+ `/organizations/${id}/billing/invoices${queryString}`,
null,
true,
true,
From ce871672d25761664384df0b27f5fec4f878042c Mon Sep 17 00:00:00 2001
From: Cesar Gonzalez
Date: Tue, 8 Oct 2024 14:03:58 -0500
Subject: [PATCH 12/25] [PM-13284] Reworked notification bar does not display
for certain websites (#11462)
* [PM-13284] Notification bar does not display for certain websites
* [PM-13284] Notification bar does not display for certain websites
* [PM-13284] Notification bar does not display for certain websites
---
.../overlay-notifications.background.spec.ts | 20 +++++++++++
.../overlay-notifications.background.ts | 34 ++++++++++++++++++-
2 files changed, 53 insertions(+), 1 deletion(-)
diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts
index d694438c00f..1d4ff20b79a 100644
--- a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts
+++ b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts
@@ -164,8 +164,17 @@ describe("OverlayNotificationsBackground", () => {
});
describe("storing the modified login form data", () => {
+ const pageDetails = mock({ fields: [mock()] });
const sender = mock({ tab: { id: 1 } });
+ beforeEach(async () => {
+ sendMockExtensionMessage(
+ { command: "collectPageDetailsResponse", details: pageDetails },
+ sender,
+ );
+ await flushPromises();
+ });
+
it("stores the modified login cipher form data", async () => {
sendMockExtensionMessage(
{
@@ -349,8 +358,14 @@ describe("OverlayNotificationsBackground", () => {
describe("web requests that trigger notifications", () => {
const requestId = "123345";
+ const pageDetails = mock({ fields: [mock()] });
beforeEach(async () => {
+ sendMockExtensionMessage(
+ { command: "collectPageDetailsResponse", details: pageDetails },
+ sender,
+ );
+ await flushPromises();
sendMockExtensionMessage(
{
command: "formFieldSubmitted",
@@ -446,6 +461,11 @@ describe("OverlayNotificationsBackground", () => {
it("triggers the notification on the beforeRequest listener when a post-submission redirection is encountered", async () => {
sender.tab = mock({ id: 4 });
+ sendMockExtensionMessage(
+ { command: "collectPageDetailsResponse", details: pageDetails },
+ sender,
+ );
+ await flushPromises();
sendMockExtensionMessage(
{
command: "formFieldSubmitted",
diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts
index ca8f05b77dc..5ba3e388b7d 100644
--- a/apps/browser/src/autofill/background/overlay-notifications.background.ts
+++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts
@@ -24,6 +24,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set();
private modifyLoginCipherFormData: ModifyLoginCipherFormDataForTab = new Map();
private clearLoginCipherFormDataSubject: Subject = new Subject();
+ private notificationFallbackTimeout: number | NodeJS.Timeout | null;
private readonly formSubmissionRequestMethods: Set = new Set(["POST", "PUT", "PATCH"]);
private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = {
formFieldSubmitted: ({ message, sender }) => this.storeModifiedLoginFormData(message, sender),
@@ -126,6 +127,10 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
message: OverlayNotificationsExtensionMessage,
sender: chrome.runtime.MessageSender,
) => {
+ if (!this.websiteOriginsWithFields.has(sender.tab.id)) {
+ return;
+ }
+
const { uri, username, password, newPassword } = message;
if (!username && !password && !newPassword) {
return;
@@ -142,8 +147,29 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
}
this.modifyLoginCipherFormData.set(sender.tab.id, formData);
+
+ this.clearNotificationFallbackTimeout();
+ this.notificationFallbackTimeout = setTimeout(
+ () =>
+ this.setupNotificationInitTrigger(
+ sender.tab.id,
+ "",
+ this.modifyLoginCipherFormData.get(sender.tab.id),
+ ).catch((error) => this.logService.error(error)),
+ 1500,
+ );
};
+ /**
+ * Clears the timeout used when triggering a notification on click of the submit button.
+ */
+ private clearNotificationFallbackTimeout() {
+ if (this.notificationFallbackTimeout) {
+ clearTimeout(this.notificationFallbackTimeout);
+ this.notificationFallbackTimeout = null;
+ }
+ }
+
/**
* Determines if the sender of the message is from an excluded domain. This is used to prevent the
* add login or change password notification from being triggered on the user's vault domain or
@@ -306,12 +332,16 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
private handleOnCompletedRequestEvent = async (details: chrome.webRequest.WebResponseDetails) => {
if (
this.requestHostIsInvalid(details) ||
- isInvalidResponseStatusCode(details.statusCode) ||
!this.activeFormSubmissionRequests.has(details.requestId)
) {
return;
}
+ if (isInvalidResponseStatusCode(details.statusCode)) {
+ this.clearNotificationFallbackTimeout();
+ return;
+ }
+
const modifyLoginData = this.modifyLoginCipherFormData.get(details.tabId);
if (!modifyLoginData) {
return;
@@ -335,6 +365,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
requestId: string,
modifyLoginData: ModifyLoginCipherFormData,
) => {
+ this.clearNotificationFallbackTimeout();
+
const tab = await BrowserApi.getTab(tabId);
if (tab.status !== "complete") {
await this.delayNotificationInitUntilTabIsComplete(tabId, requestId, modifyLoginData);
From fdfbe66513a22a8da1ae5895612de3670d018c06 Mon Sep 17 00:00:00 2001
From: Cesar Gonzalez
Date: Tue, 8 Oct 2024 15:47:20 -0500
Subject: [PATCH 13/25] [PM-13284] Implement method to ensure that we can
handle logic when switching the notification improvements feature flag
(#11468)
---
.../overlay-notifications.background.spec.ts | 7 ++-
.../overlay-notifications.background.ts | 47 +++++++++++++++----
2 files changed, 43 insertions(+), 11 deletions(-)
diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts
index 1d4ff20b79a..8bac8ea6913 100644
--- a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts
+++ b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts
@@ -1,4 +1,5 @@
import { mock, MockProxy } from "jest-mock-extended";
+import { BehaviorSubject } from "rxjs";
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -24,6 +25,7 @@ import { OverlayNotificationsBackground } from "./overlay-notifications.backgrou
describe("OverlayNotificationsBackground", () => {
let logService: MockProxy;
+ let getFeatureFlagMock$: BehaviorSubject;
let configService: MockProxy;
let notificationBackground: NotificationBackground;
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
@@ -33,7 +35,10 @@ describe("OverlayNotificationsBackground", () => {
beforeEach(async () => {
jest.useFakeTimers();
logService = mock();
- configService = mock();
+ getFeatureFlagMock$ = new BehaviorSubject(true);
+ configService = mock({
+ getFeatureFlag$: jest.fn().mockReturnValue(getFeatureFlagMock$),
+ });
notificationBackground = mock();
getEnableChangedPasswordPromptSpy = jest
.spyOn(notificationBackground, "getEnableChangedPasswordPrompt")
diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts
index 5ba3e388b7d..5ea3e8b8d6b 100644
--- a/apps/browser/src/autofill/background/overlay-notifications.background.ts
+++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts
@@ -1,4 +1,5 @@
-import { Subject, switchMap, timer } from "rxjs";
+import { startWith, Subject, Subscription, switchMap, timer } from "rxjs";
+import { pairwise } from "rxjs/operators";
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -23,6 +24,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
private websiteOriginsWithFields: WebsiteOriginsWithFields = new Map();
private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set();
private modifyLoginCipherFormData: ModifyLoginCipherFormDataForTab = new Map();
+ private featureFlagState$: Subscription;
private clearLoginCipherFormDataSubject: Subject = new Subject();
private notificationFallbackTimeout: number | NodeJS.Timeout | null;
private readonly formSubmissionRequestMethods: Set = new Set(["POST", "PUT", "PATCH"]);
@@ -42,19 +44,35 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
* Initialize the overlay notifications background service.
*/
async init() {
- const featureFlagActive = await this.configService.getFeatureFlag(
- FeatureFlag.NotificationBarAddLoginImprovements,
- );
- if (!featureFlagActive) {
- return;
- }
-
- this.setupExtensionListeners();
+ this.featureFlagState$ = this.configService
+ .getFeatureFlag$(FeatureFlag.NotificationBarAddLoginImprovements)
+ .pipe(startWith(undefined), pairwise())
+ .subscribe(([prev, current]) => this.handleInitFeatureFlagChange(prev, current));
this.clearLoginCipherFormDataSubject
.pipe(switchMap(() => timer(CLEAR_NOTIFICATION_LOGIN_DATA_DURATION)))
.subscribe(() => this.modifyLoginCipherFormData.clear());
}
+ /**
+ * Handles enabling/disabling the extension listeners that trigger the
+ * overlay notifications based on the feature flag state.
+ *
+ * @param previousValue - The previous value of the feature flag
+ * @param currentValue - The current value of the feature flag
+ */
+ private handleInitFeatureFlagChange = (previousValue: boolean, currentValue: boolean) => {
+ if (previousValue === currentValue) {
+ return;
+ }
+
+ if (currentValue) {
+ this.setupExtensionListeners();
+ return;
+ }
+
+ this.removeExtensionListeners();
+ };
+
/**
* Handles the response from the content script with the page details. Triggers an initialization
* of the add login or change password notification if the conditions are met.
@@ -495,11 +513,20 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
* Sets up the listeners for the extension messages and the tab events.
*/
private setupExtensionListeners() {
- BrowserApi.messageListener("overlay-notifications", this.handleExtensionMessage);
+ BrowserApi.addListener(chrome.runtime.onMessage, this.handleExtensionMessage);
chrome.tabs.onRemoved.addListener(this.handleTabRemoved);
chrome.tabs.onUpdated.addListener(this.handleTabUpdated);
}
+ /**
+ * Removes the listeners for the extension messages and the tab events.
+ */
+ private removeExtensionListeners() {
+ BrowserApi.removeListener(chrome.runtime.onMessage, this.handleExtensionMessage);
+ chrome.tabs.onRemoved.removeListener(this.handleTabRemoved);
+ chrome.tabs.onUpdated.removeListener(this.handleTabUpdated);
+ }
+
/**
* Handles messages that are sent to the extension background.
*
From a5c1a5a42f054aa83364db1d9eb905e9adb7c7ba Mon Sep 17 00:00:00 2001
From: Cesar Gonzalez
Date: Tue, 8 Oct 2024 16:02:49 -0500
Subject: [PATCH 14/25] [PM-12548] Fido2 scripts should not load when user is
logged out (#11444)
* [PM-12548] Fido2 scripts should not load when user is logged out
* [PM-12548] Fido2 scripts should not load when user is logged out
---
.../autofill/background/overlay.background.ts | 4 +-
.../abstractions/fido2.background.ts | 1 -
.../fido2/background/fido2.background.spec.ts | 71 +++++++++----------
.../fido2/background/fido2.background.ts | 43 +++++++++--
.../content/fido2-page-script-append.mv2.ts | 1 +
.../fido2-page-script-delay-append.mv2.ts | 1 +
.../fido2/content/fido2-page-script.ts | 6 ++
.../browser/src/background/main.background.ts | 2 +-
.../src/background/runtime.background.ts | 3 -
9 files changed, 82 insertions(+), 50 deletions(-)
diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts
index 653d31ca52c..b45a4a25485 100644
--- a/apps/browser/src/autofill/background/overlay.background.ts
+++ b/apps/browser/src/autofill/background/overlay.background.ts
@@ -1484,9 +1484,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
}
/**
- * Gets the user's authentication status from the auth service. If the user's authentication
- * status has changed, the inline menu button's authentication status will be updated
- * and the inline menu list's ciphers will be updated.
+ * Gets the user's authentication status from the auth service.
*/
private async getAuthStatus() {
return await firstValueFrom(this.authService.activeAccountStatus$);
diff --git a/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts b/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts
index d77a60d3c7b..6ad069ad56e 100644
--- a/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts
+++ b/apps/browser/src/autofill/fido2/background/abstractions/fido2.background.ts
@@ -45,7 +45,6 @@ type Fido2BackgroundExtensionMessageHandlers = {
interface Fido2Background {
init(): void;
- injectFido2ContentScriptsInAllTabs(): Promise;
}
export {
diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts
index 23d0292e188..99ed4619954 100644
--- a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts
+++ b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts
@@ -1,6 +1,8 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
+import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
+import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
import {
@@ -59,6 +61,8 @@ describe("Fido2Background", () => {
let scriptInjectorServiceMock!: MockProxy;
let configServiceMock!: MockProxy;
let enablePasskeysMock$!: BehaviorSubject;
+ let activeAccountStatusMock$: BehaviorSubject;
+ let authServiceMock!: MockProxy;
let fido2Background!: Fido2Background;
beforeEach(() => {
@@ -81,6 +85,9 @@ describe("Fido2Background", () => {
vaultSettingsService.enablePasskeys$ = enablePasskeysMock$;
fido2ActiveRequestManager = mock();
fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(true);
+ activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
+ authServiceMock = mock();
+ authServiceMock.activeAccountStatus$ = activeAccountStatusMock$;
fido2Background = new Fido2Background(
logService,
fido2ActiveRequestManager,
@@ -88,6 +95,7 @@ describe("Fido2Background", () => {
vaultSettingsService,
scriptInjectorServiceMock,
configServiceMock,
+ authServiceMock,
);
fido2Background["abortManager"] = abortManagerMock;
abortManagerMock.runWithAbortController.mockImplementation((_requestId, runner) =>
@@ -101,55 +109,31 @@ describe("Fido2Background", () => {
jest.clearAllMocks();
});
- describe("injectFido2ContentScriptsInAllTabs", () => {
- it("does not inject any FIDO2 content scripts when no tabs have a secure url protocol", async () => {
- const insecureTab = mock({ id: 789, url: "http://example.com" });
- tabsQuerySpy.mockResolvedValueOnce([insecureTab]);
+ describe("handleAuthStatusUpdate", () => {
+ let updateContentScriptRegistrationSpy: jest.SpyInstance;
- await fido2Background.injectFido2ContentScriptsInAllTabs();
-
- expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled();
+ beforeEach(() => {
+ updateContentScriptRegistrationSpy = jest
+ .spyOn(fido2Background as any, "updateContentScriptRegistration")
+ .mockImplementation();
});
- it("only injects the FIDO2 content script into tabs that contain a secure url protocol", async () => {
- const secondTabMock = mock({ id: 456, url: "https://example.com" });
- const insecureTab = mock({ id: 789, url: "http://example.com" });
- const noUrlTab = mock({ id: 101, url: undefined });
- tabsQuerySpy.mockResolvedValueOnce([tabMock, secondTabMock, insecureTab, noUrlTab]);
+ it("skips triggering the passkeys settings update if the user is logged out", async () => {
+ activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut);
- await fido2Background.injectFido2ContentScriptsInAllTabs();
+ fido2Background.init();
await flushPromises();
- expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({
- tabId: tabMock.id,
- injectDetails: contentScriptDetails,
- });
- expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({
- tabId: secondTabMock.id,
- injectDetails: contentScriptDetails,
- });
- expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalledWith({
- tabId: insecureTab.id,
- injectDetails: contentScriptDetails,
- });
- expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalledWith({
- tabId: noUrlTab.id,
- injectDetails: contentScriptDetails,
- });
+ expect(updateContentScriptRegistrationSpy).not.toHaveBeenCalled();
});
- it("injects the `page-script.js` content script into the provided tab", async () => {
- tabsQuerySpy.mockResolvedValueOnce([tabMock]);
+ it("triggers the passkeys setting update if the user is logged in", async () => {
+ activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
- await fido2Background.injectFido2ContentScriptsInAllTabs();
+ fido2Background.init();
await flushPromises();
- expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({
- tabId: tabMock.id,
- injectDetails: sharedScriptInjectionDetails,
- mv2Details: { file: Fido2ContentScript.PageScriptAppend },
- mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" },
- });
+ expect(updateContentScriptRegistrationSpy).toHaveBeenCalled();
});
});
@@ -157,6 +141,7 @@ describe("Fido2Background", () => {
let portMock!: MockProxy;
beforeEach(() => {
+ jest.spyOn(fido2Background as any, "handleAuthStatusUpdate").mockImplementation();
fido2Background.init();
jest.spyOn(BrowserApi, "registerContentScriptsMv2");
jest.spyOn(BrowserApi, "registerContentScriptsMv3");
@@ -168,6 +153,15 @@ describe("Fido2Background", () => {
tabsQuerySpy.mockResolvedValue([tabMock]);
});
+ it("skips handling the passkey update if the user is logged out", async () => {
+ activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut);
+
+ enablePasskeysMock$.next(true);
+
+ expect(portMock.disconnect).not.toHaveBeenCalled();
+ expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled();
+ });
+
it("does not destroy and re-inject the content scripts when triggering `handleEnablePasskeysUpdate` with an undefined currentEnablePasskeysSetting property", async () => {
await flushPromises();
@@ -421,6 +415,7 @@ describe("Fido2Background", () => {
let portMock!: MockProxy;
beforeEach(() => {
+ jest.spyOn(fido2Background as any, "handleAuthStatusUpdate").mockImplementation();
fido2Background.init();
portMock = createPortSpyMock(Fido2PortName.InjectedScript);
triggerRuntimeOnConnectEvent(portMock);
diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.ts b/apps/browser/src/autofill/fido2/background/fido2.background.ts
index a9d1b314770..810cdf74657 100644
--- a/apps/browser/src/autofill/fido2/background/fido2.background.ts
+++ b/apps/browser/src/autofill/fido2/background/fido2.background.ts
@@ -1,6 +1,8 @@
-import { firstValueFrom, startWith } from "rxjs";
+import { firstValueFrom, startWith, Subscription } from "rxjs";
import { pairwise } from "rxjs/operators";
+import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
+import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
@@ -29,6 +31,7 @@ import {
} from "./abstractions/fido2.background";
export class Fido2Background implements Fido2BackgroundInterface {
+ private currentAuthStatus$: Subscription;
private abortManager = new AbortManager();
private fido2ContentScriptPortsSet = new Set();
private registeredContentScripts: browser.contentScripts.RegisteredContentScript;
@@ -55,6 +58,7 @@ export class Fido2Background implements Fido2BackgroundInterface {
private vaultSettingsService: VaultSettingsService,
private scriptInjectorService: ScriptInjectorService,
private configService: ConfigService,
+ private authService: AuthService,
) {}
/**
@@ -68,12 +72,32 @@ export class Fido2Background implements Fido2BackgroundInterface {
this.vaultSettingsService.enablePasskeys$
.pipe(startWith(undefined), pairwise())
.subscribe(([previous, current]) => this.handleEnablePasskeysUpdate(previous, current));
+ this.currentAuthStatus$ = this.authService.activeAccountStatus$
+ .pipe(startWith(undefined), pairwise())
+ .subscribe(([_previous, current]) => this.handleAuthStatusUpdate(current));
+ }
+
+ /**
+ * Handles initializing the FIDO2 content scripts based on the current
+ * authentication status. We only want to inject the FIDO2 content scripts
+ * if the user is logged in.
+ *
+ * @param authStatus - The current authentication status.
+ */
+ private async handleAuthStatusUpdate(authStatus: AuthenticationStatus) {
+ if (authStatus === AuthenticationStatus.LoggedOut) {
+ return;
+ }
+
+ const enablePasskeys = await this.isPasskeySettingEnabled();
+ await this.handleEnablePasskeysUpdate(enablePasskeys, enablePasskeys);
+ this.currentAuthStatus$.unsubscribe();
}
/**
* Injects the FIDO2 content and page script into all existing browser tabs.
*/
- async injectFido2ContentScriptsInAllTabs() {
+ private async injectFido2ContentScriptsInAllTabs() {
const tabs = await BrowserApi.tabsQuery({});
for (let index = 0; index < tabs.length; index++) {
@@ -85,6 +109,13 @@ export class Fido2Background implements Fido2BackgroundInterface {
}
}
+ /**
+ * Gets the user's authentication status from the auth service.
+ */
+ private async getAuthStatus() {
+ return await firstValueFrom(this.authService.activeAccountStatus$);
+ }
+
/**
* Handles reacting to the enablePasskeys setting being updated. If the setting
* is enabled, the FIDO2 content scripts are injected into all tabs. If the setting
@@ -98,13 +129,17 @@ export class Fido2Background implements Fido2BackgroundInterface {
previousEnablePasskeysSetting: boolean,
enablePasskeys: boolean,
) {
- this.fido2ActiveRequestManager.removeAllActiveRequests();
- await this.updateContentScriptRegistration();
+ if ((await this.getAuthStatus()) === AuthenticationStatus.LoggedOut) {
+ return;
+ }
if (previousEnablePasskeysSetting === undefined) {
return;
}
+ this.fido2ActiveRequestManager.removeAllActiveRequests();
+ await this.updateContentScriptRegistration();
+
this.destroyLoadedFido2ContentScripts();
if (enablePasskeys) {
void this.injectFido2ContentScriptsInAllTabs();
diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts
index dd5f33dffb0..f835d2f175b 100644
--- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts
+++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.ts
@@ -9,6 +9,7 @@
const script = globalContext.document.createElement("script");
script.src = chrome.runtime.getURL("content/fido2-page-script.js");
+ script.async = false;
const scriptInsertionPoint =
globalContext.document.head || globalContext.document.documentElement;
diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts
index 2ada31fdfe2..775bc76266d 100644
--- a/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts
+++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-delay-append.mv2.ts
@@ -9,6 +9,7 @@
const script = globalContext.document.createElement("script");
script.src = chrome.runtime.getURL("content/fido2-page-script.js");
+ script.async = false;
// We are ensuring that the script injection is delayed in the event that we are loading
// within an iframe element. This prevents an issue with web mail clients that load content
diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts
index c44c263dd23..dfc2bba681a 100644
--- a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts
+++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts
@@ -4,6 +4,12 @@ import { MessageType } from "./messaging/message";
import { Messenger } from "./messaging/messenger";
(function (globalContext) {
+ if (globalContext.document.currentScript) {
+ globalContext.document.currentScript.parentNode.removeChild(
+ globalContext.document.currentScript,
+ );
+ }
+
const shouldExecuteContentScript =
globalContext.document.contentType === "text/html" &&
(globalContext.document.location.protocol === "https:" ||
diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts
index 85e1ee19b07..318b856b324 100644
--- a/apps/browser/src/background/main.background.ts
+++ b/apps/browser/src/background/main.background.ts
@@ -1103,6 +1103,7 @@ export default class MainBackground {
this.vaultSettingsService,
this.scriptInjectorService,
this.configService,
+ this.authService,
);
const lockService = new DefaultLockService(this.accountService, this.vaultTimeoutService);
@@ -1118,7 +1119,6 @@ export default class MainBackground {
this.messagingService,
this.logService,
this.configService,
- this.fido2Background,
messageListener,
this.accountService,
lockService,
diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts
index 3e0933942b5..2bc2eadf261 100644
--- a/apps/browser/src/background/runtime.background.ts
+++ b/apps/browser/src/background/runtime.background.ts
@@ -21,7 +21,6 @@ import {
openTwoFactorAuthPopout,
} from "../auth/popup/utils/auth-popout-window";
import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background";
-import { Fido2Background } from "../autofill/fido2/background/abstractions/fido2.background";
import { AutofillService } from "../autofill/services/abstractions/autofill.service";
import { BrowserApi } from "../platform/browser/browser-api";
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
@@ -46,7 +45,6 @@ export default class RuntimeBackground {
private messagingService: MessagingService,
private logService: LogService,
private configService: ConfigService,
- private fido2Background: Fido2Background,
private messageListener: MessageListener,
private accountService: AccountService,
private readonly lockService: LockService,
@@ -365,7 +363,6 @@ export default class RuntimeBackground {
private async checkOnInstalled() {
setTimeout(async () => {
- void this.fido2Background.injectFido2ContentScriptsInAllTabs();
void this.autofillService.loadAutofillScriptsOnInstall();
if (this.onInstalledReason != null) {
From c6169432bb1c157a6ca67ed3c8d40406c0d4bc73 Mon Sep 17 00:00:00 2001
From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Date: Tue, 8 Oct 2024 16:41:28 -0500
Subject: [PATCH 15/25] account for adding a new cipher in the admin console
(#11469)
- When adding a new cipher, the `editCipher` method is called but without a given cipher
---
.../web/src/app/vault/org-vault/vault.component.ts | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts
index 3840d5ec6aa..94bb6011dc7 100644
--- a/apps/web/src/app/vault/org-vault/vault.component.ts
+++ b/apps/web/src/app/vault/org-vault/vault.component.ts
@@ -788,8 +788,8 @@ export class VaultComponent implements OnInit, OnDestroy {
}
/**
- * Edit the given cipher
- * @param cipherView - The cipher to be edited
+ * Edit the given cipher or add a new cipher
+ * @param cipherView - When set, the cipher to be edited
* @param cloneCipher - `true` when the cipher should be cloned.
* Used in place of the `additionalComponentParameters`, as
* the `editCipherIdV2` method has a differing implementation.
@@ -797,7 +797,7 @@ export class VaultComponent implements OnInit, OnDestroy {
* the `AddEditComponent` to edit methods directly.
*/
async editCipher(
- cipher: CipherView,
+ cipher: CipherView | null,
cloneCipher: boolean,
additionalComponentParameters?: (comp: AddEditComponent) => void,
) {
@@ -805,7 +805,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async editCipherId(
- cipher: CipherView,
+ cipher: CipherView | null,
cloneCipher: boolean,
additionalComponentParameters?: (comp: AddEditComponent) => void,
) {
@@ -827,7 +827,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const defaultComponentParameters = (comp: AddEditComponent) => {
comp.organization = this.organization;
comp.organizationId = this.organization.id;
- comp.cipherId = cipher.id;
+ comp.cipherId = cipher?.id;
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
@@ -866,10 +866,10 @@ export class VaultComponent implements OnInit, OnDestroy {
* Edit a cipher using the new AddEditCipherDialogV2 component.
* Only to be used behind the ExtensionRefresh feature flag.
*/
- private async editCipherIdV2(cipher: CipherView, cloneCipher: boolean) {
+ private async editCipherIdV2(cipher: CipherView | null, cloneCipher: boolean) {
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
cloneCipher ? "clone" : "edit",
- cipher.id as CipherId,
+ cipher?.id as CipherId | null,
);
await this.openVaultItemDialog("form", cipherFormConfig, cipher);
From 4ce6df304097f69d719814a0bc01495031eae391 Mon Sep 17 00:00:00 2001
From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com>
Date: Wed, 9 Oct 2024 04:28:39 -0700
Subject: [PATCH 16/25] [PM-11926] - fix send created page text (#11467)
* fix send created page
* remove unused i18n key
* remove superfluous spec
* fix failing tests
---
apps/browser/src/_locales/en/messages.json | 20 +++-
.../send-created/send-created.component.html | 4 +-
.../send-created.component.spec.ts | 106 +++++++++++++-----
.../send-created/send-created.component.ts | 19 +++-
4 files changed, 117 insertions(+), 32 deletions(-)
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 7b30e5aa334..e52f78583d4 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -2498,7 +2498,25 @@
"message": "Send created successfully!",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
- "sendAvailability": {
+ "sendExpiresInHoursSingle": {
+ "message": "The Send will be available to anyone with the link for the next 1 hour.",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "sendExpiresInHours": {
+ "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
+ "placeholders": {
+ "hours": {
+ "content": "$1",
+ "example": "5"
+ }
+ }
+ },
+ "sendExpiresInDaysSingle": {
+ "message": "The Send will be available to anyone with the link for the next 1 day.",
+ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
+ },
+ "sendExpiresInDays": {
"message": "The Send will be available to anyone with the link for the next $DAYS$ days.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
"placeholders": {
diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html
index c97d3da1396..7c65cbeb17d 100644
--- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html
+++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html
@@ -16,7 +16,9 @@
>
{{ "createdSendSuccessfully" | i18n }}
- {{ "sendAvailability" | i18n: daysAvailable }}
+
+ {{ formatExpirationDate() }}
+
diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts
index bcc4d2e2ccb..24186ad4275 100644
--- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts
+++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts
@@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { MockProxy, mock } from "jest-mock-extended";
-import { of } from "rxjs";
+import { BehaviorSubject, of } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -13,7 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
-import { ButtonModule, IconModule, ToastService } from "@bitwarden/components";
+import { ButtonModule, I18nMockService, IconModule, ToastService } from "@bitwarden/components";
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
@@ -26,7 +26,6 @@ import { SendCreatedComponent } from "./send-created.component";
describe("SendCreatedComponent", () => {
let component: SendCreatedComponent;
let fixture: ComponentFixture;
- let i18nService: MockProxy;
let platformUtilsService: MockProxy;
let sendService: MockProxy;
let toastService: MockProxy;
@@ -36,17 +35,10 @@ describe("SendCreatedComponent", () => {
let router: MockProxy;
const sendId = "test-send-id";
- const deletionDate = new Date();
- deletionDate.setDate(deletionDate.getDate() + 7);
- const sendView: SendView = {
- id: sendId,
- deletionDate,
- accessId: "abc",
- urlB64Key: "123",
- } as SendView;
+ let sendView: SendView;
+ let sendViewsSubject: BehaviorSubject;
beforeEach(async () => {
- i18nService = mock();
platformUtilsService = mock();
sendService = mock();
toastService = mock();
@@ -54,6 +46,17 @@ describe("SendCreatedComponent", () => {
activatedRoute = mock();
environmentService = mock();
router = mock();
+
+ sendView = {
+ id: sendId,
+ deletionDate: new Date(),
+ accessId: "abc",
+ urlB64Key: "123",
+ } as SendView;
+
+ sendViewsSubject = new BehaviorSubject([sendView]);
+ sendService.sendViews$ = sendViewsSubject.asObservable();
+
Object.defineProperty(environmentService, "environment$", {
configurable: true,
get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })),
@@ -65,8 +68,6 @@ describe("SendCreatedComponent", () => {
},
} as any;
- sendService.sendViews$ = of([sendView]);
-
await TestBed.configureTestingModule({
imports: [
CommonModule,
@@ -82,7 +83,25 @@ describe("SendCreatedComponent", () => {
SendCreatedComponent,
],
providers: [
- { provide: I18nService, useValue: i18nService },
+ {
+ provide: I18nService,
+ useFactory: () => {
+ return new I18nMockService({
+ back: "back",
+ loading: "loading",
+ copyLink: "copyLink",
+ close: "close",
+ createdSend: "createdSend",
+ createdSendSuccessfully: "createdSendSuccessfully",
+ popOutNewWindow: "popOutNewWindow",
+ sendExpiresInHours: (hours) => `sendExpiresInHours ${hours}`,
+ sendExpiresInHoursSingle: "sendExpiresInHoursSingle",
+ sendExpiresInDays: (days) => `sendExpiresInDays ${days}`,
+ sendExpiresInDaysSingle: "sendExpiresInDaysSingle",
+ sendLinkCopied: "sendLinkCopied",
+ });
+ },
+ },
{ provide: PlatformUtilsService, useValue: platformUtilsService },
{ provide: SendService, useValue: sendService },
{ provide: ToastService, useValue: toastService },
@@ -94,40 +113,73 @@ describe("SendCreatedComponent", () => {
{ provide: Router, useValue: router },
],
}).compileComponents();
- });
- beforeEach(() => {
fixture = TestBed.createComponent(SendCreatedComponent);
component = fixture.componentInstance;
+ fixture.detectChanges();
});
it("should create", () => {
- fixture.detectChanges();
expect(component).toBeTruthy();
});
- it("should initialize send and daysAvailable", () => {
- fixture.detectChanges();
+ it("should initialize send, daysAvailable, and hoursAvailable", () => {
expect(component["send"]).toBe(sendView);
- expect(component["daysAvailable"]).toBe(7);
+ expect(component["daysAvailable"]).toBe(0);
+ expect(component["hoursAvailable"]).toBe(0);
});
it("should navigate back to send list on close", async () => {
- fixture.detectChanges();
await component.close();
expect(router.navigate).toHaveBeenCalledWith(["/tabs/send"]);
});
- describe("getDaysAvailable", () => {
- it("returns the correct number of days", () => {
+ describe("getHoursAvailable", () => {
+ it("returns the correct number of hours", () => {
+ sendView.deletionDate.setDate(sendView.deletionDate.getDate() + 7);
+ sendViewsSubject.next([sendView]);
fixture.detectChanges();
- expect(component.getDaysAvailable(sendView)).toBe(7);
+
+ expect(component.getHoursAvailable(sendView)).toBeCloseTo(168, 0);
+ });
+ });
+
+ describe("formatExpirationDate", () => {
+ it("returns days plural if expiry is more than 24 hours", () => {
+ sendView.deletionDate.setDate(sendView.deletionDate.getDate() + 7);
+ sendViewsSubject.next([sendView]);
+ fixture.detectChanges();
+
+ expect(component.formatExpirationDate()).toBe("sendExpiresInDays 7");
+ });
+
+ it("returns days singular if expiry is 24 hours", () => {
+ sendView.deletionDate.setDate(sendView.deletionDate.getDate() + 1);
+ sendViewsSubject.next([sendView]);
+ fixture.detectChanges();
+
+ expect(component.formatExpirationDate()).toBe("sendExpiresInDaysSingle");
+ });
+
+ it("returns hours plural if expiry is more than 1 hour but less than 24", () => {
+ sendView.deletionDate.setHours(sendView.deletionDate.getHours() + 2);
+ sendViewsSubject.next([sendView]);
+ fixture.detectChanges();
+
+ expect(component.formatExpirationDate()).toBe("sendExpiresInHours 2");
+ });
+
+ it("returns hours singular if expiry is in 1 hour", () => {
+ sendView.deletionDate.setHours(sendView.deletionDate.getHours() + 1);
+ sendViewsSubject.next([sendView]);
+ fixture.detectChanges();
+
+ expect(component.formatExpirationDate()).toBe("sendExpiresInHoursSingle");
});
});
describe("copyLink", () => {
it("should copy link and show toast", async () => {
- fixture.detectChanges();
const link = "https://example.com/#/send/abc/123";
await component.copyLink();
@@ -136,7 +188,7 @@ describe("SendCreatedComponent", () => {
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "success",
title: null,
- message: i18nService.t("sendLinkCopied"),
+ message: "sendLinkCopied",
});
});
});
diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts
index 4ed4da2f81d..ae66d14d3f0 100644
--- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts
+++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts
@@ -39,6 +39,7 @@ export class SendCreatedComponent {
protected sendCreatedIcon = SendCreatedIcon;
protected send: SendView;
protected daysAvailable = 0;
+ protected hoursAvailable = 0;
constructor(
private i18nService: I18nService,
@@ -54,14 +55,26 @@ export class SendCreatedComponent {
this.sendService.sendViews$.pipe(takeUntilDestroyed()).subscribe((sendViews) => {
this.send = sendViews.find((s) => s.id === sendId);
if (this.send) {
- this.daysAvailable = this.getDaysAvailable(this.send);
+ this.hoursAvailable = this.getHoursAvailable(this.send);
+ this.daysAvailable = Math.ceil(this.hoursAvailable / 24);
}
});
}
- getDaysAvailable(send: SendView): number {
+ formatExpirationDate(): string {
+ if (this.hoursAvailable < 24) {
+ return this.hoursAvailable === 1
+ ? this.i18nService.t("sendExpiresInHoursSingle")
+ : this.i18nService.t("sendExpiresInHours", this.hoursAvailable);
+ }
+ return this.daysAvailable === 1
+ ? this.i18nService.t("sendExpiresInDaysSingle")
+ : this.i18nService.t("sendExpiresInDays", this.daysAvailable);
+ }
+
+ getHoursAvailable(send: SendView): number {
const now = new Date().getTime();
- return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60 * 24)));
+ return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60)));
}
async close() {
From f1dab68e467bb866694a8bec2a60806b69bdca0d Mon Sep 17 00:00:00 2001
From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com>
Date: Wed, 9 Oct 2024 06:00:05 -0700
Subject: [PATCH 17/25] don't change password field label (#11267)
---
.../send-form/components/options/send-options.component.html | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
index 29235401de2..b8458fa9c12 100644
--- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
+++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
@@ -12,8 +12,7 @@
>
- {{ "password" | i18n }}
- {{ "newPassword" | i18n }}
+ {{ "password" | i18n }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts
index ce605a6f5aa..46d0c550947 100644
--- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts
+++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts
@@ -1,16 +1,9 @@
-import {
- Component,
- EventEmitter,
- Input,
- OnDestroy,
- OnInit,
- Output,
- ViewChild,
-} from "@angular/core";
+import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
+import { Component, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
+import { FormBuilder, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
-import zxcvbn from "zxcvbn";
-import { PasswordStrengthComponent } from "@bitwarden/angular/tools/password-strength/password-strength.component";
+import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -22,27 +15,60 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
import { OrganizationUserResetPasswordService } from "../services/organization-user-reset-password/organization-user-reset-password.service";
+/**
+ * Encapsulates a few key data inputs needed to initiate an account recovery
+ * process for the organization user in question.
+ */
+export type ResetPasswordDialogData = {
+ /**
+ * The organization user's full name
+ */
+ name: string;
+
+ /**
+ * The organization user's email address
+ */
+ email: string;
+
+ /**
+ * The `organizationUserId` for the user
+ */
+ id: string;
+
+ /**
+ * The organization's `organizationId`
+ */
+ organizationId: string;
+};
+
+export enum ResetPasswordDialogResult {
+ Ok = "ok",
+}
+
@Component({
selector: "app-reset-password",
templateUrl: "reset-password.component.html",
})
+/**
+ * Used in a dialog for initiating the account recovery process against a
+ * given organization user. An admin will access this form when they want to
+ * reset a user's password and log them out of sessions.
+ */
export class ResetPasswordComponent implements OnInit, OnDestroy {
- @Input() name: string;
- @Input() email: string;
- @Input() id: string;
- @Input() organizationId: string;
- @Output() passwordReset = new EventEmitter();
- @ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
+ formGroup = this.formBuilder.group({
+ newPassword: ["", Validators.required],
+ });
+
+ @ViewChild(PasswordStrengthV2Component) passwordStrengthComponent: PasswordStrengthV2Component;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
- newPassword: string = null;
showPassword = false;
- passwordStrengthResult: zxcvbn.ZXCVBNResult;
- formPromise: Promise;
+ passwordStrengthScore: number;
private destroy$ = new Subject();
constructor(
+ @Inject(DIALOG_DATA) protected data: ResetPasswordDialogData,
private resetPasswordService: OrganizationUserResetPasswordService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
@@ -51,6 +77,8 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
private logService: LogService,
private dialogService: DialogService,
private toastService: ToastService,
+ private formBuilder: FormBuilder,
+ private dialogRef: DialogRef,
) {}
async ngOnInit() {
@@ -69,13 +97,15 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
}
get loggedOutWarningName() {
- return this.name != null ? this.name : this.i18nService.t("thisUser");
+ return this.data.name != null ? this.data.name : this.i18nService.t("thisUser");
}
async generatePassword() {
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
- this.newPassword = await this.passwordGenerationService.generatePassword(options);
- this.passwordStrengthComponent.updatePasswordStrength(this.newPassword);
+ this.formGroup.patchValue({
+ newPassword: await this.passwordGenerationService.generatePassword(options),
+ });
+ this.passwordStrengthComponent.updatePasswordStrength(this.formGroup.value.newPassword);
}
togglePassword() {
@@ -83,7 +113,8 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
document.getElementById("newPassword").focus();
}
- copy(value: string) {
+ copy() {
+ const value = this.formGroup.value.newPassword;
if (value == null) {
return;
}
@@ -96,9 +127,9 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
});
}
- async submit() {
+ submit = async () => {
// Validation
- if (this.newPassword == null || this.newPassword === "") {
+ if (this.formGroup.value.newPassword == null || this.formGroup.value.newPassword === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
@@ -107,7 +138,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
return false;
}
- if (this.newPassword.length < Utils.minimumPasswordLength) {
+ if (this.formGroup.value.newPassword.length < Utils.minimumPasswordLength) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
@@ -119,8 +150,8 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
if (
this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
- this.passwordStrengthResult.score,
- this.newPassword,
+ this.passwordStrengthScore,
+ this.formGroup.value.newPassword,
this.enforcedPolicyOptions,
)
) {
@@ -132,7 +163,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
return;
}
- if (this.passwordStrengthResult.score < 3) {
+ if (this.passwordStrengthScore < 3) {
const result = await this.dialogService.openSimpleDialog({
title: { key: "weakMasterPassword" },
content: { key: "weakMasterPasswordDesc" },
@@ -145,26 +176,29 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
}
try {
- this.formPromise = this.resetPasswordService.resetMasterPassword(
- this.newPassword,
- this.email,
- this.id,
- this.organizationId,
+ await this.resetPasswordService.resetMasterPassword(
+ this.formGroup.value.newPassword,
+ this.data.email,
+ this.data.id,
+ this.data.organizationId,
);
- await this.formPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("resetPasswordSuccess"),
});
- this.passwordReset.emit();
} catch (e) {
this.logService.error(e);
}
- this.formPromise = null;
+
+ this.dialogRef.close(ResetPasswordDialogResult.Ok);
+ };
+
+ getStrengthScore(result: number) {
+ this.passwordStrengthScore = result;
}
- getStrengthResult(result: zxcvbn.ZXCVBNResult) {
- this.passwordStrengthResult = result;
- }
+ static open = (dialogService: DialogService, input: DialogConfig) => {
+ return dialogService.open(ResetPasswordComponent, input);
+ };
}
diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts
index 698c260632d..3cc73c84a97 100644
--- a/apps/web/src/app/admin-console/organizations/members/members.component.ts
+++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts
@@ -70,7 +70,10 @@ import {
MemberDialogTab,
openUserAddEditDialog,
} from "./components/member-dialog";
-import { ResetPasswordComponent } from "./components/reset-password.component";
+import {
+ ResetPasswordComponent,
+ ResetPasswordDialogResult,
+} from "./components/reset-password.component";
class MembersTableDataSource extends PeopleTableDataSource {
protected statusType = OrganizationUserStatusType;
@@ -663,24 +666,19 @@ export class MembersComponent extends BaseMembersComponent
}
async resetPassword(user: OrganizationUserView) {
- const [modal] = await this.modalService.openViewRef(
- ResetPasswordComponent,
- this.resetPasswordModalRef,
- (comp) => {
- comp.name = this.userNamePipe.transform(user);
- comp.email = user != null ? user.email : null;
- comp.organizationId = this.organization.id;
- comp.id = user != null ? user.id : null;
-
- // eslint-disable-next-line rxjs-angular/prefer-takeuntil
- comp.passwordReset.subscribe(() => {
- modal.close();
- // 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.load();
- });
+ const dialogRef = ResetPasswordComponent.open(this.dialogService, {
+ data: {
+ name: this.userNamePipe.transform(user),
+ email: user != null ? user.email : null,
+ organizationId: this.organization.id,
+ id: user != null ? user.id : null,
},
- );
+ });
+
+ const result = await lastValueFrom(dialogRef.closed);
+ if (result === ResetPasswordDialogResult.Ok) {
+ await this.load();
+ }
}
protected async removeUserConfirmationDialog(user: OrganizationUserView) {
diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts
index 6e8870a675f..d849b1f1f3c 100644
--- a/apps/web/src/app/admin-console/organizations/members/members.module.ts
+++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts
@@ -1,6 +1,7 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { NgModule } from "@angular/core";
+import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
import { LooseComponentsModule } from "../../../shared";
@@ -24,6 +25,7 @@ import { MembersComponent } from "./members.component";
UserDialogModule,
PasswordCalloutComponent,
ScrollingModule,
+ PasswordStrengthV2Component,
],
declarations: [
BulkConfirmComponent,
From e2275ad0bc138bf2fd0c2577206f488a756839b5 Mon Sep 17 00:00:00 2001
From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Date: Wed, 9 Oct 2024 12:05:31 -0500
Subject: [PATCH 21/25] [PM-12776] Draggable Items within a dialog (#11396)
* increase z-index `cdk-drag-preview` so it displays on top of modals/dialogs when draggable elements are within them
* update comment overlay z-index and add ticket number
* update comment
---
libs/components/src/tw-theme.css | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css
index 6e5bb32edad..7214ae73ef8 100644
--- a/libs/components/src/tw-theme.css
+++ b/libs/components/src/tw-theme.css
@@ -1,9 +1,9 @@
@import "./reset.css";
-/**
- Note that the value of the *-600 colors is currently equivalent to the value
+/**
+ Note that the value of the *-600 colors is currently equivalent to the value
of the *-500 variant of that color. This is a temporary change to make BW-42
- updates easier.
+ updates easier.
TODO remove comment when the color palette portion of BW-42 is completed.
*/
@@ -196,7 +196,7 @@
@import "./toast/toast.tokens.css";
@import "./toast/toastr.css";
-/**
+/**
* tw-break-words does not work with table cells:
* https://github.com/tailwindlabs/tailwindcss/issues/835
*/
@@ -204,7 +204,7 @@ td.tw-break-words {
overflow-wrap: anywhere;
}
-/**
+/**
* tw-list-none hides summary arrow in Firefox & Chrome but not Safari:
* https://github.com/tailwindlabs/tailwindcss/issues/924#issuecomment-915509785
*/
@@ -213,7 +213,7 @@ summary.tw-list-none::-webkit-details-marker {
display: none;
}
-/**
+/**
* Arbitrary values can't be used with `text-align`:
* https://github.com/tailwindlabs/tailwindcss/issues/802#issuecomment-849013311
*/
@@ -222,10 +222,11 @@ summary.tw-list-none::-webkit-details-marker {
}
/**
- * Bootstrap uses z-index: 1050 for modals, dialogs should appear above them.
- * Remove once bootstrap is removed from our codebase.
- * CL-XYZ
+ * Bootstrap uses z-index: 1050 for modals, dialogs and drag-and-drop previews should appear above them.
+ * When bootstrap is removed, test if these styles are still needed and that overlays display properly over other content.
+ * CL-483
*/
+.cdk-drag-preview,
.cdk-overlay-container,
.cdk-global-overlay-wrapper,
.cdk-overlay-connected-position-bounding-box,
From 1ca03e781531fbfd2d9f8b0c0de7c7f5888a7370 Mon Sep 17 00:00:00 2001
From: Jared McCannon
Date: Wed, 9 Oct 2024 14:30:39 -0500
Subject: [PATCH 22/25] [PM-12357] - Using new Verified SSO Domain call for SSO
(#11446)
* Added new SSO verified domain call and added calling of it behind feature flag.
---
apps/web/src/app/auth/sso.component.ts | 26 +++++++++++----
.../org-domain-api.service.abstraction.ts | 6 ++++
...rganization-domain-sso-details.response.ts | 15 +++++++++
.../org-domain-api.service.spec.ts | 33 +++++++++++++++++++
.../org-domain-api.service.ts | 15 +++++++++
libs/common/src/enums/feature-flag.enum.ts | 2 ++
6 files changed, 91 insertions(+), 6 deletions(-)
create mode 100644 libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts
diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts
index e498384c278..019ab5e5ac4 100644
--- a/apps/web/src/app/auth/sso.component.ts
+++ b/apps/web/src/app/auth/sso.component.ts
@@ -12,11 +12,14 @@ import {
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction";
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response";
+import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { HttpStatusCode } from "@bitwarden/common/enums";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
+import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@@ -107,13 +110,24 @@ export class SsoComponent extends BaseSsoComponent implements OnInit {
// show loading spinner
this.loggingIn = true;
try {
- const response: OrganizationDomainSsoDetailsResponse =
- await this.orgDomainApiService.getClaimedOrgDomainByEmail(qParams.email);
+ if (await this.configService.getFeatureFlag(FeatureFlag.VerifiedSsoDomainEndpoint)) {
+ const response: ListResponse =
+ await this.orgDomainApiService.getVerifiedOrgDomainsByEmail(qParams.email);
- if (response?.ssoAvailable && response?.verifiedDate) {
- this.identifierFormControl.setValue(response.organizationIdentifier);
- await this.submit();
- return;
+ if (response.data.length > 0) {
+ this.identifierFormControl.setValue(response.data[0].organizationIdentifier);
+ await this.submit();
+ return;
+ }
+ } else {
+ const response: OrganizationDomainSsoDetailsResponse =
+ await this.orgDomainApiService.getClaimedOrgDomainByEmail(qParams.email);
+
+ if (response?.ssoAvailable && response?.verifiedDate) {
+ this.identifierFormControl.setValue(response.organizationIdentifier);
+ await this.submit();
+ return;
+ }
}
} catch (error) {
this.handleGetClaimedDomainByEmailError(error);
diff --git a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts
index 5486250279b..d7783cfe1c9 100644
--- a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts
+++ b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts
@@ -1,7 +1,10 @@
+import { ListResponse } from "@bitwarden/common/models/response/list.response";
+
import { OrganizationDomainRequest } from "../../services/organization-domain/requests/organization-domain.request";
import { OrganizationDomainSsoDetailsResponse } from "./responses/organization-domain-sso-details.response";
import { OrganizationDomainResponse } from "./responses/organization-domain.response";
+import { VerifiedOrganizationDomainSsoDetailsResponse } from "./responses/verified-organization-domain-sso-details.response";
export abstract class OrgDomainApiServiceAbstraction {
getAllByOrgId: (orgId: string) => Promise>;
@@ -16,4 +19,7 @@ export abstract class OrgDomainApiServiceAbstraction {
verify: (orgId: string, orgDomainId: string) => Promise;
delete: (orgId: string, orgDomainId: string) => Promise;
getClaimedOrgDomainByEmail: (email: string) => Promise;
+ getVerifiedOrgDomainsByEmail: (
+ email: string,
+ ) => Promise>;
}
diff --git a/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts b/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts
new file mode 100644
index 00000000000..c4817306a63
--- /dev/null
+++ b/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts
@@ -0,0 +1,15 @@
+import { BaseResponse } from "@bitwarden/common/models/response/base.response";
+
+export class VerifiedOrganizationDomainSsoDetailsResponse extends BaseResponse {
+ organizationName: string;
+ organizationIdentifier: string;
+ domainName: string;
+
+ constructor(response: any) {
+ super(response);
+
+ this.organizationName = this.getResponseProperty("organizationName");
+ this.organizationIdentifier = this.getResponseProperty("organizationIdentifier");
+ this.domainName = this.getResponseProperty("domainName");
+ }
+}
diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts
index 1b9234b2fc1..7497a77e6f2 100644
--- a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts
+++ b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts
@@ -1,6 +1,9 @@
import { mock } from "jest-mock-extended";
import { lastValueFrom } from "rxjs";
+import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response";
+import { ListResponse } from "@bitwarden/common/models/response/list.response";
+
import { ApiService } from "../../../abstractions/api.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
@@ -81,6 +84,19 @@ const mockedOrganizationDomainSsoDetailsResponse = new OrganizationDomainSsoDeta
mockedOrganizationDomainSsoDetailsServerResponse,
);
+const mockedVerifiedOrganizationDomain = {
+ organizationIdentifier: "fake-org-identifier",
+ organizationName: "fake-org",
+ domainName: "fake-domain-name",
+};
+
+const mockedVerifiedOrganizationDomainSsoResponse =
+ new VerifiedOrganizationDomainSsoDetailsResponse(mockedVerifiedOrganizationDomain);
+
+const mockedVerifiedOrganizationDomainSsoDetailsListResponse = {
+ data: [mockedVerifiedOrganizationDomain],
+} as ListResponse;
+
describe("Org Domain API Service", () => {
let orgDomainApiService: OrgDomainApiService;
@@ -229,4 +245,21 @@ describe("Org Domain API Service", () => {
expect(result).toEqual(mockedOrganizationDomainSsoDetailsResponse);
});
+
+ it("getVerifiedOrgDomainsByEmail should call ApiService.send with correct parameters and return response", async () => {
+ const email = "test@example.com";
+ apiService.send.mockResolvedValue(mockedVerifiedOrganizationDomainSsoDetailsListResponse);
+
+ const result = await orgDomainApiService.getVerifiedOrgDomainsByEmail(email);
+
+ expect(apiService.send).toHaveBeenCalledWith(
+ "POST",
+ "/organizations/domain/sso/verified",
+ new OrganizationDomainSsoDetailsRequest(email),
+ false, //anonymous
+ true,
+ );
+
+ expect(result.data).toContainEqual(mockedVerifiedOrganizationDomainSsoResponse);
+ });
});
diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.ts b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.ts
index 79b39867e2b..1424fad9b9b 100644
--- a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.ts
+++ b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.ts
@@ -4,6 +4,7 @@ import { OrgDomainApiServiceAbstraction } from "../../abstractions/organization-
import { OrgDomainInternalServiceAbstraction } from "../../abstractions/organization-domain/org-domain.service.abstraction";
import { OrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/organization-domain-sso-details.response";
import { OrganizationDomainResponse } from "../../abstractions/organization-domain/responses/organization-domain.response";
+import { VerifiedOrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/verified-organization-domain-sso-details.response";
import { OrganizationDomainSsoDetailsRequest } from "./requests/organization-domain-sso-details.request";
import { OrganizationDomainRequest } from "./requests/organization-domain.request";
@@ -109,4 +110,18 @@ export class OrgDomainApiService implements OrgDomainApiServiceAbstraction {
return response;
}
+
+ async getVerifiedOrgDomainsByEmail(
+ email: string,
+ ): Promise> {
+ const result = await this.apiService.send(
+ "POST",
+ `/organizations/domain/sso/verified`,
+ new OrganizationDomainSsoDetailsRequest(email),
+ false, // anonymous
+ true,
+ );
+
+ return new ListResponse(result, VerifiedOrganizationDomainSsoDetailsResponse);
+ }
}
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index 676acb61575..45b02471f3c 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -31,6 +31,7 @@ export enum FeatureFlag {
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api",
CipherKeyEncryption = "cipher-key-encryption",
+ VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint",
PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader",
Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api",
AccessIntelligence = "pm-13227-access-intelligence",
@@ -75,6 +76,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
[FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE,
[FeatureFlag.CipherKeyEncryption]: FALSE,
+ [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
[FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE,
[FeatureFlag.Pm3478RefactorOrganizationUserApi]: FALSE,
[FeatureFlag.AccessIntelligence]: FALSE,
From 877dfeae7e0567e7b3237718b8934f84fa874c8f Mon Sep 17 00:00:00 2001
From: Cesar Gonzalez
Date: Wed, 9 Oct 2024 18:14:39 -0500
Subject: [PATCH 23/25] [PM-13184] Filter out specific-nodes within TreeWalker
query (#11415)
---
.../autofill/services/dom-query.service.ts | 51 +++++++++++++++++--
1 file changed, 48 insertions(+), 3 deletions(-)
diff --git a/apps/browser/src/autofill/services/dom-query.service.ts b/apps/browser/src/autofill/services/dom-query.service.ts
index 570027b2d12..3ab6c6771cb 100644
--- a/apps/browser/src/autofill/services/dom-query.service.ts
+++ b/apps/browser/src/autofill/services/dom-query.service.ts
@@ -7,6 +7,27 @@ import { DomQueryService as DomQueryServiceInterface } from "./abstractions/dom-
export class DomQueryService implements DomQueryServiceInterface {
private pageContainsShadowDom: boolean;
private useTreeWalkerStrategyFlagSet = true;
+ private ignoredTreeWalkerNodes = new Set([
+ "svg",
+ "script",
+ "noscript",
+ "head",
+ "style",
+ "link",
+ "meta",
+ "title",
+ "base",
+ "img",
+ "picture",
+ "video",
+ "audio",
+ "object",
+ "source",
+ "track",
+ "param",
+ "map",
+ "area",
+ ]);
constructor() {
void this.init();
@@ -21,6 +42,7 @@ export class DomQueryService implements DomQueryServiceInterface {
* @param treeWalkerFilter - The filter callback to use for the treeWalker query
* @param mutationObserver - The MutationObserver to use for observing shadow roots
* @param forceDeepQueryAttempt - Whether to force a deep query attempt
+ * @param ignoredTreeWalkerNodesOverride - An optional set of node names to ignore when using the treeWalker strategy
*/
query(
root: Document | ShadowRoot | Element,
@@ -28,15 +50,28 @@ export class DomQueryService implements DomQueryServiceInterface {
treeWalkerFilter: CallableFunction,
mutationObserver?: MutationObserver,
forceDeepQueryAttempt?: boolean,
+ ignoredTreeWalkerNodesOverride?: Set,
): T[] {
+ const ignoredTreeWalkerNodes = ignoredTreeWalkerNodesOverride || this.ignoredTreeWalkerNodes;
+
if (!forceDeepQueryAttempt && this.pageContainsShadowDomElements()) {
- return this.queryAllTreeWalkerNodes(root, treeWalkerFilter, mutationObserver);
+ return this.queryAllTreeWalkerNodes(
+ root,
+ treeWalkerFilter,
+ ignoredTreeWalkerNodes,
+ mutationObserver,
+ );
}
try {
return this.deepQueryElements(root, queryString, mutationObserver);
} catch {
- return this.queryAllTreeWalkerNodes(root, treeWalkerFilter, mutationObserver);
+ return this.queryAllTreeWalkerNodes(
+ root,
+ treeWalkerFilter,
+ ignoredTreeWalkerNodes,
+ mutationObserver,
+ );
}
}
@@ -207,11 +242,13 @@ export class DomQueryService implements DomQueryServiceInterface {
* and returns a collection of nodes.
* @param rootNode
* @param filterCallback
+ * @param ignoredTreeWalkerNodes
* @param mutationObserver
*/
private queryAllTreeWalkerNodes(
rootNode: Node,
filterCallback: CallableFunction,
+ ignoredTreeWalkerNodes: Set,
mutationObserver?: MutationObserver,
): T[] {
const treeWalkerQueryResults: T[] = [];
@@ -220,6 +257,7 @@ export class DomQueryService implements DomQueryServiceInterface {
rootNode,
treeWalkerQueryResults,
filterCallback,
+ ignoredTreeWalkerNodes,
mutationObserver,
);
@@ -233,15 +271,21 @@ export class DomQueryService implements DomQueryServiceInterface {
* @param rootNode
* @param treeWalkerQueryResults
* @param filterCallback
+ * @param ignoredTreeWalkerNodes
* @param mutationObserver
*/
private buildTreeWalkerNodesQueryResults(
rootNode: Node,
treeWalkerQueryResults: T[],
filterCallback: CallableFunction,
+ ignoredTreeWalkerNodes: Set,
mutationObserver?: MutationObserver,
) {
- const treeWalker = document?.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT);
+ const treeWalker = document?.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT, (node) =>
+ ignoredTreeWalkerNodes.has(node.nodeName?.toLowerCase())
+ ? NodeFilter.FILTER_REJECT
+ : NodeFilter.FILTER_ACCEPT,
+ );
let currentNode = treeWalker?.currentNode;
while (currentNode) {
@@ -263,6 +307,7 @@ export class DomQueryService implements DomQueryServiceInterface {
nodeShadowRoot,
treeWalkerQueryResults,
filterCallback,
+ ignoredTreeWalkerNodes,
mutationObserver,
);
}
From b9be15c84b4ef6d1452db1fc3d283f7fcbbd90b3 Mon Sep 17 00:00:00 2001
From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com>
Date: Thu, 10 Oct 2024 01:28:08 -0700
Subject: [PATCH 24/25] fix password label (#11478)
---
.../send-form/components/options/send-options.component.html | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
index b8458fa9c12..9699f832ed0 100644
--- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
+++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html
@@ -12,7 +12,8 @@
>
- {{ "password" | i18n }}
+ {{ "password" | i18n }}
+ {{ "newPassword" | i18n }}
|