mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 13:40:06 +00:00
Merge branch 'main' into innovation/archive/clients
This commit is contained in:
4
.github/workflows/build-web.yml
vendored
4
.github/workflows/build-web.yml
vendored
@@ -312,6 +312,7 @@ jobs:
|
||||
cosign sign --yes ${images}
|
||||
|
||||
- name: Scan Docker image
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
id: container-scan
|
||||
uses: anchore/scan-action@869c549e657a088dc0441b08ce4fc0ecdac2bb65 # v5.3.0
|
||||
with:
|
||||
@@ -320,9 +321,12 @@ jobs:
|
||||
output-format: sarif
|
||||
|
||||
- name: Upload Grype results to GitHub
|
||||
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2
|
||||
with:
|
||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||
|
||||
- name: Log out of Docker
|
||||
run: docker logout
|
||||
|
||||
2
.github/workflows/scan.yml
vendored
2
.github/workflows/scan.yml
vendored
@@ -49,6 +49,8 @@ jobs:
|
||||
uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2025.3.0",
|
||||
"version": "2025.3.1",
|
||||
"scripts": {
|
||||
"build": "npm run build:chrome",
|
||||
"build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
@@ -59,6 +60,7 @@ describe("NotificationBackground", () => {
|
||||
const themeStateService = mock<ThemeStateService>();
|
||||
const configService = mock<ConfigService>();
|
||||
const accountService = mock<AccountService>();
|
||||
const organizationService = mock<OrganizationService>();
|
||||
|
||||
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
|
||||
id: "testId" as UserId,
|
||||
@@ -73,18 +75,19 @@ describe("NotificationBackground", () => {
|
||||
authService.activeAccountStatus$ = activeAccountStatusMock$;
|
||||
accountService.activeAccount$ = activeAccountSubject;
|
||||
notificationBackground = new NotificationBackground(
|
||||
accountService,
|
||||
authService,
|
||||
autofillService,
|
||||
cipherService,
|
||||
authService,
|
||||
policyService,
|
||||
folderService,
|
||||
userNotificationSettingsService,
|
||||
configService,
|
||||
domainSettingsService,
|
||||
environmentService,
|
||||
folderService,
|
||||
logService,
|
||||
organizationService,
|
||||
policyService,
|
||||
themeStateService,
|
||||
configService,
|
||||
accountService,
|
||||
userNotificationSettingsService,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -63,46 +64,48 @@ export default class NotificationBackground {
|
||||
ExtensionCommand.AutofillIdentity,
|
||||
]);
|
||||
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
|
||||
unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender),
|
||||
bgGetFolderData: () => this.getFolderData(),
|
||||
bgCloseNotificationBar: ({ message, sender }) =>
|
||||
this.handleCloseNotificationBarMessage(message, sender),
|
||||
bgAddLogin: ({ message, sender }) => this.addLogin(message, sender),
|
||||
bgAdjustNotificationBar: ({ message, sender }) =>
|
||||
this.handleAdjustNotificationBarMessage(message, sender),
|
||||
bgAddLogin: ({ message, sender }) => this.addLogin(message, sender),
|
||||
bgChangedPassword: ({ message, sender }) => this.changedPassword(message, sender),
|
||||
bgRemoveTabFromNotificationQueue: ({ sender }) =>
|
||||
this.removeTabFromNotificationQueue(sender.tab),
|
||||
bgSaveCipher: ({ message, sender }) => this.handleSaveCipherMessage(message, sender),
|
||||
bgNeverSave: ({ sender }) => this.saveNever(sender.tab),
|
||||
collectPageDetailsResponse: ({ message }) =>
|
||||
this.handleCollectPageDetailsResponseMessage(message),
|
||||
bgUnlockPopoutOpened: ({ message, sender }) => this.unlockVault(message, sender.tab),
|
||||
checkNotificationQueue: ({ sender }) => this.checkNotificationQueue(sender.tab),
|
||||
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
|
||||
bgCloseNotificationBar: ({ message, sender }) =>
|
||||
this.handleCloseNotificationBarMessage(message, sender),
|
||||
bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(),
|
||||
bgGetDecryptedCiphers: () => this.getNotificationCipherData(),
|
||||
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
|
||||
bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(),
|
||||
bgGetExcludedDomains: () => this.getExcludedDomains(),
|
||||
bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(),
|
||||
bgGetFolderData: () => this.getFolderData(),
|
||||
bgGetOrgData: () => this.getOrgData(),
|
||||
bgNeverSave: ({ sender }) => this.saveNever(sender.tab),
|
||||
bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab),
|
||||
bgRemoveTabFromNotificationQueue: ({ sender }) =>
|
||||
this.removeTabFromNotificationQueue(sender.tab),
|
||||
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
|
||||
bgSaveCipher: ({ message, sender }) => this.handleSaveCipherMessage(message, sender),
|
||||
bgUnlockPopoutOpened: ({ message, sender }) => this.unlockVault(message, sender.tab),
|
||||
checkNotificationQueue: ({ sender }) => this.checkNotificationQueue(sender.tab),
|
||||
collectPageDetailsResponse: ({ message }) =>
|
||||
this.handleCollectPageDetailsResponseMessage(message),
|
||||
getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
|
||||
notificationRefreshFlagValue: () => this.getNotificationFlag(),
|
||||
bgGetDecryptedCiphers: () => this.getNotificationCipherData(),
|
||||
bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab),
|
||||
unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender),
|
||||
};
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
private autofillService: AutofillService,
|
||||
private cipherService: CipherService,
|
||||
private authService: AuthService,
|
||||
private policyService: PolicyService,
|
||||
private folderService: FolderService,
|
||||
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
private environmentService: EnvironmentService,
|
||||
private folderService: FolderService,
|
||||
private logService: LogService,
|
||||
private organizationService: OrganizationService,
|
||||
private policyService: PolicyService,
|
||||
private themeStateService: ThemeStateService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
@@ -744,6 +747,26 @@ export default class NotificationBackground {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first value found from the organization service organizations$ observable.
|
||||
*/
|
||||
private async getOrgData() {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
const organizations = await firstValueFrom(
|
||||
this.organizationService.organizations$(activeUserId),
|
||||
);
|
||||
return organizations.map((org) => {
|
||||
const { id, name, productTierType } = org;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
productTierType,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the unlockCompleted extension message. Will close the notification bar
|
||||
* after an attempted autofill action, and retry the autofill action if the message
|
||||
|
||||
@@ -16,12 +16,12 @@ const { css } = createEmotion({
|
||||
});
|
||||
|
||||
export function NotificationBody({
|
||||
ciphers,
|
||||
ciphers = [],
|
||||
notificationType,
|
||||
theme = ThemeTypes.Light,
|
||||
handleEditOrUpdateAction,
|
||||
}: {
|
||||
ciphers: NotificationCipherData[];
|
||||
ciphers?: NotificationCipherData[];
|
||||
customClasses?: string[];
|
||||
notificationType?: NotificationType;
|
||||
theme: Theme;
|
||||
|
||||
@@ -60,23 +60,18 @@ export function NotificationButtonRow({
|
||||
)
|
||||
: ([] as Option[]);
|
||||
|
||||
const noFolderOption: Option = {
|
||||
default: true,
|
||||
icon: Folder,
|
||||
text: "No folder", // @TODO localize
|
||||
value: "0",
|
||||
};
|
||||
const folderOptions: Option[] = folders?.length
|
||||
? folders.reduce(
|
||||
? folders.reduce<Option[]>(
|
||||
(options, { id, name }: FolderView) => [
|
||||
...options,
|
||||
{
|
||||
icon: Folder,
|
||||
text: name,
|
||||
value: id,
|
||||
value: id === null ? "0" : id,
|
||||
default: id === null,
|
||||
},
|
||||
],
|
||||
[noFolderOption],
|
||||
[],
|
||||
)
|
||||
: [];
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
NotificationType,
|
||||
} from "../../../notification/abstractions/notification-bar";
|
||||
import { NotificationCipherData } from "../cipher/types";
|
||||
import { FolderView, OrgView } from "../common-types";
|
||||
import { themes, spacing } from "../constants/styles";
|
||||
|
||||
import { NotificationBody, componentClassPrefix as notificationBodyClassPrefix } from "./body";
|
||||
@@ -20,20 +21,24 @@ import {
|
||||
|
||||
export function NotificationContainer({
|
||||
handleCloseNotification,
|
||||
handleEditOrUpdateAction,
|
||||
handleSaveAction,
|
||||
ciphers,
|
||||
folders,
|
||||
i18n,
|
||||
organizations,
|
||||
theme = ThemeTypes.Light,
|
||||
type,
|
||||
ciphers,
|
||||
handleSaveAction,
|
||||
handleEditOrUpdateAction,
|
||||
}: NotificationBarIframeInitData & {
|
||||
handleCloseNotification: (e: Event) => void;
|
||||
handleSaveAction: (e: Event) => void;
|
||||
handleEditOrUpdateAction: (e: Event) => void;
|
||||
} & {
|
||||
ciphers?: NotificationCipherData[];
|
||||
folders?: FolderView[];
|
||||
i18n: { [key: string]: string };
|
||||
organizations?: OrgView[];
|
||||
type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type`
|
||||
ciphers: NotificationCipherData[];
|
||||
}) {
|
||||
const headerMessage = getHeaderMessage(i18n, type);
|
||||
const showBody = true;
|
||||
@@ -42,8 +47,8 @@ export function NotificationContainer({
|
||||
<div class=${notificationContainerStyles(theme)}>
|
||||
${NotificationHeader({
|
||||
handleCloseNotification,
|
||||
standalone: showBody,
|
||||
message: headerMessage,
|
||||
standalone: showBody,
|
||||
theme,
|
||||
})}
|
||||
${showBody
|
||||
@@ -56,9 +61,11 @@ export function NotificationContainer({
|
||||
: null}
|
||||
${NotificationFooter({
|
||||
handleSaveAction,
|
||||
theme,
|
||||
notificationType: type,
|
||||
folders,
|
||||
i18n,
|
||||
notificationType: type,
|
||||
organizations,
|
||||
theme,
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -880,7 +880,7 @@ async function loadNotificationBar() {
|
||||
|
||||
const baseStyle = useComponentBar
|
||||
? isNotificationFresh
|
||||
? "height: calc(276px + 25px); width: 450px; right: 0; transform:translateX(100%); opacity:0;"
|
||||
? "height: calc(276px + 50px); width: 450px; right: 0; transform:translateX(100%); opacity:0;"
|
||||
: "height: calc(276px + 25px); width: 450px; right: 0; transform:translateX(0%); opacity:1;"
|
||||
: "height: 42px; width: 100%;";
|
||||
|
||||
@@ -910,7 +910,7 @@ async function loadNotificationBar() {
|
||||
function getFrameStyle(useComponentBar: boolean): string {
|
||||
return (
|
||||
(useComponentBar
|
||||
? "height: calc(276px + 25px); width: 450px; right: 0;"
|
||||
? "height: calc(276px + 50px); width: 450px; right: 0;"
|
||||
: "height: 42px; width: 100%; left: 0;") +
|
||||
" top: 0; padding: 0; position: fixed;" +
|
||||
" z-index: 2147483647; visibility: visible;"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { NotificationCipherData } from "../../../autofill/content/components/cipher/types";
|
||||
import { FolderView, OrgView } from "../../../autofill/content/components/common-types";
|
||||
|
||||
const NotificationTypes = {
|
||||
Add: "add",
|
||||
Change: "change",
|
||||
@@ -9,21 +12,24 @@ const NotificationTypes = {
|
||||
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
|
||||
|
||||
type NotificationBarIframeInitData = {
|
||||
type?: string; // @TODO use `NotificationType`
|
||||
isVaultLocked?: boolean;
|
||||
theme?: Theme;
|
||||
removeIndividualVault?: boolean;
|
||||
importType?: string;
|
||||
applyRedesign?: boolean;
|
||||
ciphers?: NotificationCipherData[];
|
||||
folders?: FolderView[];
|
||||
importType?: string;
|
||||
isVaultLocked?: boolean;
|
||||
launchTimestamp?: number;
|
||||
organizations?: OrgView[];
|
||||
removeIndividualVault?: boolean;
|
||||
theme?: Theme;
|
||||
type?: string; // @TODO use `NotificationType`
|
||||
};
|
||||
|
||||
type NotificationBarWindowMessage = {
|
||||
cipherId?: string;
|
||||
command: string;
|
||||
error?: string;
|
||||
initData?: NotificationBarIframeInitData;
|
||||
username?: string;
|
||||
cipherId?: string;
|
||||
};
|
||||
|
||||
type NotificationBarWindowMessageHandlers = {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
|
||||
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background";
|
||||
import { NotificationCipherData } from "../content/components/cipher/types";
|
||||
import { OrgView } from "../content/components/common-types";
|
||||
import { NotificationConfirmationContainer } from "../content/components/notification/confirmation-container";
|
||||
import { NotificationContainer } from "../content/components/notification/container";
|
||||
import { buildSvgDomElement } from "../utils";
|
||||
@@ -115,7 +117,7 @@ function setElementText(template: HTMLTemplateElement, elementId: string, text:
|
||||
}
|
||||
}
|
||||
|
||||
function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
const { initData } = message;
|
||||
if (!initData) {
|
||||
return;
|
||||
@@ -131,7 +133,23 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
// Current implementations utilize a require for scss files which creates the need to remove the node.
|
||||
document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove());
|
||||
|
||||
sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, (cipherData) => {
|
||||
await Promise.all([
|
||||
new Promise<OrgView[]>((resolve) =>
|
||||
sendPlatformMessage({ command: "bgGetOrgData" }, resolve),
|
||||
),
|
||||
new Promise<FolderView[]>((resolve) =>
|
||||
sendPlatformMessage({ command: "bgGetFolderData" }, resolve),
|
||||
),
|
||||
new Promise<NotificationCipherData[]>((resolve) =>
|
||||
sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, resolve),
|
||||
),
|
||||
]).then(([organizations, folders, ciphers]) => {
|
||||
notificationBarIframeInitData = {
|
||||
...notificationBarIframeInitData,
|
||||
folders,
|
||||
ciphers,
|
||||
organizations,
|
||||
};
|
||||
// @TODO use context to avoid prop drilling
|
||||
return render(
|
||||
NotificationContainer({
|
||||
@@ -142,7 +160,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
handleSaveAction,
|
||||
handleEditOrUpdateAction,
|
||||
i18n,
|
||||
ciphers: cipherData,
|
||||
}),
|
||||
document.body,
|
||||
);
|
||||
|
||||
@@ -1173,18 +1173,19 @@ export default class MainBackground {
|
||||
() => this.generatePasswordToClipboard(),
|
||||
);
|
||||
this.notificationBackground = new NotificationBackground(
|
||||
this.accountService,
|
||||
this.authService,
|
||||
this.autofillService,
|
||||
this.cipherService,
|
||||
this.authService,
|
||||
this.policyService,
|
||||
this.folderService,
|
||||
this.userNotificationSettingsService,
|
||||
this.configService,
|
||||
this.domainSettingsService,
|
||||
this.environmentService,
|
||||
this.folderService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.policyService,
|
||||
this.themeStateService,
|
||||
this.configService,
|
||||
this.accountService,
|
||||
this.userNotificationSettingsService,
|
||||
);
|
||||
|
||||
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "__MSG_appName__",
|
||||
"version": "2025.3.0",
|
||||
"version": "2025.3.1",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"minimum_chrome_version": "102.0",
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "__MSG_appName__",
|
||||
"version": "2025.3.0",
|
||||
"version": "2025.3.1",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -3,14 +3,19 @@ import { BrowserApi } from "../browser/browser-api";
|
||||
import BrowserClipboardService from "../services/browser-clipboard.service";
|
||||
|
||||
describe("OffscreenDocument", () => {
|
||||
const browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener");
|
||||
const browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy");
|
||||
const browserClipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read");
|
||||
const consoleErrorSpy = jest.spyOn(console, "error");
|
||||
let browserClipboardServiceCopySpy: jest.SpyInstance;
|
||||
let browserClipboardServiceReadSpy: jest.SpyInstance;
|
||||
let browserApiMessageListenerSpy: jest.SpyInstance;
|
||||
let consoleErrorSpy: jest.SpyInstance;
|
||||
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("../offscreen-document/offscreen-document");
|
||||
beforeEach(async () => {
|
||||
browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener");
|
||||
browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy");
|
||||
browserClipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read");
|
||||
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation();
|
||||
|
||||
await import("./offscreen-document");
|
||||
});
|
||||
|
||||
describe("init", () => {
|
||||
it("sets up a `chrome.runtime.onMessage` listener", () => {
|
||||
@@ -47,6 +52,7 @@ describe("OffscreenDocument", () => {
|
||||
it("copies the message text", async () => {
|
||||
const text = "test";
|
||||
|
||||
browserClipboardServiceCopySpy.mockResolvedValueOnce(undefined);
|
||||
sendMockExtensionMessage({ command: "offscreenCopyToClipboard", text });
|
||||
await flushPromises();
|
||||
|
||||
@@ -56,6 +62,7 @@ describe("OffscreenDocument", () => {
|
||||
|
||||
describe("handleOffscreenReadFromClipboard", () => {
|
||||
it("reads the value from the clipboard service", async () => {
|
||||
browserClipboardServiceReadSpy.mockResolvedValueOnce("");
|
||||
sendMockExtensionMessage({ command: "offscreenReadFromClipboard" });
|
||||
await flushPromises();
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import BrowserClipboardService from "./browser-clipboard.service";
|
||||
|
||||
describe("BrowserClipboardService", () => {
|
||||
let windowMock: any;
|
||||
const consoleWarnSpy = jest.spyOn(console, "warn");
|
||||
let consoleWarnSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation();
|
||||
windowMock = {
|
||||
navigator: {
|
||||
clipboard: {
|
||||
@@ -104,8 +105,6 @@ describe("BrowserClipboardService", () => {
|
||||
});
|
||||
|
||||
await BrowserClipboardService.read(windowMock as Window);
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,7 +185,9 @@ describe("Browser Utils Service", () => {
|
||||
describe("copyToClipboard", () => {
|
||||
const getManifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
||||
const sendMessageToAppSpy = jest.spyOn(SafariApp, "sendMessageToApp");
|
||||
const clipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy");
|
||||
const clipboardServiceCopySpy = jest
|
||||
.spyOn(BrowserClipboardService, "copy")
|
||||
.mockResolvedValue(undefined);
|
||||
let triggerOffscreenCopyToClipboardSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -281,7 +283,9 @@ describe("Browser Utils Service", () => {
|
||||
describe("readFromClipboard", () => {
|
||||
const getManifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get");
|
||||
const sendMessageToAppSpy = jest.spyOn(SafariApp, "sendMessageToApp");
|
||||
const clipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read");
|
||||
const clipboardServiceReadSpy = jest
|
||||
.spyOn(BrowserClipboardService, "read")
|
||||
.mockResolvedValue("");
|
||||
|
||||
beforeEach(() => {
|
||||
getManifestVersionSpy.mockReturnValue(2);
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitDialogClose buttonType="primary" type="button">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" (click)="copyVersion()">
|
||||
{{ "copy" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
|
||||
@@ -440,6 +440,32 @@ const mapAddEditCipherInfoToInitialValues = (
|
||||
initialValues.name = cipher.name;
|
||||
}
|
||||
|
||||
if (cipher.type === CipherType.Card) {
|
||||
const card = cipher.card;
|
||||
|
||||
if (card != null) {
|
||||
if (card.cardholderName != null) {
|
||||
initialValues.cardholderName = card.cardholderName;
|
||||
}
|
||||
|
||||
if (card.number != null) {
|
||||
initialValues.number = card.number;
|
||||
}
|
||||
|
||||
if (card.expMonth != null) {
|
||||
initialValues.expMonth = card.expMonth;
|
||||
}
|
||||
|
||||
if (card.expYear != null) {
|
||||
initialValues.expYear = card.expYear;
|
||||
}
|
||||
|
||||
if (card.code != null) {
|
||||
initialValues.code = card.code;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cipher.type === CipherType.Login) {
|
||||
const login = cipher.login;
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
<b>{{params.cipherName}}</b>
|
||||
{{ "sshkeyApprovalMessageSuffix" | i18n }} {{ params.action | i18n }}
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
<span>{{ "authorize" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||
{{ "deny" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
class="tw-mr-3"
|
||||
></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<div>
|
||||
<div class="tw-flex tw-flex-row tw-gap-2">
|
||||
<button type="button" bitLink>
|
||||
{{ u.name ?? u.email }}
|
||||
</button>
|
||||
@@ -196,22 +196,25 @@
|
||||
class="tw-text-xs"
|
||||
variant="secondary"
|
||||
*ngIf="u.status === userStatusType.Invited"
|
||||
>{{ "invited" | i18n }}</span
|
||||
>
|
||||
{{ "invited" | i18n }}
|
||||
</span>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="warning"
|
||||
*ngIf="u.status === userStatusType.Accepted"
|
||||
>{{ "needsConfirmation" | i18n }}</span
|
||||
>
|
||||
{{ "needsConfirmation" | i18n }}
|
||||
</span>
|
||||
<span
|
||||
bitBadge
|
||||
class="tw-text-xs"
|
||||
variant="secondary"
|
||||
*ngIf="u.status === userStatusType.Revoked"
|
||||
>{{ "revoked" | i18n }}</span
|
||||
>
|
||||
{{ "revoked" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
||||
{{ u.email }}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
[color]="c.avatarColor"
|
||||
size="small"
|
||||
></bit-avatar>
|
||||
<span>
|
||||
<span class="tw-inline-flex tw-gap-2">
|
||||
<a bitLink href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
|
||||
<span
|
||||
bitBadge
|
||||
@@ -175,7 +175,7 @@
|
||||
[color]="c.avatarColor"
|
||||
size="small"
|
||||
></bit-avatar>
|
||||
<span>
|
||||
<span class="tw-inline-flex tw-gap-2">
|
||||
<span>{{ c.email }}</span>
|
||||
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.Invited">{{
|
||||
"invited" | i18n
|
||||
|
||||
@@ -13,6 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -65,6 +66,7 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
useValue: ChangeLoginPasswordService,
|
||||
},
|
||||
{ provide: ConfigService, useValue: ConfigService },
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
],
|
||||
},
|
||||
add: {
|
||||
@@ -79,6 +81,7 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
useValue: mock<ChangeLoginPasswordService>(),
|
||||
},
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -180,7 +180,7 @@ export class DeviceManagementComponent {
|
||||
private updateDeviceTable(devices: Array<DeviceView>): void {
|
||||
this.dataSource.data = devices
|
||||
.map((device: DeviceView): DeviceTableData | null => {
|
||||
if (device.id == undefined) {
|
||||
if (!device.id) {
|
||||
this.validationService.showError(new Error(this.i18nService.t("deviceIdMissing")));
|
||||
return null;
|
||||
}
|
||||
@@ -190,7 +190,7 @@ export class DeviceManagementComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (device.creationDate == undefined) {
|
||||
if (!device.creationDate) {
|
||||
this.validationService.showError(
|
||||
new Error(this.i18nService.t("deviceCreationDateMissing")),
|
||||
);
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
<h2 bitTypography="h2">
|
||||
{{ "logInWithPasskey" | i18n }}
|
||||
<ng-container *ngIf="hasData">
|
||||
<span
|
||||
*ngIf="requireSsoPolicyEnabled"
|
||||
bitBadge
|
||||
variant="secondary"
|
||||
class="tw-max-w-full !tw-align-middle"
|
||||
>
|
||||
{{ "off" | i18n }} - {{ "ssoLoginIsRequired" | i18n }}
|
||||
</span>
|
||||
<ng-container *ngIf="!requireSsoPolicyEnabled">
|
||||
<span *ngIf="hasCredentials" bitBadge variant="success" class="!tw-align-middle">{{
|
||||
"on" | i18n
|
||||
}}</span>
|
||||
<span *ngIf="!hasCredentials" bitBadge variant="secondary" class="!tw-align-middle">{{
|
||||
"off" | i18n
|
||||
}}</span>
|
||||
<span class="tw-inline-flex tw-gap-1 tw-align-middle">
|
||||
<ng-container *ngIf="hasData">
|
||||
<span
|
||||
*ngIf="requireSsoPolicyEnabled"
|
||||
bitBadge
|
||||
variant="secondary"
|
||||
class="tw-max-w-full !tw-align-middle"
|
||||
>
|
||||
{{ "off" | i18n }} - {{ "ssoLoginIsRequired" | i18n }}
|
||||
</span>
|
||||
<ng-container *ngIf="!requireSsoPolicyEnabled">
|
||||
<span *ngIf="hasCredentials" bitBadge variant="success" class="!tw-align-middle">
|
||||
{{ "on" | i18n }}
|
||||
</span>
|
||||
<span *ngIf="!hasCredentials" bitBadge variant="secondary" class="!tw-align-middle">
|
||||
{{ "off" | i18n }}
|
||||
</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<span bitBadge variant="warning" class="!tw-align-middle">{{ "beta" | i18n }}</span>
|
||||
<span bitBadge variant="warning" class="!tw-align-middle">{{ "beta" | i18n }}</span>
|
||||
</span>
|
||||
<ng-container *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin tw-ml-1" aria-hidden="true"></i>
|
||||
</ng-container>
|
||||
|
||||
@@ -49,8 +49,9 @@
|
||||
appStopClick
|
||||
(click)="selectCipher(row)"
|
||||
title="{{ 'editItemWithName' | i18n: row.name }}"
|
||||
>{{ row.name }}</a
|
||||
>
|
||||
{{ row.name }}
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-template #cantManage>
|
||||
<span>{{ row.name }}</span>
|
||||
|
||||
@@ -7,25 +7,27 @@
|
||||
<span bitDialogTitle>
|
||||
<span *ngIf="!data.orgDomain">{{ "newDomain" | i18n }}</span>
|
||||
<span *ngIf="data.orgDomain">
|
||||
{{ ((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") | i18n }}
|
||||
</span>
|
||||
|
||||
<span *ngIf="data.orgDomain" class="tw-text-xs tw-text-muted">
|
||||
{{ data.orgDomain.domainName }}
|
||||
</span>
|
||||
|
||||
<span *ngIf="data?.orgDomain && !data.orgDomain?.verifiedDate" bitBadge variant="warning">
|
||||
{{
|
||||
((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") | i18n
|
||||
}}</span
|
||||
>
|
||||
|
||||
<span *ngIf="data.orgDomain" class="tw-text-xs tw-text-muted">{{
|
||||
data.orgDomain.domainName
|
||||
}}</span>
|
||||
|
||||
<span *ngIf="data?.orgDomain && !data.orgDomain?.verifiedDate" bitBadge variant="warning">{{
|
||||
((accountDeprovisioningEnabled$ | async)
|
||||
? "domainStatusUnderVerification"
|
||||
: "domainStatusUnverified"
|
||||
) | i18n
|
||||
}}</span>
|
||||
<span *ngIf="data?.orgDomain && data?.orgDomain?.verifiedDate" bitBadge variant="success">{{
|
||||
((accountDeprovisioningEnabled$ | async) ? "domainStatusClaimed" : "domainStatusVerified")
|
||||
| i18n
|
||||
}}</span>
|
||||
((accountDeprovisioningEnabled$ | async)
|
||||
? "domainStatusUnderVerification"
|
||||
: "domainStatusUnverified"
|
||||
) | i18n
|
||||
}}
|
||||
</span>
|
||||
<span *ngIf="data?.orgDomain && data?.orgDomain?.verifiedDate" bitBadge variant="success">
|
||||
{{
|
||||
((accountDeprovisioningEnabled$ | async) ? "domainStatusClaimed" : "domainStatusVerified")
|
||||
| i18n
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
<div bitDialogContent>
|
||||
<bit-form-field>
|
||||
|
||||
@@ -225,9 +225,10 @@ describe("NotificationsService", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked },
|
||||
{ initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked },
|
||||
{ initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked },
|
||||
// Temporarily rolling back notifications being connected while locked
|
||||
// { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Unlocked },
|
||||
// { initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Locked },
|
||||
// { initialStatus: AuthenticationStatus.Locked, updatedStatus: AuthenticationStatus.Locked },
|
||||
{ initialStatus: AuthenticationStatus.Unlocked, updatedStatus: AuthenticationStatus.Unlocked },
|
||||
])(
|
||||
"does not re-connect when the user transitions from $initialStatus to $updatedStatus",
|
||||
@@ -252,7 +253,11 @@ describe("NotificationsService", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked])(
|
||||
it.each([
|
||||
// Temporarily disabling notifications connecting while in a locked state
|
||||
// AuthenticationStatus.Locked,
|
||||
AuthenticationStatus.Unlocked,
|
||||
])(
|
||||
"connects when a user transitions from logged out to %s",
|
||||
async (newStatus: AuthenticationStatus) => {
|
||||
emitActiveUser(mockUser1);
|
||||
|
||||
@@ -123,13 +123,13 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
|
||||
);
|
||||
}
|
||||
|
||||
// This method name is a lie currently as we also have an access token
|
||||
// when locked, this is eventually where we want to be but it increases load
|
||||
// on signalR so we are rolling back until we can move the load of browser to
|
||||
// web push.
|
||||
private hasAccessToken$(userId: UserId) {
|
||||
return this.authService.authStatusFor$(userId).pipe(
|
||||
map(
|
||||
(authStatus) =>
|
||||
authStatus === AuthenticationStatus.Locked ||
|
||||
authStatus === AuthenticationStatus.Unlocked,
|
||||
),
|
||||
map((authStatus) => authStatus === AuthenticationStatus.Unlocked),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,24 @@ describe("MSecureCsvImporter.parse", () => {
|
||||
importer = new MSecureCsvImporter();
|
||||
});
|
||||
|
||||
it("should correctly parse legacy formatted cards", async () => {
|
||||
const mockCsvData =
|
||||
`aWeirdOldStyleCard|1032,Credit Card,,Security code 1234,Card Number|12|5555 4444 3333 2222,Expiration Date|11|04/0029,Name on Card|9|Obi Wan Kenobi,Security Code|9|444,`.trim();
|
||||
const result = await importer.parse(mockCsvData);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.ciphers.length).toBe(1);
|
||||
const cipher = result.ciphers[0];
|
||||
expect(cipher.name).toBe("aWeirdOldStyleCard");
|
||||
expect(cipher.type).toBe(CipherType.Card);
|
||||
expect(cipher.card.number).toBe("5555 4444 3333 2222");
|
||||
expect(cipher.card.expiration).toBe("04 / 2029");
|
||||
expect(cipher.card.code).toBe("444");
|
||||
expect(cipher.card.cardholderName).toBe("Obi Wan Kenobi");
|
||||
expect(cipher.notes).toBe("Security code 1234");
|
||||
expect(cipher.card.brand).toBe("");
|
||||
});
|
||||
|
||||
it("should correctly parse credit card entries as Secret Notes", async () => {
|
||||
const mockCsvData =
|
||||
`myCreditCard|155089404,Credit Card,,,Card Number|12|41111111111111111,Expiration Date|11|05/2026,Security Code|9|123,Name on Card|0|John Doe,PIN|9|1234,Issuing Bank|0|Visa,Phone Number|4|,Billing Address|0|,`.trim();
|
||||
|
||||
@@ -43,23 +43,34 @@ export class MSecureCsvImporter extends BaseImporter implements Importer {
|
||||
).split("/");
|
||||
cipher.card.expMonth = month.trim();
|
||||
cipher.card.expYear = year.trim();
|
||||
cipher.card.code = this.getValueOrDefault(this.splitValueRetainingLastPart(value[6]));
|
||||
cipher.card.cardholderName = this.getValueOrDefault(
|
||||
this.splitValueRetainingLastPart(value[7]),
|
||||
const securityCodeRegex = RegExp("^Security Code\\|\\d*\\|");
|
||||
const securityCodeEntry = value.find((entry: string) => securityCodeRegex.test(entry));
|
||||
cipher.card.code = this.getValueOrDefault(
|
||||
this.splitValueRetainingLastPart(securityCodeEntry),
|
||||
);
|
||||
cipher.card.brand = this.getValueOrDefault(this.splitValueRetainingLastPart(value[9]));
|
||||
cipher.notes =
|
||||
this.getValueOrDefault(value[8].split("|")[0]) +
|
||||
": " +
|
||||
this.getValueOrDefault(this.splitValueRetainingLastPart(value[8]), "") +
|
||||
"\n" +
|
||||
this.getValueOrDefault(value[10].split("|")[0]) +
|
||||
": " +
|
||||
this.getValueOrDefault(this.splitValueRetainingLastPart(value[10]), "") +
|
||||
"\n" +
|
||||
this.getValueOrDefault(value[11].split("|")[0]) +
|
||||
": " +
|
||||
this.getValueOrDefault(this.splitValueRetainingLastPart(value[11]), "");
|
||||
|
||||
const cardNameRegex = RegExp("^Name on Card\\|\\d*\\|");
|
||||
const nameOnCardEntry = value.find((entry: string) => entry.match(cardNameRegex));
|
||||
cipher.card.cardholderName = this.getValueOrDefault(
|
||||
this.splitValueRetainingLastPart(nameOnCardEntry),
|
||||
);
|
||||
|
||||
cipher.card.brand = this.getValueOrDefault(this.splitValueRetainingLastPart(value[9]), "");
|
||||
|
||||
const noteRegex = RegExp("\\|\\d*\\|");
|
||||
const rawNotes = value
|
||||
.slice(2)
|
||||
.filter((entry: string) => !this.isNullOrWhitespace(entry) && !noteRegex.test(entry));
|
||||
const noteIndexes = [8, 10, 11];
|
||||
const indexedNotes = noteIndexes
|
||||
.filter((idx) => value[idx] && noteRegex.test(value[idx]))
|
||||
.map((idx) => value[idx])
|
||||
.map((val) => {
|
||||
const key = val.split("|")[0];
|
||||
const value = this.getValueOrDefault(this.splitValueRetainingLastPart(val), "");
|
||||
return `${key}: ${value}`;
|
||||
});
|
||||
cipher.notes = [...rawNotes, ...indexedNotes].join("\n");
|
||||
} else if (value.length > 3) {
|
||||
cipher.type = CipherType.SecureNote;
|
||||
cipher.secureNote = new SecureNoteView();
|
||||
@@ -95,6 +106,6 @@ export class MSecureCsvImporter extends BaseImporter implements Importer {
|
||||
// like "Password|8|myPassword", we want to keep the "myPassword" but also ensure that if
|
||||
// the value contains any "|" it works fine
|
||||
private splitValueRetainingLastPart(value: string) {
|
||||
return value.split("|").slice(0, 2).concat(value.split("|").slice(2).join("|")).pop();
|
||||
return value && value.split("|").slice(0, 2).concat(value.split("|").slice(2).join("|")).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ export type OptionalInitialValues = {
|
||||
username?: string;
|
||||
password?: string;
|
||||
name?: string;
|
||||
cardholderName?: string;
|
||||
number?: string;
|
||||
expMonth?: string;
|
||||
expYear?: string;
|
||||
code?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,8 @@ describe("CardDetailsSectionComponent", () => {
|
||||
cardView.cardholderName = "Ron Burgundy";
|
||||
cardView.number = "4242 4242 4242 4242";
|
||||
cardView.brand = "Visa";
|
||||
cardView.expMonth = "";
|
||||
cardView.code = "";
|
||||
|
||||
expect(patchCipherSpy).toHaveBeenCalled();
|
||||
const patchFn = patchCipherSpy.mock.lastCall[0];
|
||||
@@ -79,6 +81,10 @@ describe("CardDetailsSectionComponent", () => {
|
||||
});
|
||||
|
||||
const cardView = new CardView();
|
||||
cardView.cardholderName = "";
|
||||
cardView.number = "";
|
||||
cardView.expMonth = "";
|
||||
cardView.code = "";
|
||||
cardView.expYear = "2022";
|
||||
|
||||
expect(patchCipherSpy).toHaveBeenCalled();
|
||||
|
||||
@@ -97,6 +97,10 @@ export class CardDetailsSectionComponent implements OnInit {
|
||||
|
||||
EventType = EventType;
|
||||
|
||||
get initialValues() {
|
||||
return this.cipherFormContainer.config.initialValues;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private cipherFormContainer: CipherFormContainer,
|
||||
private formBuilder: FormBuilder,
|
||||
@@ -139,7 +143,9 @@ export class CardDetailsSectionComponent implements OnInit {
|
||||
const prefillCipher = this.cipherFormContainer.getInitialCipherView();
|
||||
|
||||
if (prefillCipher) {
|
||||
this.setInitialValues(prefillCipher);
|
||||
this.initFromExistingCipher(prefillCipher.card);
|
||||
} else {
|
||||
this.initNewCipher();
|
||||
}
|
||||
|
||||
if (this.disabled) {
|
||||
@@ -147,6 +153,26 @@ export class CardDetailsSectionComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
private initFromExistingCipher(existingCard: CardView) {
|
||||
this.cardDetailsForm.patchValue({
|
||||
cardholderName: this.initialValues?.cardholderName ?? existingCard.cardholderName,
|
||||
number: this.initialValues?.number ?? existingCard.number,
|
||||
expMonth: this.initialValues?.expMonth ?? existingCard.expMonth,
|
||||
expYear: this.initialValues?.expYear ?? existingCard.expYear,
|
||||
code: this.initialValues?.code ?? existingCard.code,
|
||||
});
|
||||
}
|
||||
|
||||
private initNewCipher() {
|
||||
this.cardDetailsForm.patchValue({
|
||||
cardholderName: this.initialValues?.cardholderName || "",
|
||||
number: this.initialValues?.number || "",
|
||||
expMonth: this.initialValues?.expMonth || "",
|
||||
expYear: this.initialValues?.expYear || "",
|
||||
code: this.initialValues?.code || "",
|
||||
});
|
||||
}
|
||||
|
||||
/** Get the section heading based on the card brand */
|
||||
getSectionHeading(): string {
|
||||
const { brand } = this.cardDetailsForm.value;
|
||||
|
||||
@@ -15,7 +15,8 @@ import { isCardExpired } from "@bitwarden/common/autofill/utils";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
@@ -87,6 +88,7 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private changeLoginPasswordService: ChangeLoginPasswordService,
|
||||
private configService: ConfigService,
|
||||
private cipherService: CipherService,
|
||||
) {}
|
||||
|
||||
async ngOnChanges() {
|
||||
@@ -152,7 +154,12 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
|
||||
|
||||
const userId = await firstValueFrom(this.activeUserId$);
|
||||
|
||||
if (this.cipher.edit && this.cipher.viewPassword) {
|
||||
// Show Tasks for Manage and Edit permissions
|
||||
// Using cipherService to see if user has access to cipher in a non-AC context to address with Edit Except Password permissions
|
||||
const allCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
|
||||
const cipherServiceCipher = allCiphers[this.cipher?.id as CipherId];
|
||||
|
||||
if (cipherServiceCipher?.edit && cipherServiceCipher?.viewPassword) {
|
||||
await this.checkPendingChangePasswordTasks(userId);
|
||||
}
|
||||
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -189,7 +189,7 @@
|
||||
},
|
||||
"apps/browser": {
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2025.3.0"
|
||||
"version": "2025.3.1"
|
||||
},
|
||||
"apps/cli": {
|
||||
"name": "@bitwarden/cli",
|
||||
|
||||
Reference in New Issue
Block a user