diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 21786339299..c14abd7cd86 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -119,9 +119,9 @@ jobs: run: cargo sort --workspace --check - name: Install cargo-deny - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45 with: - tool: cargo-deny + tool: cargo-deny@0.18.5 - name: Run cargo deny working-directory: ./apps/desktop/desktop_native diff --git a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts index 8de48a49a8e..5e523a1a48d 100644 --- a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts @@ -6,7 +6,7 @@ import { filter, firstValueFrom, fromEvent, - fromEventPattern, + map, merge, Observable, Subject, @@ -28,6 +28,7 @@ import { import { Utils } from "@bitwarden/common/platform/misc/utils"; import { BrowserApi } from "../../../platform/browser/browser-api"; +import { fromChromeEvent } from "../../../platform/browser/from-chrome-event"; // FIXME (PM-22628): Popup imports are forbidden in background // eslint-disable-next-line no-restricted-imports import { closeFido2Popout, openFido2Popout } from "../../../vault/popup/utils/vault-popout-window"; @@ -232,12 +233,8 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi } }); - this.windowClosed$ = fromEventPattern( - // FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener - // and test that it doesn't break. Tracking Ticket: https://bitwarden.atlassian.net/browse/PM-4735 - // eslint-disable-next-line no-restricted-syntax - (handler: any) => chrome.windows.onRemoved.addListener(handler), - (handler: any) => chrome.windows.onRemoved.removeListener(handler), + this.windowClosed$ = fromChromeEvent(chrome.windows.onRemoved).pipe( + map(([windowId]) => windowId), ); BrowserFido2UserInterfaceSession.sendMessage({ 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 f0d66bd49ed..16711fabbf4 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 @@ -17,7 +17,7 @@
-

+

{{ "createdSendSuccessfully" | i18n }}

diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 9a69ca62a1f..18ea0337a04 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -900,19 +900,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "der" version = "0.7.10" @@ -1533,12 +1520,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hashbrown" version = "0.15.3" @@ -1554,7 +1535,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.3", + "hashbrown", ] [[package]] @@ -1719,7 +1700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.3", + "hashbrown", ] [[package]] @@ -1891,7 +1872,6 @@ version = "0.0.0" dependencies = [ "desktop_core", "futures", - "oslog", "serde", "serde_json", "tokio", @@ -2379,17 +2359,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "oslog" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" -dependencies = [ - "cc", - "dashmap", - "log", -] - [[package]] name = "p256" version = "0.13.2" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 4b5c1335c6b..edc15675c86 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -41,13 +41,11 @@ interprocess = "=2.2.1" keytar = "=0.1.6" libc = "=0.2.172" linux-keyutils = "=0.2.4" -log = "=0.4.25" memsec = "=0.7.0" napi = "=2.16.17" napi-build = "=2.2.0" napi-derive = "=2.16.13" oo7 = "=0.4.3" -oslog = "=0.2.0" pin-project = "=1.1.10" pkcs8 = "=0.10.2" rand = "=0.9.1" @@ -60,7 +58,6 @@ security-framework-sys = "=2.15.0" serde = "=1.0.209" serde_json = "=1.0.127" sha2 = "=0.10.8" -simplelog = "=0.12.2" ssh-encoding = "=0.2.0" ssh-key = { version = "=0.6.7", default-features = false } sysinfo = "=0.35.0" diff --git a/apps/desktop/desktop_native/macos_provider/Cargo.toml b/apps/desktop/desktop_native/macos_provider/Cargo.toml index 97a8b7d545a..ea44f3d9a27 100644 --- a/apps/desktop/desktop_native/macos_provider/Cargo.toml +++ b/apps/desktop/desktop_native/macos_provider/Cargo.toml @@ -21,12 +21,11 @@ serde_json = { workspace = true } tokio = { workspace = true, features = ["sync"] } tokio-util = { workspace = true } tracing = { workspace = true } -tracing-oslog = "0.3.0" tracing-subscriber = { workspace = true } uniffi = { workspace = true, features = ["cli"] } [target.'cfg(target_os = "macos")'.dependencies] -oslog = { workspace = true } +tracing-oslog = "0.3.0" [build-dependencies] uniffi = { workspace = true, features = ["build"] } diff --git a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts index 85c447db052..655bd4ae5fd 100644 --- a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts @@ -6,6 +6,7 @@ import { LogService } from "@bitwarden/logging"; import { WindowMain } from "../../main/window.main"; import { stringIsNotUndefinedNullAndEmpty } from "../../utils"; import { AutotypeConfig } from "../models/autotype-configure"; +import { AutotypeVaultData } from "../models/autotype-vault-data"; import { AUTOTYPE_IPC_CHANNELS } from "../models/ipc-channels"; import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut"; @@ -49,18 +50,12 @@ export class MainDesktopAutotypeService { this.setKeyboardShortcut(newKeyboardShortcut); }); - ipcMain.on(AUTOTYPE_IPC_CHANNELS.EXECUTE, (_event, data) => { - const { response } = data; - + ipcMain.on(AUTOTYPE_IPC_CHANNELS.EXECUTE, (_event, vaultData: AutotypeVaultData) => { if ( - stringIsNotUndefinedNullAndEmpty(response.username) && - stringIsNotUndefinedNullAndEmpty(response.password) + stringIsNotUndefinedNullAndEmpty(vaultData.username) && + stringIsNotUndefinedNullAndEmpty(vaultData.password) ) { - this.doAutotype( - response.username, - response.password, - this.autotypeKeyboardShortcut.getArrayFormat(), - ); + this.doAutotype(vaultData, this.autotypeKeyboardShortcut.getArrayFormat()); } }); @@ -137,8 +132,9 @@ export class MainDesktopAutotypeService { } } - private doAutotype(username: string, password: string, keyboardShortcut: string[]) { - const inputPattern = username + "\t" + password; + private doAutotype(vaultData: AutotypeVaultData, keyboardShortcut: string[]) { + const TAB = "\t"; + const inputPattern = vaultData.username + TAB + vaultData.password; const inputArray = new Array(inputPattern.length); for (let i = 0; i < inputPattern.length; i++) { diff --git a/apps/desktop/src/autofill/models/autotype-vault-data.ts b/apps/desktop/src/autofill/models/autotype-vault-data.ts new file mode 100644 index 00000000000..ee3db98c334 --- /dev/null +++ b/apps/desktop/src/autofill/models/autotype-vault-data.ts @@ -0,0 +1,8 @@ +/** + * Vault data used in autotype operations. + * `username` and `password` are guaranteed to be not null/undefined. + */ +export interface AutotypeVaultData { + username: string; + password: string; +} diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index 01fa1c692f9..71f7867cbdd 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -6,6 +6,7 @@ import { Command } from "../platform/main/autofill/command"; import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main"; import { AutotypeConfig } from "./models/autotype-configure"; +import { AutotypeVaultData } from "./models/autotype-vault-data"; import { AUTOTYPE_IPC_CHANNELS } from "./models/ipc-channels"; export default { @@ -145,10 +146,7 @@ export default { listenAutotypeRequest: ( fn: ( windowTitle: string, - completeCallback: ( - error: Error | null, - response: { username?: string; password?: string }, - ) => void, + completeCallback: (error: Error | null, response: AutotypeVaultData | null) => void, ) => void, ) => { ipcRenderer.on( @@ -161,7 +159,7 @@ export default { ) => { const { windowTitle } = data; - fn(windowTitle, (error, response) => { + fn(windowTitle, (error, vaultData) => { if (error) { ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.EXECUTION_ERROR, { windowTitle, @@ -170,10 +168,9 @@ export default { return; } - ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.EXECUTE, { - windowTitle, - response, - }); + if (vaultData !== null) { + ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.EXECUTE, vaultData); + } }); }, ); diff --git a/apps/desktop/src/autofill/services/desktop-autotype.service.spec.ts b/apps/desktop/src/autofill/services/desktop-autotype.service.spec.ts new file mode 100644 index 00000000000..30cc800dd28 --- /dev/null +++ b/apps/desktop/src/autofill/services/desktop-autotype.service.spec.ts @@ -0,0 +1,50 @@ +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { getAutotypeVaultData } from "./desktop-autotype.service"; + +describe("getAutotypeVaultData", () => { + it("should return vault data when cipher has username and password", () => { + const cipherView = new CipherView(); + cipherView.login.username = "foo"; + cipherView.login.password = "bar"; + + const [error, vaultData] = getAutotypeVaultData(cipherView); + + expect(error).toBeNull(); + expect(vaultData?.username).toEqual("foo"); + expect(vaultData?.password).toEqual("bar"); + }); + + it("should return error when firstCipher is undefined", () => { + const cipherView = undefined; + const [error, vaultData] = getAutotypeVaultData(cipherView); + + expect(vaultData).toBeNull(); + expect(error).toBeDefined(); + expect(error?.message).toEqual("No matching vault item."); + }); + + it("should return error when username is undefined", () => { + const cipherView = new CipherView(); + cipherView.login.username = undefined; + cipherView.login.password = "bar"; + + const [error, vaultData] = getAutotypeVaultData(cipherView); + + expect(vaultData).toBeNull(); + expect(error).toBeDefined(); + expect(error?.message).toEqual("Vault item is undefined."); + }); + + it("should return error when password is undefined", () => { + const cipherView = new CipherView(); + cipherView.login.username = "foo"; + cipherView.login.password = undefined; + + const [error, vaultData] = getAutotypeVaultData(cipherView); + + expect(vaultData).toBeNull(); + expect(error).toBeDefined(); + expect(error?.message).toEqual("Vault item is undefined."); + }); +}); diff --git a/apps/desktop/src/autofill/services/desktop-autotype.service.ts b/apps/desktop/src/autofill/services/desktop-autotype.service.ts index 7f75a55cf7d..08be86e307e 100644 --- a/apps/desktop/src/autofill/services/desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autotype.service.ts @@ -32,6 +32,7 @@ import { LogService } from "@bitwarden/logging"; import { UserId } from "@bitwarden/user-core"; import { AutotypeConfig } from "../models/autotype-configure"; +import { AutotypeVaultData } from "../models/autotype-vault-data"; import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service"; @@ -43,6 +44,8 @@ export const AUTOTYPE_ENABLED = new KeyDefinition( { deserializer: (b) => b }, ); +export type Result = [E, null] | [null, T]; + /* Valid windows shortcut keys: Control, Alt, Super, Shift, letters A - Z Valid macOS shortcut keys: Control, Alt, Command, Shift, letters A - Z @@ -121,11 +124,8 @@ export class DesktopAutotypeService implements OnDestroy { ipc.autofill.listenAutotypeRequest(async (windowTitle, callback) => { const possibleCiphers = await this.matchCiphersToWindowTitle(windowTitle); const firstCipher = possibleCiphers?.at(0); - - return callback(null, { - username: firstCipher?.login?.username, - password: firstCipher?.login?.password, - }); + const [error, vaultData] = getAutotypeVaultData(firstCipher); + callback(error, vaultData); }); // If `autotypeDefaultPolicy` is `true` for a user's organization, and the @@ -245,3 +245,23 @@ export class DesktopAutotypeService implements OnDestroy { this.destroy$.complete(); } } + +/** + * @return an `AutotypeVaultData` object or an `Error` if the + * cipher or vault data within are undefined. + */ +export function getAutotypeVaultData( + cipherView: CipherView | undefined, +): Result { + if (!cipherView) { + return [Error("No matching vault item."), null]; + } else if (cipherView.login.username === undefined || cipherView.login.password === undefined) { + return [Error("Vault item is undefined."), null]; + } else { + const vaultData: AutotypeVaultData = { + username: cipherView.login.username, + password: cipherView.login.password, + }; + return [null, vaultData]; + } +} diff --git a/apps/desktop/src/platform/components/approve-ssh-request.html b/apps/desktop/src/platform/components/approve-ssh-request.html index b7005872f25..55092788079 100644 --- a/apps/desktop/src/platform/components/approve-ssh-request.html +++ b/apps/desktop/src/platform/components/approve-ssh-request.html @@ -1,6 +1,6 @@

-
{{ "sshkeyApprovalTitle" | i18n }}
+
{{ "sshkeyApprovalTitle" | i18n }}
{ + let service: WebPremiumInterestStateService; + let stateProvider: FakeStateProvider; + let accountService: FakeAccountService; + + const mockUserId = newGuid() as UserId; + const mockUserEmail = "user@example.com"; + + beforeEach(() => { + accountService = mockAccountServiceWith(mockUserId, { email: mockUserEmail }); + stateProvider = new FakeStateProvider(accountService); + service = new WebPremiumInterestStateService(stateProvider); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("getPremiumInterest", () => { + it("should throw an error when userId is not provided", async () => { + const promise = service.getPremiumInterest(null); + + await expect(promise).rejects.toThrow("UserId is required. Cannot get 'premiumInterest'."); + }); + + it("should return null when no value is set", async () => { + const result = await service.getPremiumInterest(mockUserId); + + expect(result).toBeNull(); + }); + + it("should return true when value is set to true", async () => { + await stateProvider.setUserState(PREMIUM_INTEREST_KEY, true, mockUserId); + + const result = await service.getPremiumInterest(mockUserId); + + expect(result).toBe(true); + }); + + it("should return false when value is set to false", async () => { + await stateProvider.setUserState(PREMIUM_INTEREST_KEY, false, mockUserId); + + const result = await service.getPremiumInterest(mockUserId); + + expect(result).toBe(false); + }); + + it("should use getUserState$ to retrieve the value", async () => { + const getUserStateSpy = jest.spyOn(stateProvider, "getUserState$"); + await stateProvider.setUserState(PREMIUM_INTEREST_KEY, true, mockUserId); + + await service.getPremiumInterest(mockUserId); + + expect(getUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, mockUserId); + }); + }); + + describe("setPremiumInterest", () => { + it("should throw an error when userId is not provided", async () => { + const promise = service.setPremiumInterest(null, true); + + await expect(promise).rejects.toThrow("UserId is required. Cannot set 'premiumInterest'."); + }); + + it("should set the value to true", async () => { + await service.setPremiumInterest(mockUserId, true); + + const result = await firstValueFrom( + stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId), + ); + + expect(result).toBe(true); + }); + + it("should set the value to false", async () => { + await service.setPremiumInterest(mockUserId, false); + + const result = await firstValueFrom( + stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId), + ); + + expect(result).toBe(false); + }); + + it("should update an existing value", async () => { + await service.setPremiumInterest(mockUserId, true); + await service.setPremiumInterest(mockUserId, false); + + const result = await firstValueFrom( + stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId), + ); + + expect(result).toBe(false); + }); + + it("should use setUserState to store the value", async () => { + const setUserStateSpy = jest.spyOn(stateProvider, "setUserState"); + + await service.setPremiumInterest(mockUserId, true); + + expect(setUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, true, mockUserId); + }); + }); + + describe("clearPremiumInterest", () => { + it("should throw an error when userId is not provided", async () => { + const promise = service.clearPremiumInterest(null); + + await expect(promise).rejects.toThrow("UserId is required. Cannot clear 'premiumInterest'."); + }); + + it("should clear the value by setting it to null", async () => { + await service.setPremiumInterest(mockUserId, true); + await service.clearPremiumInterest(mockUserId); + + const result = await firstValueFrom( + stateProvider.getUserState$(PREMIUM_INTEREST_KEY, mockUserId), + ); + + expect(result).toBeNull(); + }); + + it("should use setUserState with null to clear the value", async () => { + const setUserStateSpy = jest.spyOn(stateProvider, "setUserState"); + await service.setPremiumInterest(mockUserId, true); + + await service.clearPremiumInterest(mockUserId); + + expect(setUserStateSpy).toHaveBeenCalledWith(PREMIUM_INTEREST_KEY, null, mockUserId); + }); + }); +}); diff --git a/apps/web/src/app/billing/services/premium-interest/web-premium-interest-state.service.ts b/apps/web/src/app/billing/services/premium-interest/web-premium-interest-state.service.ts new file mode 100644 index 00000000000..f66fba559f4 --- /dev/null +++ b/apps/web/src/app/billing/services/premium-interest/web-premium-interest-state.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction"; +import { BILLING_MEMORY, StateProvider, UserKeyDefinition } from "@bitwarden/state"; +import { UserId } from "@bitwarden/user-core"; + +export const PREMIUM_INTEREST_KEY = new UserKeyDefinition( + BILLING_MEMORY, + "premiumInterest", + { + deserializer: (value: boolean) => value, + clearOn: ["lock", "logout"], + }, +); + +@Injectable() +export class WebPremiumInterestStateService implements PremiumInterestStateService { + constructor(private stateProvider: StateProvider) {} + + async getPremiumInterest(userId: UserId): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot get 'premiumInterest'."); + } + + return await firstValueFrom(this.stateProvider.getUserState$(PREMIUM_INTEREST_KEY, userId)); + } + + async setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot set 'premiumInterest'."); + } + + await this.stateProvider.setUserState(PREMIUM_INTEREST_KEY, premiumInterest, userId); + } + + async clearPremiumInterest(userId: UserId): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot clear 'premiumInterest'."); + } + + await this.stateProvider.setUserState(PREMIUM_INTEREST_KEY, null, userId); + } +} diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 9619c3e23bf..72117f547d4 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -14,6 +14,7 @@ import { DefaultDeviceManagementComponentService } from "@bitwarden/angular/auth import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/auth/device-management/device-management-component.service.abstraction"; import { ChangePasswordService } from "@bitwarden/angular/auth/password-management/change-password"; import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction"; +import { PremiumInterestStateService } from "@bitwarden/angular/billing/services/premium-interest/premium-interest-state.service.abstraction"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { CLIENT_TYPE, @@ -129,6 +130,7 @@ import { WebSetInitialPasswordService, } from "../auth"; import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service"; +import { WebPremiumInterestStateService } from "../billing/services/premium-interest/web-premium-interest-state.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; import { WebFileDownloadService } from "../core/web-file-download.service"; @@ -421,6 +423,11 @@ const safeProviders: SafeProvider[] = [ Router, ], }), + safeProvider({ + provide: PremiumInterestStateService, + useClass: WebPremiumInterestStateService, + deps: [StateProvider], + }), ]; @NgModule({ diff --git a/apps/web/src/app/tools/send/send.component.html b/apps/web/src/app/tools/send/send.component.html index b79f50311ed..b8538606aec 100644 --- a/apps/web/src/app/tools/send/send.component.html +++ b/apps/web/src/app/tools/send/send.component.html @@ -21,7 +21,7 @@
{{ "filters" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html index b7d05c73768..15ccd3241e4 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.html @@ -6,7 +6,8 @@ } @else { - @if (!(dataService.hasReportData$ | async)) { + @if (isRiskInsightsActivityTabFeatureEnabled && !(dataService.hasReportData$ | async)) { +
@if (!hasCiphers) { @@ -33,47 +34,47 @@ }
} @else { - +

{{ "riskInsights" | i18n }}

{{ "reviewAtRiskPasswords" | i18n }}
- @if (dataLastUpdated) { -
- + @let isRunningReport = dataService.isGeneratingReport$ | async; +
+ + @if (dataLastUpdated) { {{ "dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a") }} - @let isRunningReport = dataService.isGeneratingReport$ | async; - - - - - + } + + + + -
- } + +
diff --git a/libs/angular/src/billing/services/premium-interest/noop-premium-interest-state.service.ts b/libs/angular/src/billing/services/premium-interest/noop-premium-interest-state.service.ts new file mode 100644 index 00000000000..f941e86e0d0 --- /dev/null +++ b/libs/angular/src/billing/services/premium-interest/noop-premium-interest-state.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from "@angular/core"; + +import { UserId } from "@bitwarden/user-core"; + +import { PremiumInterestStateService } from "./premium-interest-state.service.abstraction"; + +@Injectable() +export class NoopPremiumInterestStateService implements PremiumInterestStateService { + async getPremiumInterest(userId: UserId): Promise { + return null; + } // no-op + async setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise {} // no-op + async clearPremiumInterest(userId: UserId): Promise {} // no-op +} diff --git a/libs/angular/src/billing/services/premium-interest/premium-interest-state.service.abstraction.ts b/libs/angular/src/billing/services/premium-interest/premium-interest-state.service.abstraction.ts new file mode 100644 index 00000000000..850560df38c --- /dev/null +++ b/libs/angular/src/billing/services/premium-interest/premium-interest-state.service.abstraction.ts @@ -0,0 +1,14 @@ +import { UserId } from "@bitwarden/user-core"; + +/** + * A service that manages state which conveys whether or not a user has expressed interest + * in setting up a premium subscription. This applies for users who began the registration + * process on https://bitwarden.com/go/start-premium/, which is a marketing page designed + * to streamline users who intend to setup a premium subscription after registration. + * - Implemented in Web only. No-op for other clients. + */ +export abstract class PremiumInterestStateService { + abstract getPremiumInterest(userId: UserId): Promise; + abstract setPremiumInterest(userId: UserId, premiumInterest: boolean): Promise; + abstract clearPremiumInterest(userId: UserId): Promise; +} diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index fc9c5b8b15c..38ce3c0fcc2 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -380,6 +380,8 @@ import { DefaultSetInitialPasswordService } from "../auth/password-management/se import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction"; import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation"; +import { NoopPremiumInterestStateService } from "../billing/services/premium-interest/noop-premium-interest-state.service"; +import { PremiumInterestStateService } from "../billing/services/premium-interest/premium-interest-state.service.abstraction"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; import { DocumentLangSetter } from "../platform/i18n"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; @@ -1724,6 +1726,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultNewDeviceVerificationComponentService, deps: [], }), + safeProvider({ + provide: PremiumInterestStateService, + useClass: NoopPremiumInterestStateService, + deps: [], + }), ]; @NgModule({ diff --git a/libs/common/src/vault/models/data/attachment.data.ts b/libs/common/src/vault/models/data/attachment.data.ts index dfc9f9d1afa..dde5db24b57 100644 --- a/libs/common/src/vault/models/data/attachment.data.ts +++ b/libs/common/src/vault/models/data/attachment.data.ts @@ -1,14 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { AttachmentResponse } from "../response/attachment.response"; export class AttachmentData { - id: string; - url: string; - fileName: string; - key: string; - size: string; - sizeName: string; + id?: string; + url?: string; + fileName?: string; + key?: string; + size?: string; + sizeName?: string; constructor(response?: AttachmentResponse) { if (response == null) { diff --git a/libs/common/src/vault/models/data/card.data.ts b/libs/common/src/vault/models/data/card.data.ts index 677c33f4886..8345c345fd7 100644 --- a/libs/common/src/vault/models/data/card.data.ts +++ b/libs/common/src/vault/models/data/card.data.ts @@ -1,14 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CardApi } from "../api/card.api"; export class CardData { - cardholderName: string; - brand: string; - number: string; - expMonth: string; - expYear: string; - code: string; + cardholderName?: string; + brand?: string; + number?: string; + expMonth?: string; + expYear?: string; + code?: string; constructor(data?: CardApi) { if (data == null) { diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts index 4921cce8df2..743ea941b0e 100644 --- a/libs/common/src/vault/models/data/cipher.data.ts +++ b/libs/common/src/vault/models/data/cipher.data.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; @@ -17,18 +15,18 @@ import { SecureNoteData } from "./secure-note.data"; import { SshKeyData } from "./ssh-key.data"; export class CipherData { - id: string; - organizationId: string; - folderId: string; - edit: boolean; - viewPassword: boolean; - permissions: CipherPermissionsApi; - organizationUseTotp: boolean; - favorite: boolean; + id: string = ""; + organizationId?: string; + folderId?: string; + edit: boolean = false; + viewPassword: boolean = true; + permissions?: CipherPermissionsApi; + organizationUseTotp: boolean = false; + favorite: boolean = false; revisionDate: string; - type: CipherType; - name: string; - notes: string; + type: CipherType = CipherType.Login; + name: string = ""; + notes?: string; login?: LoginData; secureNote?: SecureNoteData; card?: CardData; @@ -39,13 +37,14 @@ export class CipherData { passwordHistory?: PasswordHistoryData[]; collectionIds?: string[]; creationDate: string; - deletedDate: string | undefined; - archivedDate: string | undefined; - reprompt: CipherRepromptType; - key: string; + deletedDate?: string; + archivedDate?: string; + reprompt: CipherRepromptType = CipherRepromptType.None; + key?: string; constructor(response?: CipherResponse, collectionIds?: string[]) { if (response == null) { + this.creationDate = this.revisionDate = new Date().toISOString(); return; } @@ -101,7 +100,9 @@ export class CipherData { static fromJSON(obj: Jsonify) { const result = Object.assign(new CipherData(), obj); - result.permissions = CipherPermissionsApi.fromJSON(obj.permissions); + if (obj.permissions != null) { + result.permissions = CipherPermissionsApi.fromJSON(obj.permissions); + } return result; } } diff --git a/libs/common/src/vault/models/data/fido2-credential.data.ts b/libs/common/src/vault/models/data/fido2-credential.data.ts index 94716e8d86c..602b74f9805 100644 --- a/libs/common/src/vault/models/data/fido2-credential.data.ts +++ b/libs/common/src/vault/models/data/fido2-credential.data.ts @@ -1,21 +1,19 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Fido2CredentialApi } from "../api/fido2-credential.api"; export class Fido2CredentialData { - credentialId: string; - keyType: "public-key"; - keyAlgorithm: "ECDSA"; - keyCurve: "P-256"; - keyValue: string; - rpId: string; - userHandle: string; - userName: string; - counter: string; - rpName: string; - userDisplayName: string; - discoverable: string; - creationDate: string; + credentialId!: string; + keyType!: string; + keyAlgorithm!: string; + keyCurve!: string; + keyValue!: string; + rpId!: string; + userHandle?: string; + userName?: string; + counter!: string; + rpName?: string; + userDisplayName?: string; + discoverable!: string; + creationDate!: string; constructor(data?: Fido2CredentialApi) { if (data == null) { diff --git a/libs/common/src/vault/models/data/field.data.ts b/libs/common/src/vault/models/data/field.data.ts index cf9df69a6b0..a63e903e665 100644 --- a/libs/common/src/vault/models/data/field.data.ts +++ b/libs/common/src/vault/models/data/field.data.ts @@ -1,13 +1,11 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { FieldType, LinkedIdType } from "../../enums"; import { FieldApi } from "../api/field.api"; export class FieldData { - type: FieldType; - name: string; - value: string; - linkedId: LinkedIdType | null; + type: FieldType = FieldType.Text; + name?: string; + value?: string; + linkedId?: LinkedIdType; constructor(response?: FieldApi) { if (response == null) { diff --git a/libs/common/src/vault/models/data/identity.data.ts b/libs/common/src/vault/models/data/identity.data.ts index 158daace371..de854df1ec6 100644 --- a/libs/common/src/vault/models/data/identity.data.ts +++ b/libs/common/src/vault/models/data/identity.data.ts @@ -1,26 +1,24 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { IdentityApi } from "../api/identity.api"; export class IdentityData { - title: string; - firstName: string; - middleName: string; - lastName: string; - address1: string; - address2: string; - address3: string; - city: string; - state: string; - postalCode: string; - country: string; - company: string; - email: string; - phone: string; - ssn: string; - username: string; - passportNumber: string; - licenseNumber: string; + title?: string; + firstName?: string; + middleName?: string; + lastName?: string; + address1?: string; + address2?: string; + address3?: string; + city?: string; + state?: string; + postalCode?: string; + country?: string; + company?: string; + email?: string; + phone?: string; + ssn?: string; + username?: string; + passportNumber?: string; + licenseNumber?: string; constructor(data?: IdentityApi) { if (data == null) { diff --git a/libs/common/src/vault/models/data/login-uri.data.ts b/libs/common/src/vault/models/data/login-uri.data.ts index 852dad4e112..ea3a1d9adce 100644 --- a/libs/common/src/vault/models/data/login-uri.data.ts +++ b/libs/common/src/vault/models/data/login-uri.data.ts @@ -1,12 +1,10 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { LoginUriApi } from "../api/login-uri.api"; export class LoginUriData { - uri: string; - uriChecksum: string; - match: UriMatchStrategySetting = null; + uri?: string; + uriChecksum?: string; + match?: UriMatchStrategySetting; constructor(data?: LoginUriApi) { if (data == null) { diff --git a/libs/common/src/vault/models/data/login.data.ts b/libs/common/src/vault/models/data/login.data.ts index 0fe021d923c..8c0aba0fdaa 100644 --- a/libs/common/src/vault/models/data/login.data.ts +++ b/libs/common/src/vault/models/data/login.data.ts @@ -1,17 +1,15 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { LoginApi } from "../api/login.api"; import { Fido2CredentialData } from "./fido2-credential.data"; import { LoginUriData } from "./login-uri.data"; export class LoginData { - uris: LoginUriData[]; - username: string; - password: string; - passwordRevisionDate: string; - totp: string; - autofillOnPageLoad: boolean; + uris?: LoginUriData[]; + username?: string; + password?: string; + passwordRevisionDate?: string; + totp?: string; + autofillOnPageLoad?: boolean; fido2Credentials?: Fido2CredentialData[]; constructor(data?: LoginApi) { diff --git a/libs/common/src/vault/models/data/password-history.data.ts b/libs/common/src/vault/models/data/password-history.data.ts index 75a51ed3728..465b5e59b8d 100644 --- a/libs/common/src/vault/models/data/password-history.data.ts +++ b/libs/common/src/vault/models/data/password-history.data.ts @@ -1,10 +1,8 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { PasswordHistoryResponse } from "../response/password-history.response"; export class PasswordHistoryData { - password: string; - lastUsedDate: string; + password!: string; + lastUsedDate!: string; constructor(response?: PasswordHistoryResponse) { if (response == null) { diff --git a/libs/common/src/vault/models/data/secure-note.data.ts b/libs/common/src/vault/models/data/secure-note.data.ts index 7d109398ab7..5556417ef9b 100644 --- a/libs/common/src/vault/models/data/secure-note.data.ts +++ b/libs/common/src/vault/models/data/secure-note.data.ts @@ -1,10 +1,8 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { SecureNoteType } from "../../enums"; import { SecureNoteApi } from "../api/secure-note.api"; export class SecureNoteData { - type: SecureNoteType; + type: SecureNoteType = SecureNoteType.Generic; constructor(data?: SecureNoteApi) { if (data == null) { diff --git a/libs/common/src/vault/models/data/ssh-key.data.ts b/libs/common/src/vault/models/data/ssh-key.data.ts index 2b93c93d931..1e06a1a7df5 100644 --- a/libs/common/src/vault/models/data/ssh-key.data.ts +++ b/libs/common/src/vault/models/data/ssh-key.data.ts @@ -1,11 +1,9 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { SshKeyApi } from "../api/ssh-key.api"; export class SshKeyData { - privateKey: string; - publicKey: string; - keyFingerprint: string; + privateKey!: string; + publicKey!: string; + keyFingerprint!: string; constructor(data?: SshKeyApi) { if (data == null) { diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index 972c77537ff..77bb3eda38d 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -39,6 +39,12 @@ describe("Attachment", () => { key: undefined, fileName: undefined, }); + expect(data.id).toBeUndefined(); + expect(data.url).toBeUndefined(); + expect(data.fileName).toBeUndefined(); + expect(data.key).toBeUndefined(); + expect(data.size).toBeUndefined(); + expect(data.sizeName).toBeUndefined(); }); it("Convert", () => { diff --git a/libs/common/src/vault/models/domain/card.spec.ts b/libs/common/src/vault/models/domain/card.spec.ts index a4d242329a4..185c2fa4b8f 100644 --- a/libs/common/src/vault/models/domain/card.spec.ts +++ b/libs/common/src/vault/models/domain/card.spec.ts @@ -29,6 +29,13 @@ describe("Card", () => { expYear: undefined, code: undefined, }); + + expect(data.cardholderName).toBeUndefined(); + expect(data.brand).toBeUndefined(); + expect(data.number).toBeUndefined(); + expect(data.expMonth).toBeUndefined(); + expect(data.expYear).toBeUndefined(); + expect(data.code).toBeUndefined(); }); it("Convert", () => { diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 4052c9e5338..87301928c57 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -44,22 +44,22 @@ describe("Cipher DTO", () => { const data = new CipherData(); const cipher = new Cipher(data); - expect(cipher.id).toBeUndefined(); + expect(cipher.id).toEqual(""); expect(cipher.organizationId).toBeUndefined(); expect(cipher.folderId).toBeUndefined(); expect(cipher.name).toBeInstanceOf(EncString); expect(cipher.notes).toBeUndefined(); - expect(cipher.type).toBeUndefined(); - expect(cipher.favorite).toBeUndefined(); - expect(cipher.organizationUseTotp).toBeUndefined(); - expect(cipher.edit).toBeUndefined(); - expect(cipher.viewPassword).toBeUndefined(); + expect(cipher.type).toEqual(CipherType.Login); + expect(cipher.favorite).toEqual(false); + expect(cipher.organizationUseTotp).toEqual(false); + expect(cipher.edit).toEqual(false); + expect(cipher.viewPassword).toEqual(true); expect(cipher.revisionDate).toBeInstanceOf(Date); expect(cipher.collectionIds).toEqual([]); expect(cipher.localData).toBeUndefined(); expect(cipher.creationDate).toBeInstanceOf(Date); expect(cipher.deletedDate).toBeUndefined(); - expect(cipher.reprompt).toBeUndefined(); + expect(cipher.reprompt).toEqual(CipherRepromptType.None); expect(cipher.attachments).toBeUndefined(); expect(cipher.fields).toBeUndefined(); expect(cipher.passwordHistory).toBeUndefined(); @@ -836,6 +836,38 @@ describe("Cipher DTO", () => { expect(actual).toBeInstanceOf(Cipher); }); + it("handles null permissions correctly without calling CipherPermissionsApi constructor", () => { + const spy = jest.spyOn(CipherPermissionsApi.prototype, "constructor" as any); + const revisionDate = new Date("2022-08-04T01:06:40.441Z"); + const actual = Cipher.fromJSON({ + name: "myName", + revisionDate: revisionDate.toISOString(), + permissions: null, + } as Jsonify); + + expect(actual.permissions).toBeUndefined(); + expect(actual).toBeInstanceOf(Cipher); + // Verify that CipherPermissionsApi constructor was not called for null permissions + expect(spy).not.toHaveBeenCalledWith(null); + spy.mockRestore(); + }); + + it("calls CipherPermissionsApi constructor when permissions are provided", () => { + const spy = jest.spyOn(CipherPermissionsApi.prototype, "constructor" as any); + const revisionDate = new Date("2022-08-04T01:06:40.441Z"); + const permissionsObj = { delete: true, restore: false }; + const actual = Cipher.fromJSON({ + name: "myName", + revisionDate: revisionDate.toISOString(), + permissions: permissionsObj, + } as Jsonify); + + expect(actual.permissions).toBeInstanceOf(CipherPermissionsApi); + expect(actual.permissions.delete).toBe(true); + expect(actual.permissions.restore).toBe(false); + spy.mockRestore(); + }); + test.each([ // Test description, CipherType, expected output ["LoginView", CipherType.Login, { login: "myLogin_fromJSON" }], @@ -1056,6 +1088,7 @@ describe("Cipher DTO", () => { card: undefined, secureNote: undefined, sshKey: undefined, + data: undefined, favorite: false, reprompt: SdkCipherRepromptType.None, organizationUseTotp: true, diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index 5e284232936..5739a9a50a7 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -421,6 +421,7 @@ export class Cipher extends Domain implements Decryptable { card: undefined, secureNote: undefined, sshKey: undefined, + data: undefined, }; switch (this.type) { diff --git a/libs/common/src/vault/models/domain/field.spec.ts b/libs/common/src/vault/models/domain/field.spec.ts index d99336adad0..0a4bc8e3c29 100644 --- a/libs/common/src/vault/models/domain/field.spec.ts +++ b/libs/common/src/vault/models/domain/field.spec.ts @@ -29,7 +29,7 @@ describe("Field", () => { const field = new Field(data); expect(field).toEqual({ - type: undefined, + type: FieldType.Text, name: undefined, value: undefined, linkedId: undefined, diff --git a/libs/common/src/vault/models/domain/identity.spec.ts b/libs/common/src/vault/models/domain/identity.spec.ts index c2c2363fa0d..411f6d1c9ea 100644 --- a/libs/common/src/vault/models/domain/identity.spec.ts +++ b/libs/common/src/vault/models/domain/identity.spec.ts @@ -53,6 +53,27 @@ describe("Identity", () => { title: undefined, username: undefined, }); + + expect(data).toEqual({ + title: undefined, + firstName: undefined, + middleName: undefined, + lastName: undefined, + address1: undefined, + address2: undefined, + address3: undefined, + city: undefined, + state: undefined, + postalCode: undefined, + country: undefined, + company: undefined, + email: undefined, + phone: undefined, + ssn: undefined, + username: undefined, + passportNumber: undefined, + licenseNumber: undefined, + }); }); it("Convert", () => { diff --git a/libs/common/src/vault/models/domain/login-uri.spec.ts b/libs/common/src/vault/models/domain/login-uri.spec.ts index 982b435384b..2effd1bb9fe 100644 --- a/libs/common/src/vault/models/domain/login-uri.spec.ts +++ b/libs/common/src/vault/models/domain/login-uri.spec.ts @@ -7,6 +7,7 @@ import { mockEnc, mockFromJson } from "../../../../spec"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import { UriMatchStrategy } from "../../../models/domain/domain-service"; +import { LoginUriApi } from "../api/login-uri.api"; import { LoginUriData } from "../data/login-uri.data"; import { LoginUri } from "./login-uri"; @@ -31,6 +32,9 @@ describe("LoginUri", () => { uri: undefined, uriChecksum: undefined, }); + expect(data.uri).toBeUndefined(); + expect(data.uriChecksum).toBeUndefined(); + expect(data.match).toBeUndefined(); }); it("Convert", () => { @@ -61,6 +65,23 @@ describe("LoginUri", () => { }); }); + it("handle null match", () => { + const apiData = Object.assign(new LoginUriApi(), { + uri: "testUri", + uriChecksum: "testChecksum", + match: null, + }); + + const loginUriData = new LoginUriData(apiData); + + // The data model stores it as-is (null or undefined) + expect(loginUriData.match).toBeNull(); + + // But the domain model converts null to undefined + const loginUri = new LoginUri(loginUriData); + expect(loginUri.match).toBeUndefined(); + }); + describe("validateChecksum", () => { let encryptService: MockProxy; @@ -118,7 +139,7 @@ describe("LoginUri", () => { }); describe("SDK Login Uri Mapping", () => { - it("should map to SDK login uri", () => { + it("maps to SDK login uri", () => { const loginUri = new LoginUri(data); const sdkLoginUri = loginUri.toSdkLoginUri(); diff --git a/libs/common/src/vault/models/domain/login.spec.ts b/libs/common/src/vault/models/domain/login.spec.ts index 9f03e225b7f..6ebcfea057a 100644 --- a/libs/common/src/vault/models/domain/login.spec.ts +++ b/libs/common/src/vault/models/domain/login.spec.ts @@ -25,6 +25,14 @@ describe("Login DTO", () => { password: undefined, totp: undefined, }); + + expect(data.username).toBeUndefined(); + expect(data.password).toBeUndefined(); + expect(data.passwordRevisionDate).toBeUndefined(); + expect(data.totp).toBeUndefined(); + expect(data.autofillOnPageLoad).toBeUndefined(); + expect(data.uris).toBeUndefined(); + expect(data.fido2Credentials).toBeUndefined(); }); it("Convert from full LoginData", () => { diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index 13342c69014..a9cec13fc7c 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -111,10 +111,7 @@ export class Login extends Domain { }); if (this.uris != null && this.uris.length > 0) { - l.uris = []; - this.uris.forEach((u) => { - l.uris.push(u.toLoginUriData()); - }); + l.uris = this.uris.map((u) => u.toLoginUriData()); } if (this.fido2Credentials != null && this.fido2Credentials.length > 0) { diff --git a/libs/common/src/vault/models/domain/password.spec.ts b/libs/common/src/vault/models/domain/password.spec.ts index 2e37c5e8375..4b2de34beca 100644 --- a/libs/common/src/vault/models/domain/password.spec.ts +++ b/libs/common/src/vault/models/domain/password.spec.ts @@ -20,6 +20,9 @@ describe("Password", () => { expect(password).toBeInstanceOf(Password); expect(password.password).toBeInstanceOf(EncString); expect(password.lastUsedDate).toBeInstanceOf(Date); + + expect(data.password).toBeUndefined(); + expect(data.lastUsedDate).toBeUndefined(); }); it("Convert", () => { @@ -83,4 +86,47 @@ describe("Password", () => { }); }); }); + + describe("fromSdkPasswordHistory", () => { + beforeEach(() => { + jest.restoreAllMocks(); + }); + + it("creates Password from SDK object", () => { + const sdkPasswordHistory = { + password: "2.encPassword|encryptedData" as EncryptedString, + lastUsedDate: "2022-01-31T12:00:00.000Z", + }; + + const password = Password.fromSdkPasswordHistory(sdkPasswordHistory); + + expect(password).toBeInstanceOf(Password); + expect(password?.password).toBeInstanceOf(EncString); + expect(password?.password.encryptedString).toBe("2.encPassword|encryptedData"); + expect(password?.lastUsedDate).toEqual(new Date("2022-01-31T12:00:00.000Z")); + }); + + it("returns undefined for null input", () => { + const result = Password.fromSdkPasswordHistory(null as any); + expect(result).toBeUndefined(); + }); + + it("returns undefined for undefined input", () => { + const result = Password.fromSdkPasswordHistory(undefined); + expect(result).toBeUndefined(); + }); + + it("handles empty SDK object", () => { + const sdkPasswordHistory = { + password: "" as EncryptedString, + lastUsedDate: "", + }; + + const password = Password.fromSdkPasswordHistory(sdkPasswordHistory); + + expect(password).toBeInstanceOf(Password); + expect(password?.password).toBeInstanceOf(EncString); + expect(password?.lastUsedDate).toBeInstanceOf(Date); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/secure-note.spec.ts b/libs/common/src/vault/models/domain/secure-note.spec.ts index 4c8e8d470ca..e445e9ea035 100644 --- a/libs/common/src/vault/models/domain/secure-note.spec.ts +++ b/libs/common/src/vault/models/domain/secure-note.spec.ts @@ -16,22 +16,27 @@ describe("SecureNote", () => { const data = new SecureNoteData(); const secureNote = new SecureNote(data); - expect(secureNote).toEqual({ - type: undefined, - }); + expect(data).toBeDefined(); + expect(secureNote).toEqual({ type: SecureNoteType.Generic }); + expect(data.type).toBe(SecureNoteType.Generic); + }); + + it("Convert from undefined", () => { + const data = new SecureNoteData(undefined); + expect(data.type).toBe(SecureNoteType.Generic); }); it("Convert", () => { const secureNote = new SecureNote(data); - expect(secureNote).toEqual({ - type: 0, - }); + expect(secureNote).toEqual({ type: 0 }); + expect(data.type).toBe(SecureNoteType.Generic); }); it("toSecureNoteData", () => { const secureNote = new SecureNote(data); expect(secureNote.toSecureNoteData()).toEqual(data); + expect(secureNote.toSecureNoteData().type).toBe(SecureNoteType.Generic); }); it("Decrypt", async () => { @@ -49,6 +54,14 @@ describe("SecureNote", () => { it("returns undefined if object is null", () => { expect(SecureNote.fromJSON(null)).toBeUndefined(); }); + + it("creates SecureNote instance from JSON object", () => { + const jsonObj = { type: SecureNoteType.Generic }; + const result = SecureNote.fromJSON(jsonObj); + + expect(result).toBeInstanceOf(SecureNote); + expect(result.type).toBe(SecureNoteType.Generic); + }); }); describe("toSdkSecureNote", () => { @@ -63,4 +76,71 @@ describe("SecureNote", () => { }); }); }); + + describe("fromSdkSecureNote", () => { + it("returns undefined when null is provided", () => { + const result = SecureNote.fromSdkSecureNote(null); + + expect(result).toBeUndefined(); + }); + + it("returns undefined when undefined is provided", () => { + const result = SecureNote.fromSdkSecureNote(undefined); + + expect(result).toBeUndefined(); + }); + + it("creates SecureNote with Generic type from SDK object", () => { + const sdkSecureNote = { + type: SecureNoteType.Generic, + }; + + const result = SecureNote.fromSdkSecureNote(sdkSecureNote); + + expect(result).toBeInstanceOf(SecureNote); + expect(result.type).toBe(SecureNoteType.Generic); + }); + + it("preserves the type value from SDK object", () => { + const sdkSecureNote = { + type: SecureNoteType.Generic, + }; + + const result = SecureNote.fromSdkSecureNote(sdkSecureNote); + + expect(result.type).toBe(0); + }); + + it("creates a new SecureNote instance", () => { + const sdkSecureNote = { + type: SecureNoteType.Generic, + }; + + const result = SecureNote.fromSdkSecureNote(sdkSecureNote); + + expect(result).not.toBe(sdkSecureNote); + expect(result).toBeInstanceOf(SecureNote); + }); + + it("handles SDK object with undefined type", () => { + const sdkSecureNote = { + type: undefined as SecureNoteType, + }; + + const result = SecureNote.fromSdkSecureNote(sdkSecureNote); + + expect(result).toBeInstanceOf(SecureNote); + expect(result.type).toBeUndefined(); + }); + + it("returns symmetric with toSdkSecureNote", () => { + const original = new SecureNote(); + original.type = SecureNoteType.Generic; + + const sdkFormat = original.toSdkSecureNote(); + const reconstructed = SecureNote.fromSdkSecureNote(sdkFormat); + + expect(reconstructed.type).toBe(original.type); + }); + }); }); diff --git a/libs/common/src/vault/models/domain/ssh-key.spec.ts b/libs/common/src/vault/models/domain/ssh-key.spec.ts index 38228e54a4a..10149ebc82d 100644 --- a/libs/common/src/vault/models/domain/ssh-key.spec.ts +++ b/libs/common/src/vault/models/domain/ssh-key.spec.ts @@ -1,4 +1,5 @@ import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { EncString as SdkEncString, SshKey as SdkSshKey } from "@bitwarden/sdk-internal"; import { mockEnc } from "../../../../spec"; import { SshKeyApi } from "../api/ssh-key.api"; @@ -37,6 +38,9 @@ describe("Sshkey", () => { expect(sshKey.privateKey).toBeInstanceOf(EncString); expect(sshKey.publicKey).toBeInstanceOf(EncString); expect(sshKey.keyFingerprint).toBeInstanceOf(EncString); + expect(data.privateKey).toBeUndefined(); + expect(data.publicKey).toBeUndefined(); + expect(data.keyFingerprint).toBeUndefined(); }); it("toSshKeyData", () => { @@ -64,6 +68,21 @@ describe("Sshkey", () => { it("returns undefined if object is null", () => { expect(SshKey.fromJSON(null)).toBeUndefined(); }); + + it("creates SshKey instance from JSON object", () => { + const jsonObj = { + privateKey: "2.privateKey|encryptedData", + publicKey: "2.publicKey|encryptedData", + keyFingerprint: "2.keyFingerprint|encryptedData", + }; + + const result = SshKey.fromJSON(jsonObj); + + expect(result).toBeInstanceOf(SshKey); + expect(result.privateKey).toBeDefined(); + expect(result.publicKey).toBeDefined(); + expect(result.keyFingerprint).toBeDefined(); + }); }); describe("toSdkSshKey", () => { @@ -78,4 +97,58 @@ describe("Sshkey", () => { }); }); }); + + describe("fromSdkSshKey", () => { + it("returns undefined when null is provided", () => { + const result = SshKey.fromSdkSshKey(null); + + expect(result).toBeUndefined(); + }); + + it("returns undefined when undefined is provided", () => { + const result = SshKey.fromSdkSshKey(undefined); + + expect(result).toBeUndefined(); + }); + + it("creates SshKey from SDK object", () => { + const sdkSshKey: SdkSshKey = { + privateKey: "2.privateKey|encryptedData" as SdkEncString, + publicKey: "2.publicKey|encryptedData" as SdkEncString, + fingerprint: "2.keyFingerprint|encryptedData" as SdkEncString, + }; + + const result = SshKey.fromSdkSshKey(sdkSshKey); + + expect(result).toBeInstanceOf(SshKey); + expect(result.privateKey).toBeDefined(); + expect(result.publicKey).toBeDefined(); + expect(result.keyFingerprint).toBeDefined(); + }); + + it("creates a new SshKey instance", () => { + const sdkSshKey: SdkSshKey = { + privateKey: "2.privateKey|encryptedData" as SdkEncString, + publicKey: "2.publicKey|encryptedData" as SdkEncString, + fingerprint: "2.keyFingerprint|encryptedData" as SdkEncString, + }; + + const result = SshKey.fromSdkSshKey(sdkSshKey); + + expect(result).not.toBe(sdkSshKey); + expect(result).toBeInstanceOf(SshKey); + }); + + it("is symmetric with toSdkSshKey", () => { + const original = new SshKey(data); + const sdkFormat = original.toSdkSshKey(); + const reconstructed = SshKey.fromSdkSshKey(sdkFormat); + + expect(reconstructed.privateKey.encryptedString).toBe(original.privateKey.encryptedString); + expect(reconstructed.publicKey.encryptedString).toBe(original.publicKey.encryptedString); + expect(reconstructed.keyFingerprint.encryptedString).toBe( + original.keyFingerprint.encryptedString, + ); + }); + }); }); diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html index 3bd4b741dbb..bd4afaf364b 100644 --- a/libs/importer/src/components/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -11,7 +11,7 @@ -

{{ "destination" | i18n }}

+

{{ "destination" | i18n }}

@@ -62,7 +62,7 @@ -

{{ "data" | i18n }}

+

{{ "data" | i18n }}

@@ -70,7 +70,7 @@ } @else {
-

{{ "keyConnectorDomain" | i18n }}:

+

{{ "keyConnectorDomain" | i18n }}:

{{ keyConnectorUrl }}

diff --git a/libs/state/src/core/state-definitions.ts b/libs/state/src/core/state-definitions.ts index 782dafe1ee2..42d7f5aaaf8 100644 --- a/libs/state/src/core/state-definitions.ts +++ b/libs/state/src/core/state-definitions.ts @@ -40,6 +40,7 @@ export const AUTO_CONFIRM = new StateDefinition("autoConfirm", "disk"); // Billing export const BILLING_DISK = new StateDefinition("billing", "disk"); +export const BILLING_MEMORY = new StateDefinition("billing", "memory"); // Auth diff --git a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html index 3442375315a..2ece050e8c3 100644 --- a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html +++ b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html @@ -1,6 +1,6 @@ -

+

{{ headerText }}

{{ sends.length }} diff --git a/package-lock.json b/package-lock.json index c8f825319e4..9636184c5ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.365", - "@bitwarden/sdk-internal": "0.2.0-main.365", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.369", + "@bitwarden/sdk-internal": "0.2.0-main.369", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4607,9 +4607,9 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.365", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.365.tgz", - "integrity": "sha512-yRc2k29rKMxss6qH2TP91VcE6tNR6/A2ASZMj+Om2MEaanV82zcx89dkShh6RP0jXICM+c/m6BgGkmu+1Pcp8w==", + "version": "0.2.0-main.369", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.369.tgz", + "integrity": "sha512-O+EaPQJQah9j3yWzgw+dwFk5iOxPXdKf1FDeykbt+cxygSYbWTR60RXenG1LysknOdy8fiTfHEaPD+LP1LxrdA==", "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" @@ -4712,9 +4712,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.365", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.365.tgz", - "integrity": "sha512-x0sqAuyknFOGf5ZfbuFTxL0olMiGyyLbJ10tXCYHnrkjdspdNm2BGZc64NQgXz5h+PH1Uwtow/01o/a4F0YTHw==", + "version": "0.2.0-main.369", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.369.tgz", + "integrity": "sha512-gyp4Wd1YbkANA0/RNxHfVk+DuiJqxItzk/YUyQ2HsLeP07xOljftmA0XspLQz59ovs7e1jHMCpH1r/XcyKiQSw==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index 181e003bf28..c1becca3a31 100644 --- a/package.json +++ b/package.json @@ -160,8 +160,8 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.365", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.365", + "@bitwarden/sdk-internal": "0.2.0-main.369", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.369", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0",