mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 00:33:44 +00:00
[PM-5189] Merging changes from main into branch
This commit is contained in:
@@ -218,6 +218,12 @@
|
|||||||
"no-restricted-imports": ["error", { "patterns": ["@bitwarden/platform/*", "src/**/*"] }]
|
"no-restricted-imports": ["error", { "patterns": ["@bitwarden/platform/*", "src/**/*"] }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"files": ["libs/tools/send/send-ui/src/**/*.ts"],
|
||||||
|
"rules": {
|
||||||
|
"no-restricted-imports": ["error", { "patterns": ["@bitwarden/send-ui/*", "src/**/*"] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"files": ["libs/vault/src/**/*.ts"],
|
"files": ["libs/vault/src/**/*.ts"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
|||||||
1
.github/whitelist-capital-letters.txt
vendored
1
.github/whitelist-capital-letters.txt
vendored
@@ -21,6 +21,7 @@
|
|||||||
./libs/platform/README.md
|
./libs/platform/README.md
|
||||||
./libs/tools/README.md
|
./libs/tools/README.md
|
||||||
./libs/tools/export/vault-export/README.md
|
./libs/tools/export/vault-export/README.md
|
||||||
|
./libs/tools/send/README.md
|
||||||
./libs/vault/README.md
|
./libs/vault/README.md
|
||||||
./README.md
|
./README.md
|
||||||
./LICENSE_BITWARDEN.txt
|
./LICENSE_BITWARDEN.txt
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@bitwarden/browser",
|
"name": "@bitwarden/browser",
|
||||||
"version": "2024.5.0",
|
"version": "2024.5.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env MANIFEST_VERSION=3 webpack",
|
"build": "cross-env MANIFEST_VERSION=3 webpack",
|
||||||
"build:mv2": "webpack",
|
"build:mv2": "webpack",
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ export default class MainBackground {
|
|||||||
ssoLoginService: SsoLoginServiceAbstraction;
|
ssoLoginService: SsoLoginServiceAbstraction;
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService;
|
billingAccountProfileStateService: BillingAccountProfileStateService;
|
||||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module
|
// eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module
|
||||||
intraprocessMessagingSubject: Subject<Message<object>>;
|
intraprocessMessagingSubject: Subject<Message<Record<string, unknown>>>;
|
||||||
userAutoUnlockKeyService: UserAutoUnlockKeyService;
|
userAutoUnlockKeyService: UserAutoUnlockKeyService;
|
||||||
scriptInjectorService: BrowserScriptInjectorService;
|
scriptInjectorService: BrowserScriptInjectorService;
|
||||||
kdfConfigService: kdfConfigServiceAbstraction;
|
kdfConfigService: kdfConfigServiceAbstraction;
|
||||||
@@ -384,7 +384,7 @@ export default class MainBackground {
|
|||||||
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
|
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
|
||||||
this.storageService = new BrowserLocalStorageService();
|
this.storageService = new BrowserLocalStorageService();
|
||||||
|
|
||||||
this.intraprocessMessagingSubject = new Subject<Message<object>>();
|
this.intraprocessMessagingSubject = new Subject<Message<Record<string, unknown>>>();
|
||||||
|
|
||||||
this.messagingService = MessageSender.combine(
|
this.messagingService = MessageSender.combine(
|
||||||
new SubjectMessageSender(this.intraprocessMessagingSubject),
|
new SubjectMessageSender(this.intraprocessMessagingSubject),
|
||||||
@@ -840,7 +840,12 @@ export default class MainBackground {
|
|||||||
this.authService,
|
this.authService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.syncServiceListener = new SyncServiceListener(this.syncService, messageListener);
|
this.syncServiceListener = new SyncServiceListener(
|
||||||
|
this.syncService,
|
||||||
|
messageListener,
|
||||||
|
this.messagingService,
|
||||||
|
this.logService,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.eventUploadService = new EventUploadService(
|
this.eventUploadService = new EventUploadService(
|
||||||
this.apiService,
|
this.apiService,
|
||||||
@@ -1171,7 +1176,7 @@ export default class MainBackground {
|
|||||||
this.contextMenusBackground?.init();
|
this.contextMenusBackground?.init();
|
||||||
await this.idleBackground.init();
|
await this.idleBackground.init();
|
||||||
this.webRequestBackground?.startListening();
|
this.webRequestBackground?.startListening();
|
||||||
this.syncServiceListener?.startListening();
|
this.syncServiceListener?.listener$().subscribe();
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"short_name": "__MSG_appName__",
|
"short_name": "__MSG_appName__",
|
||||||
"version": "2024.5.0",
|
"version": "2024.5.1",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Bitwarden Inc.",
|
"author": "Bitwarden Inc.",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"minimum_chrome_version": "102.0",
|
"minimum_chrome_version": "102.0",
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"short_name": "__MSG_appName__",
|
"short_name": "__MSG_appName__",
|
||||||
"version": "2024.5.0",
|
"version": "2024.5.1",
|
||||||
"description": "__MSG_extDesc__",
|
"description": "__MSG_extDesc__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Bitwarden Inc.",
|
"author": "Bitwarden Inc.",
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ const HANDLED_ERRORS: Record<string, ErrorHandler> = {
|
|||||||
export class ChromeMessageSender implements MessageSender {
|
export class ChromeMessageSender implements MessageSender {
|
||||||
constructor(private readonly logService: LogService) {}
|
constructor(private readonly logService: LogService) {}
|
||||||
|
|
||||||
send<T extends object>(
|
send<T extends Record<string, unknown>>(
|
||||||
commandDefinition: string | CommandDefinition<T>,
|
commandDefinition: string | CommandDefinition<T>,
|
||||||
payload: object | T = {},
|
payload: Record<string, unknown> | T = {},
|
||||||
): void {
|
): void {
|
||||||
const command = getCommand(commandDefinition);
|
const command = getCommand(commandDefinition);
|
||||||
chrome.runtime.sendMessage(Object.assign(payload, { command: command }), () => {
|
chrome.runtime.sendMessage(Object.assign(payload, { command: command }), () => {
|
||||||
|
|||||||
130
apps/browser/src/platform/sync/foreground-sync.service.spec.ts
Normal file
130
apps/browser/src/platform/sync/foreground-sync.service.spec.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||||
|
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||||
|
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||||
|
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||||
|
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
|
|
||||||
|
import { DO_FULL_SYNC, ForegroundSyncService, FullSyncMessage } from "./foreground-sync.service";
|
||||||
|
import { FullSyncFinishedMessage } from "./sync-service.listener";
|
||||||
|
|
||||||
|
describe("ForegroundSyncService", () => {
|
||||||
|
const stateService = mock<StateService>();
|
||||||
|
const folderService = mock<InternalFolderService>();
|
||||||
|
const folderApiService = mock<FolderApiServiceAbstraction>();
|
||||||
|
const messageSender = mock<MessageSender>();
|
||||||
|
const logService = mock<LogService>();
|
||||||
|
const cipherService = mock<CipherService>();
|
||||||
|
const collectionService = mock<CollectionService>();
|
||||||
|
const apiService = mock<ApiService>();
|
||||||
|
const accountService = mock<AccountService>();
|
||||||
|
const authService = mock<AuthService>();
|
||||||
|
const sendService = mock<InternalSendService>();
|
||||||
|
const sendApiService = mock<SendApiService>();
|
||||||
|
const messageListener = mock<MessageListener>();
|
||||||
|
|
||||||
|
const sut = new ForegroundSyncService(
|
||||||
|
stateService,
|
||||||
|
folderService,
|
||||||
|
folderApiService,
|
||||||
|
messageSender,
|
||||||
|
logService,
|
||||||
|
cipherService,
|
||||||
|
collectionService,
|
||||||
|
apiService,
|
||||||
|
accountService,
|
||||||
|
authService,
|
||||||
|
sendService,
|
||||||
|
sendApiService,
|
||||||
|
messageListener,
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fullSync", () => {
|
||||||
|
const getAndAssertRequestId = (doFullSyncMessage: Omit<FullSyncMessage, "requestId">) => {
|
||||||
|
expect(messageSender.send).toHaveBeenCalledWith(
|
||||||
|
DO_FULL_SYNC,
|
||||||
|
// We don't know the request id since that is created internally
|
||||||
|
expect.objectContaining(doFullSyncMessage),
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = messageSender.send.mock.calls[0][1];
|
||||||
|
|
||||||
|
if (!("requestId" in message) || typeof message.requestId !== "string") {
|
||||||
|
throw new Error("requestId property of type string was expected on the sent message.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.requestId;
|
||||||
|
};
|
||||||
|
|
||||||
|
it("correctly relays a successful fullSync", async () => {
|
||||||
|
const messages = new Subject<FullSyncFinishedMessage>();
|
||||||
|
messageListener.messages$.mockReturnValue(messages);
|
||||||
|
const fullSyncPromise = sut.fullSync(true, false);
|
||||||
|
expect(sut.syncInProgress).toBe(true);
|
||||||
|
|
||||||
|
const requestId = getAndAssertRequestId({ forceSync: true, allowThrowOnError: false });
|
||||||
|
|
||||||
|
// Pretend the sync has finished
|
||||||
|
messages.next({ successfully: true, errorMessage: null, requestId: requestId });
|
||||||
|
|
||||||
|
const result = await fullSyncPromise;
|
||||||
|
|
||||||
|
expect(sut.syncInProgress).toBe(false);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correctly relays an unsuccessful fullSync but does not throw if allowThrowOnError = false", async () => {
|
||||||
|
const messages = new Subject<FullSyncFinishedMessage>();
|
||||||
|
messageListener.messages$.mockReturnValue(messages);
|
||||||
|
const fullSyncPromise = sut.fullSync(false, false);
|
||||||
|
expect(sut.syncInProgress).toBe(true);
|
||||||
|
|
||||||
|
const requestId = getAndAssertRequestId({ forceSync: false, allowThrowOnError: false });
|
||||||
|
|
||||||
|
// Pretend the sync has finished
|
||||||
|
messages.next({
|
||||||
|
successfully: false,
|
||||||
|
errorMessage: "Error while syncing",
|
||||||
|
requestId: requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fullSyncPromise;
|
||||||
|
|
||||||
|
expect(sut.syncInProgress).toBe(false);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("correctly relays an unsuccessful fullSync but and will throw if allowThrowOnError = true", async () => {
|
||||||
|
const messages = new Subject<FullSyncFinishedMessage>();
|
||||||
|
messageListener.messages$.mockReturnValue(messages);
|
||||||
|
const fullSyncPromise = sut.fullSync(true, true);
|
||||||
|
expect(sut.syncInProgress).toBe(true);
|
||||||
|
|
||||||
|
const requestId = getAndAssertRequestId({ forceSync: true, allowThrowOnError: true });
|
||||||
|
|
||||||
|
// Pretend the sync has finished
|
||||||
|
messages.next({
|
||||||
|
successfully: false,
|
||||||
|
errorMessage: "Error while syncing",
|
||||||
|
requestId: requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(fullSyncPromise).rejects.toThrow("Error while syncing");
|
||||||
|
|
||||||
|
expect(sut.syncInProgress).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { firstValueFrom, timeout } from "rxjs";
|
import { filter, firstValueFrom, of, timeout } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
MessageListener,
|
MessageListener,
|
||||||
MessageSender,
|
MessageSender,
|
||||||
} from "@bitwarden/common/platform/messaging";
|
} from "@bitwarden/common/platform/messaging";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CoreSyncService } from "@bitwarden/common/platform/sync/internal";
|
import { CoreSyncService } from "@bitwarden/common/platform/sync/internal";
|
||||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||||
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||||
@@ -18,11 +19,11 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
|
|||||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
|
|
||||||
const SYNC_COMPLETED = new CommandDefinition<{ successfully: boolean }>("syncCompleted");
|
import { FULL_SYNC_FINISHED } from "./sync-service.listener";
|
||||||
export const DO_FULL_SYNC = new CommandDefinition<{
|
|
||||||
forceSync: boolean;
|
export type FullSyncMessage = { forceSync: boolean; allowThrowOnError: boolean; requestId: string };
|
||||||
allowThrowOnError: boolean;
|
|
||||||
}>("doFullSync");
|
export const DO_FULL_SYNC = new CommandDefinition<FullSyncMessage>("doFullSync");
|
||||||
|
|
||||||
export class ForegroundSyncService extends CoreSyncService {
|
export class ForegroundSyncService extends CoreSyncService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -59,18 +60,29 @@ export class ForegroundSyncService extends CoreSyncService {
|
|||||||
async fullSync(forceSync: boolean, allowThrowOnError: boolean = false): Promise<boolean> {
|
async fullSync(forceSync: boolean, allowThrowOnError: boolean = false): Promise<boolean> {
|
||||||
this.syncInProgress = true;
|
this.syncInProgress = true;
|
||||||
try {
|
try {
|
||||||
|
const requestId = Utils.newGuid();
|
||||||
const syncCompletedPromise = firstValueFrom(
|
const syncCompletedPromise = firstValueFrom(
|
||||||
this.messageListener.messages$(SYNC_COMPLETED).pipe(
|
this.messageListener.messages$(FULL_SYNC_FINISHED).pipe(
|
||||||
|
filter((m) => m.requestId === requestId),
|
||||||
timeout({
|
timeout({
|
||||||
first: 10_000,
|
first: 30_000,
|
||||||
|
// If we haven't heard back in 30 seconds, just pretend we heard back about an unsuccesful sync.
|
||||||
with: () => {
|
with: () => {
|
||||||
throw new Error("Timeout while doing a fullSync call.");
|
this.logService.warning(
|
||||||
|
"ForegroundSyncService did not receive a message back in a reasonable time.",
|
||||||
|
);
|
||||||
|
return of({ successfully: false, errorMessage: "Sync timed out." });
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
this.messageSender.send(DO_FULL_SYNC, { forceSync, allowThrowOnError });
|
this.messageSender.send(DO_FULL_SYNC, { forceSync, allowThrowOnError, requestId });
|
||||||
const result = await syncCompletedPromise;
|
const result = await syncCompletedPromise;
|
||||||
|
|
||||||
|
if (allowThrowOnError && result.errorMessage != null) {
|
||||||
|
throw new Error(result.errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
return result.successfully;
|
return result.successfully;
|
||||||
} finally {
|
} finally {
|
||||||
this.syncInProgress = false;
|
this.syncInProgress = false;
|
||||||
|
|||||||
60
apps/browser/src/platform/sync/sync-service.listener.spec.ts
Normal file
60
apps/browser/src/platform/sync/sync-service.listener.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { Subject, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||||
|
import { tagAsExternal } from "@bitwarden/common/platform/messaging/helpers";
|
||||||
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
|
import { FullSyncMessage } from "./foreground-sync.service";
|
||||||
|
import { FULL_SYNC_FINISHED, SyncServiceListener } from "./sync-service.listener";
|
||||||
|
|
||||||
|
describe("SyncServiceListener", () => {
|
||||||
|
const syncService = mock<SyncService>();
|
||||||
|
const messageListener = mock<MessageListener>();
|
||||||
|
const messageSender = mock<MessageSender>();
|
||||||
|
const logService = mock<LogService>();
|
||||||
|
|
||||||
|
const messages = new Subject<FullSyncMessage>();
|
||||||
|
messageListener.messages$.mockReturnValue(messages.asObservable().pipe(tagAsExternal()));
|
||||||
|
const sut = new SyncServiceListener(syncService, messageListener, messageSender, logService);
|
||||||
|
|
||||||
|
describe("listener$", () => {
|
||||||
|
it.each([true, false])(
|
||||||
|
"calls full sync and relays outcome when sync is [successfully = %s]",
|
||||||
|
async (value) => {
|
||||||
|
const listener = sut.listener$();
|
||||||
|
const emissionPromise = firstValueFrom(listener);
|
||||||
|
|
||||||
|
syncService.fullSync.mockResolvedValueOnce(value);
|
||||||
|
messages.next({ forceSync: true, allowThrowOnError: false, requestId: "1" });
|
||||||
|
|
||||||
|
await emissionPromise;
|
||||||
|
|
||||||
|
expect(syncService.fullSync).toHaveBeenCalledWith(true, false);
|
||||||
|
expect(messageSender.send).toHaveBeenCalledWith(FULL_SYNC_FINISHED, {
|
||||||
|
successfully: value,
|
||||||
|
errorMessage: null,
|
||||||
|
requestId: "1",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("calls full sync and relays error message through messaging", async () => {
|
||||||
|
const listener = sut.listener$();
|
||||||
|
const emissionPromise = firstValueFrom(listener);
|
||||||
|
|
||||||
|
syncService.fullSync.mockRejectedValueOnce(new Error("SyncError"));
|
||||||
|
messages.next({ forceSync: true, allowThrowOnError: false, requestId: "1" });
|
||||||
|
|
||||||
|
await emissionPromise;
|
||||||
|
|
||||||
|
expect(syncService.fullSync).toHaveBeenCalledWith(true, false);
|
||||||
|
expect(messageSender.send).toHaveBeenCalledWith(FULL_SYNC_FINISHED, {
|
||||||
|
successfully: false,
|
||||||
|
errorMessage: "SyncError",
|
||||||
|
requestId: "1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,25 +1,58 @@
|
|||||||
import { Subscription, concatMap, filter } from "rxjs";
|
import { Observable, concatMap, filter } from "rxjs";
|
||||||
|
|
||||||
import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import {
|
||||||
|
CommandDefinition,
|
||||||
|
MessageListener,
|
||||||
|
MessageSender,
|
||||||
|
isExternalMessage,
|
||||||
|
} from "@bitwarden/common/platform/messaging";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
import { DO_FULL_SYNC } from "./foreground-sync.service";
|
import { DO_FULL_SYNC } from "./foreground-sync.service";
|
||||||
|
|
||||||
|
export type FullSyncFinishedMessage = {
|
||||||
|
successfully: boolean;
|
||||||
|
errorMessage: string;
|
||||||
|
requestId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FULL_SYNC_FINISHED = new CommandDefinition<FullSyncFinishedMessage>(
|
||||||
|
"fullSyncFinished",
|
||||||
|
);
|
||||||
|
|
||||||
export class SyncServiceListener {
|
export class SyncServiceListener {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly syncService: SyncService,
|
private readonly syncService: SyncService,
|
||||||
private readonly messageListener: MessageListener,
|
private readonly messageListener: MessageListener,
|
||||||
|
private readonly messageSender: MessageSender,
|
||||||
|
private readonly logService: LogService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
startListening(): Subscription {
|
listener$(): Observable<void> {
|
||||||
return this.messageListener
|
return this.messageListener.messages$(DO_FULL_SYNC).pipe(
|
||||||
.messages$(DO_FULL_SYNC)
|
filter((message) => isExternalMessage(message)),
|
||||||
.pipe(
|
concatMap(async ({ forceSync, allowThrowOnError, requestId }) => {
|
||||||
filter((message) => isExternalMessage(message)),
|
await this.doFullSync(forceSync, allowThrowOnError, requestId);
|
||||||
concatMap(async ({ forceSync, allowThrowOnError }) => {
|
}),
|
||||||
await this.syncService.fullSync(forceSync, allowThrowOnError);
|
);
|
||||||
}),
|
}
|
||||||
)
|
|
||||||
.subscribe();
|
private async doFullSync(forceSync: boolean, allowThrowOnError: boolean, requestId: string) {
|
||||||
|
try {
|
||||||
|
const result = await this.syncService.fullSync(forceSync, allowThrowOnError);
|
||||||
|
this.messageSender.send(FULL_SYNC_FINISHED, {
|
||||||
|
successfully: result,
|
||||||
|
errorMessage: null,
|
||||||
|
requestId,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.logService.warning("Error while doing full sync in SyncServiceListener", err);
|
||||||
|
this.messageSender.send(FULL_SYNC_FINISHED, {
|
||||||
|
successfully: false,
|
||||||
|
errorMessage: err?.message ?? "Unknown Sync Error",
|
||||||
|
requestId,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { map, share } from "rxjs";
|
import { map, share } from "rxjs";
|
||||||
|
|
||||||
|
import { Message } from "@bitwarden/common/platform/messaging";
|
||||||
import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal";
|
import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal";
|
||||||
|
|
||||||
import { fromChromeEvent } from "../browser/from-chrome-event";
|
import { fromChromeEvent } from "../browser/from-chrome-event";
|
||||||
@@ -20,7 +21,7 @@ export const fromChromeRuntimeMessaging = () => {
|
|||||||
|
|
||||||
return message;
|
return message;
|
||||||
}),
|
}),
|
||||||
tagAsExternal,
|
tagAsExternal<Message<Record<string, unknown>>>(),
|
||||||
share(),
|
share(),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -524,7 +524,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: MessageListener,
|
provide: MessageListener,
|
||||||
useFactory: (subject: Subject<Message<object>>, ngZone: NgZone) =>
|
useFactory: (subject: Subject<Message<Record<string, unknown>>>, ngZone: NgZone) =>
|
||||||
new MessageListener(
|
new MessageListener(
|
||||||
merge(
|
merge(
|
||||||
subject.asObservable(), // For messages in the same context
|
subject.asObservable(), // For messages in the same context
|
||||||
@@ -535,7 +535,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: MessageSender,
|
provide: MessageSender,
|
||||||
useFactory: (subject: Subject<Message<object>>, logService: LogService) =>
|
useFactory: (subject: Subject<Message<Record<string, unknown>>>, logService: LogService) =>
|
||||||
MessageSender.combine(
|
MessageSender.combine(
|
||||||
new SubjectMessageSender(subject), // For sending messages in the same context
|
new SubjectMessageSender(subject), // For sending messages in the same context
|
||||||
new ChromeMessageSender(logService), // For sending messages to different contexts
|
new ChromeMessageSender(logService), // For sending messages to different contexts
|
||||||
@@ -550,14 +550,14 @@ const safeProviders: SafeProvider[] = [
|
|||||||
// we need the same instance that our in memory background is utilizing.
|
// we need the same instance that our in memory background is utilizing.
|
||||||
return getBgService("intraprocessMessagingSubject")();
|
return getBgService("intraprocessMessagingSubject")();
|
||||||
} else {
|
} else {
|
||||||
return new Subject<Message<object>>();
|
return new Subject<Message<Record<string, unknown>>>();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: MessageSender,
|
provide: MessageSender,
|
||||||
useFactory: (subject: Subject<Message<object>>, logService: LogService) =>
|
useFactory: (subject: Subject<Message<Record<string, unknown>>>, logService: LogService) =>
|
||||||
MessageSender.combine(
|
MessageSender.combine(
|
||||||
new SubjectMessageSender(subject), // For sending messages in the same context
|
new SubjectMessageSender(subject), // For sending messages in the same context
|
||||||
new ChromeMessageSender(logService), // For sending messages to different contexts
|
new ChromeMessageSender(logService), // For sending messages to different contexts
|
||||||
@@ -576,7 +576,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
// There isn't a locally created background so we will communicate with
|
// There isn't a locally created background so we will communicate with
|
||||||
// the true background through chrome apis, in that case, we can just create
|
// the true background through chrome apis, in that case, we can just create
|
||||||
// one for ourself.
|
// one for ourself.
|
||||||
return new Subject<Message<object>>();
|
return new Subject<Message<Record<string, unknown>>>();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deps: [],
|
deps: [],
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="tw-mb-2">
|
||||||
|
<bit-search
|
||||||
|
[placeholder]="'search' | i18n"
|
||||||
|
[(ngModel)]="searchText"
|
||||||
|
(ngModelChange)="onSearchTextChanged()"
|
||||||
|
>
|
||||||
|
</bit-search>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, Output, EventEmitter } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
import { FormsModule } from "@angular/forms";
|
||||||
|
import { Subject, debounceTime } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { SearchModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
const SearchTextDebounceInterval = 200;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
imports: [CommonModule, SearchModule, JslibModule, FormsModule],
|
||||||
|
standalone: true,
|
||||||
|
selector: "app-vault-v2-search",
|
||||||
|
templateUrl: "vault-v2-search.component.html",
|
||||||
|
})
|
||||||
|
export class VaultV2SearchComponent {
|
||||||
|
searchText: string;
|
||||||
|
@Output() searchTextChanged = new EventEmitter<string>();
|
||||||
|
|
||||||
|
private searchText$ = new Subject<string>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.searchText$
|
||||||
|
.pipe(debounceTime(SearchTextDebounceInterval), takeUntilDestroyed())
|
||||||
|
.subscribe((data) => {
|
||||||
|
this.searchTextChanged.emit(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchTextChanged() {
|
||||||
|
this.searchText$.next(this.searchText);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,9 @@
|
|||||||
<ng-container *ngIf="!(showEmptyState$ | async)">
|
<ng-container *ngIf="!(showEmptyState$ | async)">
|
||||||
<!-- TODO: Filter/search Section in PM-6824 and PM-6826.-->
|
<!-- TODO: Filter/search Section in PM-6824 and PM-6826.-->
|
||||||
|
|
||||||
|
<app-vault-v2-search (searchTextChanged)="handleSearchTextChange($event)">
|
||||||
|
</app-vault-v2-search>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
*ngIf="showNoResultsState$ | async"
|
*ngIf="showNoResultsState$ | async"
|
||||||
class="tw-flex tw-flex-col tw-h-full tw-justify-center"
|
class="tw-flex tw-flex-col tw-h-full tw-justify-center"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he
|
|||||||
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
||||||
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
|
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
|
||||||
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
|
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
|
||||||
|
import { VaultV2SearchComponent } from "../vault-v2/vault-search/vault-v2-search.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-vault",
|
selector: "app-vault",
|
||||||
@@ -28,6 +29,7 @@ import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } fro
|
|||||||
VaultListItemsContainerComponent,
|
VaultListItemsContainerComponent,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
|
VaultV2SearchComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class VaultV2Component implements OnInit, OnDestroy {
|
export class VaultV2Component implements OnInit, OnDestroy {
|
||||||
@@ -48,6 +50,10 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnDestroy(): void {}
|
ngOnDestroy(): void {}
|
||||||
|
|
||||||
|
handleSearchTextChange(searchText: string) {
|
||||||
|
this.vaultPopupItemsService.applyFilter(searchText);
|
||||||
|
}
|
||||||
|
|
||||||
addCipher() {
|
addCipher() {
|
||||||
// TODO: Add currently filtered organization to query params if available
|
// TODO: Add currently filtered organization to query params if available
|
||||||
void this.router.navigate(["/add-cipher"], {});
|
void this.router.navigate(["/add-cipher"], {});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { CipherId } from "@bitwarden/common/types/guid";
|
import { CipherId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
@@ -19,6 +20,7 @@ describe("VaultPopupItemsService", () => {
|
|||||||
|
|
||||||
const cipherServiceMock = mock<CipherService>();
|
const cipherServiceMock = mock<CipherService>();
|
||||||
const vaultSettingsServiceMock = mock<VaultSettingsService>();
|
const vaultSettingsServiceMock = mock<VaultSettingsService>();
|
||||||
|
const searchService = mock<SearchService>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
allCiphers = cipherFactory(10);
|
allCiphers = cipherFactory(10);
|
||||||
@@ -34,6 +36,7 @@ describe("VaultPopupItemsService", () => {
|
|||||||
cipherList[3].favorite = true;
|
cipherList[3].favorite = true;
|
||||||
|
|
||||||
cipherServiceMock.cipherViews$ = new BehaviorSubject(allCiphers).asObservable();
|
cipherServiceMock.cipherViews$ = new BehaviorSubject(allCiphers).asObservable();
|
||||||
|
searchService.searchCiphers.mockImplementation(async () => cipherList);
|
||||||
cipherServiceMock.filterCiphersForUrl.mockImplementation(async () => autoFillCiphers);
|
cipherServiceMock.filterCiphersForUrl.mockImplementation(async () => autoFillCiphers);
|
||||||
vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false).asObservable();
|
vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false).asObservable();
|
||||||
vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false).asObservable();
|
vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false).asObservable();
|
||||||
@@ -41,11 +44,19 @@ describe("VaultPopupItemsService", () => {
|
|||||||
jest
|
jest
|
||||||
.spyOn(BrowserApi, "getTabFromCurrentWindow")
|
.spyOn(BrowserApi, "getTabFromCurrentWindow")
|
||||||
.mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab);
|
.mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab);
|
||||||
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
|
service = new VaultPopupItemsService(
|
||||||
|
cipherServiceMock,
|
||||||
|
vaultSettingsServiceMock,
|
||||||
|
searchService,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be created", () => {
|
it("should be created", () => {
|
||||||
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
|
service = new VaultPopupItemsService(
|
||||||
|
cipherServiceMock,
|
||||||
|
vaultSettingsServiceMock,
|
||||||
|
searchService,
|
||||||
|
);
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,7 +84,11 @@ describe("VaultPopupItemsService", () => {
|
|||||||
vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(true).asObservable();
|
vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(true).asObservable();
|
||||||
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(currentTab);
|
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(currentTab);
|
||||||
|
|
||||||
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
|
service = new VaultPopupItemsService(
|
||||||
|
cipherServiceMock,
|
||||||
|
vaultSettingsServiceMock,
|
||||||
|
searchService,
|
||||||
|
);
|
||||||
|
|
||||||
service.autoFillCiphers$.subscribe((ciphers) => {
|
service.autoFillCiphers$.subscribe((ciphers) => {
|
||||||
expect(cipherServiceMock.filterCiphersForUrl.mock.calls.length).toBe(1);
|
expect(cipherServiceMock.filterCiphersForUrl.mock.calls.length).toBe(1);
|
||||||
@@ -99,7 +114,11 @@ describe("VaultPopupItemsService", () => {
|
|||||||
Object.values(allCiphers),
|
Object.values(allCiphers),
|
||||||
);
|
);
|
||||||
|
|
||||||
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
|
service = new VaultPopupItemsService(
|
||||||
|
cipherServiceMock,
|
||||||
|
vaultSettingsServiceMock,
|
||||||
|
searchService,
|
||||||
|
);
|
||||||
|
|
||||||
service.autoFillCiphers$.subscribe((ciphers) => {
|
service.autoFillCiphers$.subscribe((ciphers) => {
|
||||||
expect(ciphers.length).toBe(10);
|
expect(ciphers.length).toBe(10);
|
||||||
@@ -114,6 +133,24 @@ describe("VaultPopupItemsService", () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should filter autoFillCiphers$ down to search term", (done) => {
|
||||||
|
const cipherList = Object.values(allCiphers);
|
||||||
|
const searchText = "Login";
|
||||||
|
|
||||||
|
searchService.searchCiphers.mockImplementation(async () => {
|
||||||
|
return cipherList.filter((cipher) => {
|
||||||
|
return cipher.name.includes(searchText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// there is only 1 Login returned for filteredCiphers. but two results expected because of other autofill types
|
||||||
|
service.autoFillCiphers$.subscribe((ciphers) => {
|
||||||
|
expect(ciphers[0].name.includes(searchText)).toBe(true);
|
||||||
|
expect(ciphers.length).toBe(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("favoriteCiphers$", () => {
|
describe("favoriteCiphers$", () => {
|
||||||
@@ -131,6 +168,24 @@ describe("VaultPopupItemsService", () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should filter favoriteCiphers$ down to search term", (done) => {
|
||||||
|
const cipherList = Object.values(allCiphers);
|
||||||
|
const searchText = "Card 2";
|
||||||
|
|
||||||
|
searchService.searchCiphers.mockImplementation(async () => {
|
||||||
|
return cipherList.filter((cipher) => {
|
||||||
|
return cipher.name === searchText;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
service.favoriteCiphers$.subscribe((ciphers) => {
|
||||||
|
// There are 2 favorite items but only one Card 2
|
||||||
|
expect(ciphers[0].name).toBe(searchText);
|
||||||
|
expect(ciphers.length).toBe(1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("remainingCiphers$", () => {
|
describe("remainingCiphers$", () => {
|
||||||
@@ -148,12 +203,33 @@ describe("VaultPopupItemsService", () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should filter remainingCiphers$ down to search term", (done) => {
|
||||||
|
const cipherList = Object.values(allCiphers);
|
||||||
|
const searchText = "Login";
|
||||||
|
|
||||||
|
searchService.searchCiphers.mockImplementation(async () => {
|
||||||
|
return cipherList.filter((cipher) => {
|
||||||
|
return cipher.name.includes(searchText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
service.remainingCiphers$.subscribe((ciphers) => {
|
||||||
|
// There are 6 remaining ciphers but only 2 with "Login" in the name
|
||||||
|
expect(ciphers.length).toBe(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("emptyVault$", () => {
|
describe("emptyVault$", () => {
|
||||||
it("should return true if there are no ciphers", (done) => {
|
it("should return true if there are no ciphers", (done) => {
|
||||||
cipherServiceMock.cipherViews$ = new BehaviorSubject({}).asObservable();
|
cipherServiceMock.cipherViews$ = new BehaviorSubject({}).asObservable();
|
||||||
service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
|
service = new VaultPopupItemsService(
|
||||||
|
cipherServiceMock,
|
||||||
|
vaultSettingsServiceMock,
|
||||||
|
searchService,
|
||||||
|
);
|
||||||
service.emptyVault$.subscribe((empty) => {
|
service.emptyVault$.subscribe((empty) => {
|
||||||
expect(empty).toBe(true);
|
expect(empty).toBe(true);
|
||||||
done();
|
done();
|
||||||
@@ -192,6 +268,54 @@ describe("VaultPopupItemsService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("noFilteredResults$", () => {
|
||||||
|
it("should return false when filteredResults has values", (done) => {
|
||||||
|
service.noFilteredResults$.subscribe((noResults) => {
|
||||||
|
expect(noResults).toBe(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true when there are zero filteredResults", (done) => {
|
||||||
|
searchService.searchCiphers.mockImplementation(async () => []);
|
||||||
|
service.noFilteredResults$.subscribe((noResults) => {
|
||||||
|
expect(noResults).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hasFilterApplied$", () => {
|
||||||
|
it("should return true if the search term provided is searchable", (done) => {
|
||||||
|
searchService.isSearchable.mockImplementation(async () => true);
|
||||||
|
service.hasFilterApplied$.subscribe((canSearch) => {
|
||||||
|
expect(canSearch).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if the search term provided is not searchable", (done) => {
|
||||||
|
searchService.isSearchable.mockImplementation(async () => false);
|
||||||
|
service.hasFilterApplied$.subscribe((canSearch) => {
|
||||||
|
expect(canSearch).toBe(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyFilter", () => {
|
||||||
|
it("should call search Service with the new search term", (done) => {
|
||||||
|
const searchText = "Hello";
|
||||||
|
service.applyFilter(searchText);
|
||||||
|
const searchServiceSpy = jest.spyOn(searchService, "searchCiphers");
|
||||||
|
|
||||||
|
service.favoriteCiphers$.subscribe(() => {
|
||||||
|
expect(searchServiceSpy).toHaveBeenCalledWith(searchText, null, expect.anything());
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// A function to generate a list of ciphers of different types
|
// A function to generate a list of ciphers of different types
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import {
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
switchMap,
|
switchMap,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
@@ -26,6 +28,7 @@ import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
|||||||
})
|
})
|
||||||
export class VaultPopupItemsService {
|
export class VaultPopupItemsService {
|
||||||
private _refreshCurrentTab$ = new Subject<void>();
|
private _refreshCurrentTab$ = new Subject<void>();
|
||||||
|
private searchText$ = new BehaviorSubject<string>("");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable that contains the list of other cipher types that should be shown
|
* Observable that contains the list of other cipher types that should be shown
|
||||||
@@ -69,6 +72,13 @@ export class VaultPopupItemsService {
|
|||||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _filteredCipherList$ = combineLatest([this._cipherList$, this.searchText$]).pipe(
|
||||||
|
switchMap(([ciphers, searchText]) =>
|
||||||
|
this.searchService.searchCiphers(searchText, null, ciphers),
|
||||||
|
),
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of ciphers that can be used for autofill on the current tab. Includes cards and/or identities
|
* List of ciphers that can be used for autofill on the current tab. Includes cards and/or identities
|
||||||
* if enabled in the vault settings. Ciphers are sorted by type, then by last used date, then by name.
|
* if enabled in the vault settings. Ciphers are sorted by type, then by last used date, then by name.
|
||||||
@@ -76,7 +86,7 @@ export class VaultPopupItemsService {
|
|||||||
* See {@link refreshCurrentTab} to trigger re-evaluation of the current tab.
|
* See {@link refreshCurrentTab} to trigger re-evaluation of the current tab.
|
||||||
*/
|
*/
|
||||||
autoFillCiphers$: Observable<CipherView[]> = combineLatest([
|
autoFillCiphers$: Observable<CipherView[]> = combineLatest([
|
||||||
this._cipherList$,
|
this._filteredCipherList$,
|
||||||
this._otherAutoFillTypes$,
|
this._otherAutoFillTypes$,
|
||||||
this._currentAutofillTab$,
|
this._currentAutofillTab$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
@@ -96,7 +106,7 @@ export class VaultPopupItemsService {
|
|||||||
*/
|
*/
|
||||||
favoriteCiphers$: Observable<CipherView[]> = combineLatest([
|
favoriteCiphers$: Observable<CipherView[]> = combineLatest([
|
||||||
this.autoFillCiphers$,
|
this.autoFillCiphers$,
|
||||||
this._cipherList$,
|
this._filteredCipherList$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([autoFillCiphers, ciphers]) =>
|
map(([autoFillCiphers, ciphers]) =>
|
||||||
ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)),
|
ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)),
|
||||||
@@ -114,7 +124,7 @@ export class VaultPopupItemsService {
|
|||||||
remainingCiphers$: Observable<CipherView[]> = combineLatest([
|
remainingCiphers$: Observable<CipherView[]> = combineLatest([
|
||||||
this.autoFillCiphers$,
|
this.autoFillCiphers$,
|
||||||
this.favoriteCiphers$,
|
this.favoriteCiphers$,
|
||||||
this._cipherList$,
|
this._filteredCipherList$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([autoFillCiphers, favoriteCiphers, ciphers]) =>
|
map(([autoFillCiphers, favoriteCiphers, ciphers]) =>
|
||||||
ciphers.filter(
|
ciphers.filter(
|
||||||
@@ -129,7 +139,9 @@ export class VaultPopupItemsService {
|
|||||||
* Observable that indicates whether a filter is currently applied to the ciphers.
|
* Observable that indicates whether a filter is currently applied to the ciphers.
|
||||||
* @todo Implement filter/search functionality in PM-6824 and PM-6826.
|
* @todo Implement filter/search functionality in PM-6824 and PM-6826.
|
||||||
*/
|
*/
|
||||||
hasFilterApplied$: Observable<boolean> = of(false);
|
hasFilterApplied$: Observable<boolean> = this.searchText$.pipe(
|
||||||
|
switchMap((text) => this.searchService.isSearchable(text)),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable that indicates whether autofill is allowed in the current context.
|
* Observable that indicates whether autofill is allowed in the current context.
|
||||||
@@ -146,11 +158,14 @@ export class VaultPopupItemsService {
|
|||||||
* Observable that indicates whether there are no ciphers to show with the current filter.
|
* Observable that indicates whether there are no ciphers to show with the current filter.
|
||||||
* @todo Implement filter/search functionality in PM-6824 and PM-6826.
|
* @todo Implement filter/search functionality in PM-6824 and PM-6826.
|
||||||
*/
|
*/
|
||||||
noFilteredResults$: Observable<boolean> = of(false);
|
noFilteredResults$: Observable<boolean> = this._filteredCipherList$.pipe(
|
||||||
|
map((ciphers) => !ciphers.length),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private vaultSettingsService: VaultSettingsService,
|
private vaultSettingsService: VaultSettingsService,
|
||||||
|
private searchService: SearchService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,6 +175,10 @@ export class VaultPopupItemsService {
|
|||||||
this._refreshCurrentTab$.next(null);
|
this._refreshCurrentTab$.next(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyFilter(newSearchText: string) {
|
||||||
|
this.searchText$.next(newSearchText);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort function for ciphers to be used in the autofill section of the Vault tab.
|
* Sort function for ciphers to be used in the autofill section of the Vault tab.
|
||||||
* Sorts by type, then by last used date, and finally by name.
|
* Sorts by type, then by last used date, and finally by name.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"@bitwarden/importer/core": ["../../libs/importer/src"],
|
"@bitwarden/importer/core": ["../../libs/importer/src"],
|
||||||
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
|
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
|
||||||
"@bitwarden/platform": ["../../libs/platform/src"],
|
"@bitwarden/platform": ["../../libs/platform/src"],
|
||||||
|
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
|
||||||
"@bitwarden/vault": ["../../libs/vault/src"]
|
"@bitwarden/vault": ["../../libs/vault/src"]
|
||||||
},
|
},
|
||||||
"useDefineForClassFields": false
|
"useDefineForClassFields": false
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: MessageSender,
|
provide: MessageSender,
|
||||||
useFactory: (subject: Subject<Message<object>>) =>
|
useFactory: (subject: Subject<Message<Record<string, unknown>>>) =>
|
||||||
MessageSender.combine(
|
MessageSender.combine(
|
||||||
new ElectronRendererMessageSender(), // Communication with main process
|
new ElectronRendererMessageSender(), // Communication with main process
|
||||||
new SubjectMessageSender(subject), // Communication with ourself
|
new SubjectMessageSender(subject), // Communication with ourself
|
||||||
@@ -160,7 +160,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: MessageListener,
|
provide: MessageListener,
|
||||||
useFactory: (subject: Subject<Message<object>>) =>
|
useFactory: (subject: Subject<Message<Record<string, unknown>>>) =>
|
||||||
new MessageListener(
|
new MessageListener(
|
||||||
merge(
|
merge(
|
||||||
subject.asObservable(), // For messages from the same context
|
subject.asObservable(), // For messages from the same context
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ export class Main {
|
|||||||
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain);
|
this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain);
|
||||||
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService);
|
this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService);
|
||||||
|
|
||||||
const messageSubject = new Subject<Message<object>>();
|
const messageSubject = new Subject<Message<Record<string, unknown>>>();
|
||||||
this.messagingService = MessageSender.combine(
|
this.messagingService = MessageSender.combine(
|
||||||
new SubjectMessageSender(messageSubject), // For local messages
|
new SubjectMessageSender(messageSubject), // For local messages
|
||||||
new ElectronMainMessagingService(this.windowMain),
|
new ElectronMainMessagingService(this.windowMain),
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export class TrayMain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateContextMenu() {
|
updateContextMenu() {
|
||||||
if (this.contextMenu != null && this.isLinux()) {
|
if (this.tray != null && this.contextMenu != null && this.isLinux()) {
|
||||||
this.tray.setContextMenu(this.contextMenu);
|
this.tray.setContextMenu(this.contextMenu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/mes
|
|||||||
import { getCommand } from "@bitwarden/common/platform/messaging/internal";
|
import { getCommand } from "@bitwarden/common/platform/messaging/internal";
|
||||||
|
|
||||||
export class ElectronRendererMessageSender implements MessageSender {
|
export class ElectronRendererMessageSender implements MessageSender {
|
||||||
send<T extends object>(
|
send<T extends Record<string, unknown>>(
|
||||||
commandDefinition: CommandDefinition<T> | string,
|
commandDefinition: CommandDefinition<T> | string,
|
||||||
payload: object | T = {},
|
payload: Record<string, unknown> | T = {},
|
||||||
): void {
|
): void {
|
||||||
const command = getCommand(commandDefinition);
|
const command = getCommand(commandDefinition);
|
||||||
ipc.platform.sendMessage(Object.assign({}, { command: command }, payload));
|
ipc.platform.sendMessage(Object.assign({}, { command: command }, payload));
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal";
|
|||||||
* @returns An observable stream of messages.
|
* @returns An observable stream of messages.
|
||||||
*/
|
*/
|
||||||
export const fromIpcMessaging = () => {
|
export const fromIpcMessaging = () => {
|
||||||
return fromEventPattern<Message<object>>(
|
return fromEventPattern<Message<Record<string, unknown>>>(
|
||||||
(handler) => ipc.platform.onMessage.addListener(handler),
|
(handler) => ipc.platform.onMessage.addListener(handler),
|
||||||
(handler) => ipc.platform.onMessage.removeListener(handler),
|
(handler) => ipc.platform.onMessage.removeListener(handler),
|
||||||
).pipe(tagAsExternal, share());
|
).pipe(tagAsExternal(), share());
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ export class ElectronMainMessagingService implements MessageSender {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
send<T extends object>(commandDefinition: CommandDefinition<T> | string, arg: T | object = {}) {
|
send<T extends Record<string, unknown>>(
|
||||||
|
commandDefinition: CommandDefinition<T> | string,
|
||||||
|
arg: T | Record<string, unknown> = {},
|
||||||
|
) {
|
||||||
const command = getCommand(commandDefinition);
|
const command = getCommand(commandDefinition);
|
||||||
const message = Object.assign({}, { command: command }, arg);
|
const message = Object.assign({}, { command: command }, arg);
|
||||||
if (this.windowMain.win != null) {
|
if (this.windowMain.win != null) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
|
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
|
||||||
"@bitwarden/node/*": ["../../libs/node/src/*"],
|
"@bitwarden/node/*": ["../../libs/node/src/*"],
|
||||||
"@bitwarden/platform": ["../../libs/platform/src"],
|
"@bitwarden/platform": ["../../libs/platform/src"],
|
||||||
|
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
|
||||||
"@bitwarden/vault": ["../../libs/vault/src"]
|
"@bitwarden/vault": ["../../libs/vault/src"]
|
||||||
},
|
},
|
||||||
"useDefineForClassFields": false
|
"useDefineForClassFields": false
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
<ng-container bitDialogContent>
|
<ng-container bitDialogContent>
|
||||||
<app-user-verification-form-input
|
<app-user-verification-form-input
|
||||||
formControlName="secret"
|
formControlName="secret"
|
||||||
ngDefaultControl
|
|
||||||
name="secret"
|
name="secret"
|
||||||
|
[(invalidSecret)]="invalidSecret"
|
||||||
></app-user-verification-form-input>
|
></app-user-verification-form-input>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container bitDialogFooter>
|
<ng-container bitDialogFooter>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request
|
|||||||
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||||
import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response";
|
import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response";
|
||||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||||
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ export class TwoFactorVerifyComponent {
|
|||||||
protected formGroup = new FormGroup({
|
protected formGroup = new FormGroup({
|
||||||
secret: new FormControl<Verification | null>(null),
|
secret: new FormControl<Verification | null>(null),
|
||||||
});
|
});
|
||||||
|
invalidSecret: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) protected data: TwoFactorVerifyDialogData,
|
@Inject(DIALOG_DATA) protected data: TwoFactorVerifyDialogData,
|
||||||
@@ -45,23 +47,30 @@ export class TwoFactorVerifyComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
let hashedSecret: string;
|
try {
|
||||||
this.formPromise = this.userVerificationService
|
let hashedSecret: string;
|
||||||
.buildRequest(this.formGroup.value.secret)
|
this.formPromise = this.userVerificationService
|
||||||
.then((request) => {
|
.buildRequest(this.formGroup.value.secret)
|
||||||
hashedSecret =
|
.then((request) => {
|
||||||
this.formGroup.value.secret.type === VerificationType.MasterPassword
|
hashedSecret =
|
||||||
? request.masterPasswordHash
|
this.formGroup.value.secret.type === VerificationType.MasterPassword
|
||||||
: request.otp;
|
? request.masterPasswordHash
|
||||||
return this.apiCall(request);
|
: request.otp;
|
||||||
});
|
return this.apiCall(request);
|
||||||
|
});
|
||||||
|
|
||||||
const response = await this.formPromise;
|
const response = await this.formPromise;
|
||||||
this.dialogRef.close({
|
this.dialogRef.close({
|
||||||
response: response,
|
response: response,
|
||||||
secret: hashedSecret,
|
secret: hashedSecret,
|
||||||
verificationType: this.formGroup.value.secret.type,
|
verificationType: this.formGroup.value.secret.type,
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ErrorResponse && e.statusCode === 400) {
|
||||||
|
this.invalidSecret = true;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
get dialogTitle(): string {
|
get dialogTitle(): string {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@bitwarden/importer/core": ["../../libs/importer/src"],
|
"@bitwarden/importer/core": ["../../libs/importer/src"],
|
||||||
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
|
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
|
||||||
"@bitwarden/platform": ["../../libs/platform/src"],
|
"@bitwarden/platform": ["../../libs/platform/src"],
|
||||||
|
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
|
||||||
"@bitwarden/vault": ["../../libs/vault/src"],
|
"@bitwarden/vault": ["../../libs/vault/src"],
|
||||||
"@bitwarden/web-vault/*": ["src/*"]
|
"@bitwarden/web-vault/*": ["src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"../../libs/tools/export/vault-export/vault-export-core/src"
|
"../../libs/tools/export/vault-export/vault-export-core/src"
|
||||||
],
|
],
|
||||||
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"],
|
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"],
|
||||||
|
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
|
||||||
"@bitwarden/platform": ["../../libs/platform/src"],
|
"@bitwarden/platform": ["../../libs/platform/src"],
|
||||||
"@bitwarden/vault": ["../../libs/vault/src"],
|
"@bitwarden/vault": ["../../libs/vault/src"],
|
||||||
"@bitwarden/web-vault/*": ["../../apps/web/src/*"],
|
"@bitwarden/web-vault/*": ["../../apps/web/src/*"],
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@bitwarden/importer/core": ["../../libs/importer/src"],
|
"@bitwarden/importer/core": ["../../libs/importer/src"],
|
||||||
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
|
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
|
||||||
"@bitwarden/platform": ["../../libs/platform/src"],
|
"@bitwarden/platform": ["../../libs/platform/src"],
|
||||||
|
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
|
||||||
"@bitwarden/vault": ["../../libs/vault/src"],
|
"@bitwarden/vault": ["../../libs/vault/src"],
|
||||||
"@bitwarden/web-vault/*": ["../../apps/web/src/*"],
|
"@bitwarden/web-vault/*": ["../../apps/web/src/*"],
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken<Observable<ThemeTy
|
|||||||
"SYSTEM_THEME_OBSERVABLE",
|
"SYSTEM_THEME_OBSERVABLE",
|
||||||
);
|
);
|
||||||
export const DEFAULT_VAULT_TIMEOUT = new SafeInjectionToken<VaultTimeout>("DEFAULT_VAULT_TIMEOUT");
|
export const DEFAULT_VAULT_TIMEOUT = new SafeInjectionToken<VaultTimeout>("DEFAULT_VAULT_TIMEOUT");
|
||||||
export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken<Subject<Message<object>>>(
|
export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken<
|
||||||
"INTRAPROCESS_MESSAGING_SUBJECT",
|
Subject<Message<Record<string, unknown>>>
|
||||||
);
|
>("INTRAPROCESS_MESSAGING_SUBJECT");
|
||||||
export const CLIENT_TYPE = new SafeInjectionToken<ClientType>("CLIENT_TYPE");
|
export const CLIENT_TYPE = new SafeInjectionToken<ClientType>("CLIENT_TYPE");
|
||||||
|
|||||||
@@ -649,7 +649,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
safeProvider({
|
safeProvider({
|
||||||
provide: BroadcasterService,
|
provide: BroadcasterService,
|
||||||
useClass: DefaultBroadcasterService,
|
useClass: DefaultBroadcasterService,
|
||||||
deps: [MessageSender, MessageListener],
|
deps: [MessageListener],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: VaultTimeoutSettingsServiceAbstraction,
|
provide: VaultTimeoutSettingsServiceAbstraction,
|
||||||
@@ -1165,17 +1165,19 @@ const safeProviders: SafeProvider[] = [
|
|||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: INTRAPROCESS_MESSAGING_SUBJECT,
|
provide: INTRAPROCESS_MESSAGING_SUBJECT,
|
||||||
useFactory: () => new Subject<Message<object>>(),
|
useFactory: () => new Subject<Message<Record<string, unknown>>>(),
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: MessageListener,
|
provide: MessageListener,
|
||||||
useFactory: (subject: Subject<Message<object>>) => new MessageListener(subject.asObservable()),
|
useFactory: (subject: Subject<Message<Record<string, unknown>>>) =>
|
||||||
|
new MessageListener(subject.asObservable()),
|
||||||
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
|
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: MessageSender,
|
provide: MessageSender,
|
||||||
useFactory: (subject: Subject<Message<object>>) => new SubjectMessageSender(subject),
|
useFactory: (subject: Subject<Message<Record<string, unknown>>>) =>
|
||||||
|
new SubjectMessageSender(subject),
|
||||||
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
|
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ export interface MessageBase {
|
|||||||
* @deprecated Use the observable from the appropriate service instead.
|
* @deprecated Use the observable from the appropriate service instead.
|
||||||
*/
|
*/
|
||||||
export abstract class BroadcasterService {
|
export abstract class BroadcasterService {
|
||||||
/**
|
|
||||||
* @deprecated Use the observable from the appropriate service instead.
|
|
||||||
*/
|
|
||||||
abstract send(message: MessageBase, id?: string): void;
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use the observable from the appropriate service instead.
|
* @deprecated Use the observable from the appropriate service instead.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("helpers", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("can get the command from a message definition", () => {
|
it("can get the command from a message definition", () => {
|
||||||
const commandDefinition = new CommandDefinition<object>("myCommand");
|
const commandDefinition = new CommandDefinition<Record<string, unknown>>("myCommand");
|
||||||
|
|
||||||
const command = getCommand(commandDefinition);
|
const command = getCommand(commandDefinition);
|
||||||
|
|
||||||
@@ -22,9 +22,9 @@ describe("helpers", () => {
|
|||||||
|
|
||||||
describe("tag integration", () => {
|
describe("tag integration", () => {
|
||||||
it("can tag and identify as tagged", async () => {
|
it("can tag and identify as tagged", async () => {
|
||||||
const messagesSubject = new Subject<Message<object>>();
|
const messagesSubject = new Subject<Message<Record<string, unknown>>>();
|
||||||
|
|
||||||
const taggedMessages = messagesSubject.asObservable().pipe(tagAsExternal);
|
const taggedMessages = messagesSubject.asObservable().pipe(tagAsExternal());
|
||||||
|
|
||||||
const firstValuePromise = firstValueFrom(taggedMessages);
|
const firstValuePromise = firstValueFrom(taggedMessages);
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ describe("helpers", () => {
|
|||||||
describe("isExternalMessage", () => {
|
describe("isExternalMessage", () => {
|
||||||
it.each([null, { command: "myCommand", test: "object" }, undefined] as Message<
|
it.each([null, { command: "myCommand", test: "object" }, undefined] as Message<
|
||||||
Record<string, unknown>
|
Record<string, unknown>
|
||||||
>[])("returns false when value is %s", (value: Message<object>) => {
|
>[])("returns false when value is %s", (value: Message<Record<string, unknown>>) => {
|
||||||
expect(isExternalMessage(value)).toBe(false);
|
expect(isExternalMessage(value)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { MonoTypeOperatorFunction, map } from "rxjs";
|
import { map } from "rxjs";
|
||||||
|
|
||||||
import { Message, CommandDefinition } from "./types";
|
import { CommandDefinition } from "./types";
|
||||||
|
|
||||||
export const getCommand = (commandDefinition: CommandDefinition<object> | string) => {
|
export const getCommand = (
|
||||||
|
commandDefinition: CommandDefinition<Record<string, unknown>> | string,
|
||||||
|
) => {
|
||||||
if (typeof commandDefinition === "string") {
|
if (typeof commandDefinition === "string") {
|
||||||
return commandDefinition;
|
return commandDefinition;
|
||||||
} else {
|
} else {
|
||||||
@@ -16,8 +18,8 @@ export const isExternalMessage = (message: Record<PropertyKey, unknown>) => {
|
|||||||
return message?.[EXTERNAL_SOURCE_TAG] === true;
|
return message?.[EXTERNAL_SOURCE_TAG] === true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tagAsExternal: MonoTypeOperatorFunction<Message<object>> = map(
|
export const tagAsExternal = <T extends Record<PropertyKey, unknown>>() => {
|
||||||
(message: Message<object>) => {
|
return map((message: T) => {
|
||||||
return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true });
|
return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true });
|
||||||
},
|
});
|
||||||
);
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Message, CommandDefinition } from "./types";
|
|||||||
* or vault data changes and those observables should be preferred over messaging.
|
* or vault data changes and those observables should be preferred over messaging.
|
||||||
*/
|
*/
|
||||||
export class MessageListener {
|
export class MessageListener {
|
||||||
constructor(private readonly messageStream: Observable<Message<object>>) {}
|
constructor(private readonly messageStream: Observable<Message<Record<string, unknown>>>) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A stream of all messages sent through the application. It does not contain type information for the
|
* A stream of all messages sent through the application. It does not contain type information for the
|
||||||
@@ -28,7 +28,9 @@ export class MessageListener {
|
|||||||
*
|
*
|
||||||
* @param commandDefinition The CommandDefinition containing the information about the message type you care about.
|
* @param commandDefinition The CommandDefinition containing the information about the message type you care about.
|
||||||
*/
|
*/
|
||||||
messages$<T extends object>(commandDefinition: CommandDefinition<T>): Observable<T> {
|
messages$<T extends Record<string, unknown>>(
|
||||||
|
commandDefinition: CommandDefinition<T>,
|
||||||
|
): Observable<T> {
|
||||||
return this.allMessages$.pipe(
|
return this.allMessages$.pipe(
|
||||||
filter((msg) => msg?.command === commandDefinition.command),
|
filter((msg) => msg?.command === commandDefinition.command),
|
||||||
) as Observable<T>;
|
) as Observable<T>;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { CommandDefinition } from "./types";
|
|||||||
class MultiMessageSender implements MessageSender {
|
class MultiMessageSender implements MessageSender {
|
||||||
constructor(private readonly innerMessageSenders: MessageSender[]) {}
|
constructor(private readonly innerMessageSenders: MessageSender[]) {}
|
||||||
|
|
||||||
send<T extends object>(
|
send<T extends Record<string, unknown>>(
|
||||||
commandDefinition: string | CommandDefinition<T>,
|
commandDefinition: string | CommandDefinition<T>,
|
||||||
payload: object | T = {},
|
payload: Record<string, unknown> | T = {},
|
||||||
): void {
|
): void {
|
||||||
for (const messageSender of this.innerMessageSenders) {
|
for (const messageSender of this.innerMessageSenders) {
|
||||||
messageSender.send(commandDefinition, payload);
|
messageSender.send(commandDefinition, payload);
|
||||||
@@ -26,7 +26,10 @@ export abstract class MessageSender {
|
|||||||
* @param commandDefinition
|
* @param commandDefinition
|
||||||
* @param payload
|
* @param payload
|
||||||
*/
|
*/
|
||||||
abstract send<T extends object>(commandDefinition: CommandDefinition<T>, payload: T): void;
|
abstract send<T extends Record<string, unknown>>(
|
||||||
|
commandDefinition: CommandDefinition<T>,
|
||||||
|
payload: T,
|
||||||
|
): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A legacy method for sending messages in a non-type safe way.
|
* A legacy method for sending messages in a non-type safe way.
|
||||||
@@ -38,12 +41,12 @@ export abstract class MessageSender {
|
|||||||
* @param payload Extra contextual information regarding the message. Be aware that this payload may
|
* @param payload Extra contextual information regarding the message. Be aware that this payload may
|
||||||
* be serialized and lose all prototype information.
|
* be serialized and lose all prototype information.
|
||||||
*/
|
*/
|
||||||
abstract send(command: string, payload?: object): void;
|
abstract send(command: string, payload?: Record<string, unknown>): void;
|
||||||
|
|
||||||
/** Implementation of the other two overloads, read their docs instead. */
|
/** Implementation of the other two overloads, read their docs instead. */
|
||||||
abstract send<T extends object>(
|
abstract send<T extends Record<string, unknown>>(
|
||||||
commandDefinition: CommandDefinition<T> | string,
|
commandDefinition: CommandDefinition<T> | string,
|
||||||
payload: T | object,
|
payload: T | Record<string, unknown>,
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { MessageSender } from "./message.sender";
|
|||||||
import { Message, CommandDefinition } from "./types";
|
import { Message, CommandDefinition } from "./types";
|
||||||
|
|
||||||
export class SubjectMessageSender implements MessageSender {
|
export class SubjectMessageSender implements MessageSender {
|
||||||
constructor(private readonly messagesSubject: Subject<Message<object>>) {}
|
constructor(private readonly messagesSubject: Subject<Message<Record<string, unknown>>>) {}
|
||||||
|
|
||||||
send<T extends object>(
|
send<T extends Record<string, unknown>>(
|
||||||
commandDefinition: string | CommandDefinition<T>,
|
commandDefinition: string | CommandDefinition<T>,
|
||||||
payload: object | T = {},
|
payload: Record<string, unknown> | T = {},
|
||||||
): void {
|
): void {
|
||||||
const command = getCommand(commandDefinition);
|
const command = getCommand(commandDefinition);
|
||||||
this.messagesSubject.next(Object.assign(payload ?? {}, { command: command }));
|
this.messagesSubject.next(Object.assign(payload ?? {}, { command: command }));
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ declare const tag: unique symbol;
|
|||||||
* alonside `MessageSender` and `MessageListener` for providing a type
|
* alonside `MessageSender` and `MessageListener` for providing a type
|
||||||
* safe(-ish) way of sending and receiving messages.
|
* safe(-ish) way of sending and receiving messages.
|
||||||
*/
|
*/
|
||||||
export class CommandDefinition<T extends object> {
|
export class CommandDefinition<T extends Record<string, unknown>> {
|
||||||
[tag]: T;
|
[tag]: T;
|
||||||
constructor(readonly command: string) {}
|
constructor(readonly command: string) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Message<T extends object> = { command: string } & T;
|
export type Message<T extends Record<string, unknown>> = { command: string } & T;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Subscription } from "rxjs";
|
import { Subscription } from "rxjs";
|
||||||
|
|
||||||
import { BroadcasterService, MessageBase } from "../abstractions/broadcaster.service";
|
import { BroadcasterService, MessageBase } from "../abstractions/broadcaster.service";
|
||||||
import { MessageListener, MessageSender } from "../messaging";
|
import { MessageListener } from "../messaging";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary implementation that just delegates to the message sender and message listener
|
* Temporary implementation that just delegates to the message sender and message listener
|
||||||
@@ -10,14 +10,7 @@ import { MessageListener, MessageSender } from "../messaging";
|
|||||||
export class DefaultBroadcasterService implements BroadcasterService {
|
export class DefaultBroadcasterService implements BroadcasterService {
|
||||||
subscriptions = new Map<string, Subscription>();
|
subscriptions = new Map<string, Subscription>();
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly messageListener: MessageListener) {}
|
||||||
private readonly messageSender: MessageSender,
|
|
||||||
private readonly messageListener: MessageListener,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
send(message: MessageBase, id?: string) {
|
|
||||||
this.messageSender.send(message?.command, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(id: string, messageCallback: (message: MessageBase) => void) {
|
subscribe(id: string, messageCallback: (message: MessageBase) => void) {
|
||||||
this.subscriptions.set(
|
this.subscriptions.set(
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export class Parser {
|
|||||||
options: ParserOptions,
|
options: ParserOptions,
|
||||||
): Promise<Account> {
|
): Promise<Account> {
|
||||||
let id: string;
|
let id: string;
|
||||||
|
let step = 0;
|
||||||
try {
|
try {
|
||||||
const placeholder = "decryption failed";
|
const placeholder = "decryption failed";
|
||||||
const reader = new BinaryReader(chunk.payload);
|
const reader = new BinaryReader(chunk.payload);
|
||||||
@@ -42,6 +43,7 @@ export class Parser {
|
|||||||
id = Utils.fromBufferToUtf8(this.readItem(reader));
|
id = Utils.fromBufferToUtf8(this.readItem(reader));
|
||||||
|
|
||||||
// 1: name
|
// 1: name
|
||||||
|
step = 1;
|
||||||
const name = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
const name = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
||||||
this.readItem(reader),
|
this.readItem(reader),
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
@@ -49,6 +51,7 @@ export class Parser {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 2: group
|
// 2: group
|
||||||
|
step = 2;
|
||||||
const group = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
const group = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
||||||
this.readItem(reader),
|
this.readItem(reader),
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
@@ -56,6 +59,7 @@ export class Parser {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 3: url
|
// 3: url
|
||||||
|
step = 3;
|
||||||
let url = Utils.fromBufferToUtf8(
|
let url = Utils.fromBufferToUtf8(
|
||||||
this.decodeHexLoose(Utils.fromBufferToUtf8(this.readItem(reader))),
|
this.decodeHexLoose(Utils.fromBufferToUtf8(this.readItem(reader))),
|
||||||
);
|
);
|
||||||
@@ -66,6 +70,7 @@ export class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4: extra (notes)
|
// 4: extra (notes)
|
||||||
|
step = 4;
|
||||||
const notes = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
const notes = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
||||||
this.readItem(reader),
|
this.readItem(reader),
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
@@ -73,12 +78,14 @@ export class Parser {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 5: fav (is favorite)
|
// 5: fav (is favorite)
|
||||||
|
step = 5;
|
||||||
const isFavorite = Utils.fromBufferToUtf8(this.readItem(reader)) === "1";
|
const isFavorite = Utils.fromBufferToUtf8(this.readItem(reader)) === "1";
|
||||||
|
|
||||||
// 6: sharedfromaid (?)
|
// 6: sharedfromaid (?)
|
||||||
this.skipItem(reader);
|
this.skipItem(reader);
|
||||||
|
|
||||||
// 7: username
|
// 7: username
|
||||||
|
step = 7;
|
||||||
let username = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
let username = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
||||||
this.readItem(reader),
|
this.readItem(reader),
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
@@ -86,6 +93,7 @@ export class Parser {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 8: password
|
// 8: password
|
||||||
|
step = 8;
|
||||||
let password = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
let password = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
||||||
this.readItem(reader),
|
this.readItem(reader),
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
@@ -99,6 +107,7 @@ export class Parser {
|
|||||||
this.skipItem(reader);
|
this.skipItem(reader);
|
||||||
|
|
||||||
// 11: sn (is secure note)
|
// 11: sn (is secure note)
|
||||||
|
step = 11;
|
||||||
const isSecureNote = Utils.fromBufferToUtf8(this.readItem(reader)) === "1";
|
const isSecureNote = Utils.fromBufferToUtf8(this.readItem(reader)) === "1";
|
||||||
|
|
||||||
// Parse secure note
|
// Parse secure note
|
||||||
@@ -214,6 +223,7 @@ export class Parser {
|
|||||||
this.skipItem(reader);
|
this.skipItem(reader);
|
||||||
|
|
||||||
// 39: totp (?)
|
// 39: totp (?)
|
||||||
|
step = 39;
|
||||||
const totp = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
const totp = await this.cryptoUtils.decryptAes256PlainWithDefault(
|
||||||
this.readItem(reader),
|
this.readItem(reader),
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
@@ -227,6 +237,7 @@ export class Parser {
|
|||||||
// 42: last_credential_monitoring_stat (?)
|
// 42: last_credential_monitoring_stat (?)
|
||||||
|
|
||||||
// Adjust the path to include the group and the shared folder, if any.
|
// Adjust the path to include the group and the shared folder, if any.
|
||||||
|
step = 42;
|
||||||
const path = this.makeAccountPath(group, folder);
|
const path = this.makeAccountPath(group, folder);
|
||||||
|
|
||||||
const account = new Account();
|
const account = new Account();
|
||||||
@@ -243,7 +254,12 @@ export class Parser {
|
|||||||
return account;
|
return account;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Error parsing accounts on item with ID:" + id + " errorMessage: " + err.message,
|
"Error parsing accounts on item with ID:" +
|
||||||
|
id +
|
||||||
|
" step #" +
|
||||||
|
step +
|
||||||
|
" errorMessage: " +
|
||||||
|
err.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@bitwarden/importer/core": ["../importer/src"],
|
"@bitwarden/importer/core": ["../importer/src"],
|
||||||
"@bitwarden/importer/ui": ["../importer/src/components"],
|
"@bitwarden/importer/ui": ["../importer/src/components"],
|
||||||
"@bitwarden/platform": ["../platform/src"],
|
"@bitwarden/platform": ["../platform/src"],
|
||||||
|
"@bitwarden/send-ui": ["../tools/send/send-ui/src"],
|
||||||
"@bitwarden/node/*": ["../node/src/*"],
|
"@bitwarden/node/*": ["../node/src/*"],
|
||||||
"@bitwarden/vault": ["../vault/src"]
|
"@bitwarden/vault": ["../vault/src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<bit-callout type="danger" title="{{ 'vaultExportDisabled' | i18n }}" *ngIf="disabledByPolicy">
|
||||||
|
{{ "personalVaultExportPolicyInEffect" | i18n }}
|
||||||
|
</bit-callout>
|
||||||
|
<tools-export-scope-callout
|
||||||
|
[organizationId]="organizationId"
|
||||||
|
*ngIf="!disabledByPolicy"
|
||||||
|
></tools-export-scope-callout>
|
||||||
|
|
||||||
|
<form [formGroup]="exportForm" [bitSubmit]="submit" id="export_form_exportForm">
|
||||||
|
<ng-container *ngIf="organizations$ | async as organizations">
|
||||||
|
<bit-form-field *ngIf="organizations.length > 0">
|
||||||
|
<bit-label>{{ "exportFrom" | i18n }}</bit-label>
|
||||||
|
<bit-select formControlName="vaultSelector">
|
||||||
|
<bit-option [label]="'myVault' | i18n" value="myVault" icon="bwi-user" />
|
||||||
|
<bit-option
|
||||||
|
*ngFor="let o of organizations$ | async"
|
||||||
|
[value]="o.id"
|
||||||
|
[label]="o.name"
|
||||||
|
icon="bwi-business"
|
||||||
|
/>
|
||||||
|
</bit-select>
|
||||||
|
</bit-form-field>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "fileFormat" | i18n }}</bit-label>
|
||||||
|
<bit-select formControlName="format">
|
||||||
|
<bit-option *ngFor="let f of formatOptions" [value]="f.value" [label]="f.name" />
|
||||||
|
</bit-select>
|
||||||
|
</bit-form-field>
|
||||||
|
|
||||||
|
<ng-container *ngIf="format === 'encrypted_json'">
|
||||||
|
<bit-radio-group formControlName="fileEncryptionType" aria-label="exportTypeHeading">
|
||||||
|
<bit-label>{{ "exportTypeHeading" | i18n }}</bit-label>
|
||||||
|
|
||||||
|
<bit-radio-button
|
||||||
|
id="AccountEncrypted"
|
||||||
|
name="fileEncryptionType"
|
||||||
|
class="tw-block"
|
||||||
|
[value]="encryptedExportType.AccountEncrypted"
|
||||||
|
checked="fileEncryptionType === encryptedExportType.AccountEncrypted"
|
||||||
|
>
|
||||||
|
<bit-label>{{ "accountRestricted" | i18n }}</bit-label>
|
||||||
|
<bit-hint>{{ "accountRestrictedOptionDescription" | i18n }}</bit-hint>
|
||||||
|
</bit-radio-button>
|
||||||
|
|
||||||
|
<bit-radio-button
|
||||||
|
id="FileEncrypted"
|
||||||
|
name="fileEncryptionType"
|
||||||
|
class="tw-block"
|
||||||
|
[value]="encryptedExportType.FileEncrypted"
|
||||||
|
checked="fileEncryptionType === encryptedExportType.FileEncrypted"
|
||||||
|
>
|
||||||
|
<bit-label>{{ "passwordProtected" | i18n }}</bit-label>
|
||||||
|
<bit-hint>{{ "passwordProtectedOptionDescription" | i18n }}</bit-hint>
|
||||||
|
</bit-radio-button>
|
||||||
|
</bit-radio-group>
|
||||||
|
|
||||||
|
<ng-container *ngIf="fileEncryptionType == encryptedExportType.FileEncrypted">
|
||||||
|
<div class="tw-mb-3">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "filePassword" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
type="password"
|
||||||
|
id="filePassword"
|
||||||
|
formControlName="filePassword"
|
||||||
|
name="password"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitSuffix
|
||||||
|
bitIconButton
|
||||||
|
bitPasswordInputToggle
|
||||||
|
[(toggled)]="showFilePassword"
|
||||||
|
></button>
|
||||||
|
<bit-hint>{{ "exportPasswordDescription" | i18n }}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<app-password-strength [password]="filePassword" [showText]="true"> </app-password-strength>
|
||||||
|
</div>
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
type="password"
|
||||||
|
id="confirmFilePassword"
|
||||||
|
formControlName="confirmFilePassword"
|
||||||
|
name="confirmFilePassword"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitSuffix
|
||||||
|
bitIconButton
|
||||||
|
bitPasswordInputToggle
|
||||||
|
[(toggled)]="showFilePassword"
|
||||||
|
></button>
|
||||||
|
</bit-form-field>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</form>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Directive, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core";
|
import { CommonModule } from "@angular/common";
|
||||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core";
|
||||||
|
import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms";
|
||||||
import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs";
|
import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { PasswordStrengthComponent } from "@bitwarden/angular/tools/password-strength/password-strength.component";
|
import { PasswordStrengthComponent } from "@bitwarden/angular/tools/password-strength/password-strength.component";
|
||||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
@@ -16,11 +18,70 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { EncryptedExportType } from "@bitwarden/common/tools/enums/encrypted-export-type.enum";
|
import { EncryptedExportType } from "@bitwarden/common/tools/enums/encrypted-export-type.enum";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import {
|
||||||
|
AsyncActionsModule,
|
||||||
|
BitSubmitDirective,
|
||||||
|
ButtonModule,
|
||||||
|
CalloutModule,
|
||||||
|
DialogService,
|
||||||
|
FormFieldModule,
|
||||||
|
IconButtonModule,
|
||||||
|
RadioButtonModule,
|
||||||
|
SelectModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
|
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
|
||||||
|
|
||||||
@Directive()
|
import { ExportScopeCalloutComponent } from "./export-scope-callout.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "tools-export",
|
||||||
|
templateUrl: "export.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
JslibModule,
|
||||||
|
FormFieldModule,
|
||||||
|
AsyncActionsModule,
|
||||||
|
ButtonModule,
|
||||||
|
IconButtonModule,
|
||||||
|
SelectModule,
|
||||||
|
CalloutModule,
|
||||||
|
RadioButtonModule,
|
||||||
|
ExportScopeCalloutComponent,
|
||||||
|
UserVerificationDialogComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
export class ExportComponent implements OnInit, OnDestroy {
|
export class ExportComponent implements OnInit, OnDestroy {
|
||||||
|
/**
|
||||||
|
* The hosting control also needs a bitSubmitDirective (on the Submit button) which calls this components {@link submit}-method.
|
||||||
|
* This components formState (loading/disabled) is emitted back up to the hosting component so for example the Submit button can be enabled/disabled and show loading state.
|
||||||
|
*/
|
||||||
|
@ViewChild(BitSubmitDirective)
|
||||||
|
private bitSubmit: BitSubmitDirective;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits true when the BitSubmitDirective({@link bitSubmit} is executing {@link submit} and false when execution has completed.
|
||||||
|
* Example: Used to show the loading state of the submit button present on the hosting component
|
||||||
|
* */
|
||||||
|
@Output()
|
||||||
|
formLoading = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits true when this form gets disabled and false when enabled.
|
||||||
|
* Example: Used to disable the submit button, which is present on the hosting component
|
||||||
|
* */
|
||||||
|
@Output()
|
||||||
|
formDisabled = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the creation and download of the export-file have succeeded
|
||||||
|
* - Emits an null/empty string when exporting from an individual vault
|
||||||
|
* - Emits the organizationId when exporting from an organizationl vault
|
||||||
|
* */
|
||||||
|
@Output()
|
||||||
|
onSuccessfulExport = new EventEmitter<string>();
|
||||||
|
|
||||||
@Output() onSaved = new EventEmitter();
|
@Output() onSaved = new EventEmitter();
|
||||||
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
|
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
|
||||||
|
|
||||||
@@ -74,6 +135,11 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
// Setup subscription to emit when this form is enabled/disabled
|
||||||
|
this.exportForm.statusChanges.pipe(takeUntil(this.destroy$)).subscribe((c) => {
|
||||||
|
this.formDisabled.emit(c === "DISABLED");
|
||||||
|
});
|
||||||
|
|
||||||
this.policyService
|
this.policyService
|
||||||
.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport)
|
.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport)
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
@@ -88,8 +154,7 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
this.exportForm.get("format").valueChanges,
|
this.exportForm.get("format").valueChanges,
|
||||||
this.exportForm.get("fileEncryptionType").valueChanges,
|
this.exportForm.get("fileEncryptionType").valueChanges,
|
||||||
)
|
)
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(startWith(0), takeUntil(this.destroy$))
|
||||||
.pipe(startWith(0))
|
|
||||||
.subscribe(() => this.adjustValidators());
|
.subscribe(() => this.adjustValidators());
|
||||||
|
|
||||||
if (this.organizationId) {
|
if (this.organizationId) {
|
||||||
@@ -118,6 +183,12 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
this.exportForm.controls.vaultSelector.setValue("myVault");
|
this.exportForm.controls.vaultSelector.setValue("myVault");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.bitSubmit.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => {
|
||||||
|
this.formLoading.emit(loading);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
}
|
}
|
||||||
@@ -187,6 +258,7 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
protected saved() {
|
protected saved() {
|
||||||
this.onSaved.emit();
|
this.onSaved.emit();
|
||||||
|
this.onSuccessfulExport.emit(this.organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyUser(): Promise<boolean> {
|
private async verifyUser(): Promise<boolean> {
|
||||||
@@ -235,6 +307,10 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getFileName(prefix?: string) {
|
protected getFileName(prefix?: string) {
|
||||||
|
if (this.organizationId) {
|
||||||
|
prefix = "org";
|
||||||
|
}
|
||||||
|
|
||||||
let extension = this.format;
|
let extension = this.format;
|
||||||
if (this.format === "encrypted_json") {
|
if (this.format === "encrypted_json") {
|
||||||
if (prefix == null) {
|
if (prefix == null) {
|
||||||
@@ -248,7 +324,15 @@ export class ExportComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async collectEvent(): Promise<void> {
|
protected async collectEvent(): Promise<void> {
|
||||||
await this.eventCollectionService.collect(EventType.User_ClientExportedVault);
|
if (this.organizationId) {
|
||||||
|
return await this.eventCollectionService.collect(
|
||||||
|
EventType.Organization_ClientExportedVault,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
this.organizationId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await this.eventCollectionService.collect(EventType.User_ClientExportedVault);
|
||||||
}
|
}
|
||||||
|
|
||||||
get format() {
|
get format() {
|
||||||
|
|||||||
17
libs/tools/send/README.md
Normal file
17
libs/tools/send/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Bitwarden Send
|
||||||
|
|
||||||
|
This folder contains 2 packages that can be used to create and modify Sends.
|
||||||
|
|
||||||
|
## semd-core
|
||||||
|
|
||||||
|
Package name: `@bitwarden/send-core`
|
||||||
|
|
||||||
|
Contains all types, models, and services for Bitwarden Send
|
||||||
|
|
||||||
|
Currently in use by the Bitwarden Web Vault, CLI, desktop app and browser extension
|
||||||
|
|
||||||
|
## send-ui
|
||||||
|
|
||||||
|
Package name: `@bitwarden/send-ui`
|
||||||
|
|
||||||
|
Contains all UI components used for Bitwarden Send
|
||||||
13
libs/tools/send/send-ui/jest.config.js
Normal file
13
libs/tools/send/send-ui/jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||||
|
|
||||||
|
const { compilerOptions } = require("../../../shared/tsconfig.libs");
|
||||||
|
|
||||||
|
/** @type {import('jest').Config} */
|
||||||
|
module.exports = {
|
||||||
|
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||||
|
preset: "ts-jest",
|
||||||
|
testEnvironment: "jsdom",
|
||||||
|
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||||
|
prefix: "<rootDir>/../../../",
|
||||||
|
}),
|
||||||
|
};
|
||||||
23
libs/tools/send/send-ui/package.json
Normal file
23
libs/tools/send/send-ui/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "@bitwarden/send-ui",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Angular components for Bitwarden Send",
|
||||||
|
"keywords": [
|
||||||
|
"bitwarden"
|
||||||
|
],
|
||||||
|
"author": "Bitwarden Inc.",
|
||||||
|
"homepage": "https://bitwarden.com",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/bitwarden/clients"
|
||||||
|
},
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"build": "npm run clean && tsc",
|
||||||
|
"build:watch": "npm run clean && tsc -watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@bitwarden/common": "file:../../../common"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
libs/tools/send/send-ui/src/index.ts
Normal file
0
libs/tools/send/send-ui/src/index.ts
Normal file
5
libs/tools/send/send-ui/tsconfig.json
Normal file
5
libs/tools/send/send-ui/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../shared/tsconfig.libs",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
3
libs/tools/send/send-ui/tsconfig.spec.json
Normal file
3
libs/tools/send/send-ui/tsconfig.spec.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json"
|
||||||
|
}
|
||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -193,7 +193,7 @@
|
|||||||
},
|
},
|
||||||
"apps/browser": {
|
"apps/browser": {
|
||||||
"name": "@bitwarden/browser",
|
"name": "@bitwarden/browser",
|
||||||
"version": "2024.5.0"
|
"version": "2024.5.1"
|
||||||
},
|
},
|
||||||
"apps/cli": {
|
"apps/cli": {
|
||||||
"name": "@bitwarden/cli",
|
"name": "@bitwarden/cli",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"],
|
"@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"],
|
||||||
"@bitwarden/importer/core": ["./libs/importer/src"],
|
"@bitwarden/importer/core": ["./libs/importer/src"],
|
||||||
"@bitwarden/importer/ui": ["./libs/importer/src/components"],
|
"@bitwarden/importer/ui": ["./libs/importer/src/components"],
|
||||||
|
"@bitwarden/send-ui": [".libs/tools/send/send-ui/src"],
|
||||||
"@bitwarden/platform": ["./libs/platform/src"],
|
"@bitwarden/platform": ["./libs/platform/src"],
|
||||||
"@bitwarden/node/*": ["./libs/node/src/*"],
|
"@bitwarden/node/*": ["./libs/node/src/*"],
|
||||||
"@bitwarden/vault": ["./libs/vault/src"],
|
"@bitwarden/vault": ["./libs/vault/src"],
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"@bitwarden/importer/core": ["./libs/importer/src"],
|
"@bitwarden/importer/core": ["./libs/importer/src"],
|
||||||
"@bitwarden/importer/ui": ["./libs/importer/src/components"],
|
"@bitwarden/importer/ui": ["./libs/importer/src/components"],
|
||||||
"@bitwarden/platform": ["./libs/platform/src"],
|
"@bitwarden/platform": ["./libs/platform/src"],
|
||||||
|
"@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"],
|
||||||
"@bitwarden/node/*": ["./libs/node/src/*"],
|
"@bitwarden/node/*": ["./libs/node/src/*"],
|
||||||
"@bitwarden/web-vault/*": ["./apps/web/src/*"],
|
"@bitwarden/web-vault/*": ["./apps/web/src/*"],
|
||||||
"@bitwarden/vault": ["./libs/vault/src"],
|
"@bitwarden/vault": ["./libs/vault/src"],
|
||||||
|
|||||||
Reference in New Issue
Block a user