mirror of
https://github.com/bitwarden/browser
synced 2026-02-07 12:13:45 +00:00
Merge branch 'main' into dirt/pm-19322/accessibility
This commit is contained in:
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
@@ -203,10 +203,10 @@ apps/web/src/locales/en/messages.json
|
||||
.github/workflows/release-web.yml @bitwarden/dept-bre
|
||||
|
||||
## Docker files have shared ownership ##
|
||||
**/Dockerfile
|
||||
**/*.Dockerfile
|
||||
**/.dockerignore
|
||||
**/entrypoint.sh
|
||||
**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/*.Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
|
||||
## Overrides
|
||||
# For the time being platform owns tsconfig and jest config
|
||||
|
||||
@@ -1934,32 +1934,81 @@
|
||||
"typeNote": {
|
||||
"message": "Note"
|
||||
},
|
||||
"newItemHeader": {
|
||||
"message": "New $TYPE$",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1",
|
||||
"example": "Login"
|
||||
}
|
||||
}
|
||||
"newItemHeaderLogin": {
|
||||
"message": "New Login",
|
||||
"description": "Header for new login item type"
|
||||
},
|
||||
"editItemHeader": {
|
||||
"message": "Edit $TYPE$",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1",
|
||||
"example": "Login"
|
||||
}
|
||||
}
|
||||
"newItemHeaderCard": {
|
||||
"message": "New Card",
|
||||
"description": "Header for new card item type"
|
||||
},
|
||||
"viewItemHeader": {
|
||||
"message": "View $TYPE$",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1",
|
||||
"example": "Login"
|
||||
}
|
||||
}
|
||||
"newItemHeaderIdentity": {
|
||||
"message": "New Identity",
|
||||
"description": "Header for new identity item type"
|
||||
},
|
||||
"newItemHeaderNote": {
|
||||
"message": "New Note",
|
||||
"description": "Header for new note item type"
|
||||
},
|
||||
"newItemHeaderSshKey": {
|
||||
"message": "New SSH key",
|
||||
"description": "Header for new SSH key item type"
|
||||
},
|
||||
"newItemHeaderTextSend": {
|
||||
"message": "New Text Send",
|
||||
"description": "Header for new text send"
|
||||
},
|
||||
"newItemHeaderFileSend": {
|
||||
"message": "New File Send",
|
||||
"description": "Header for new file send"
|
||||
},
|
||||
"editItemHeaderLogin": {
|
||||
"message": "Edit Login",
|
||||
"description": "Header for edit login item type"
|
||||
},
|
||||
"editItemHeaderCard": {
|
||||
"message": "Edit Card",
|
||||
"description": "Header for edit card item type"
|
||||
},
|
||||
"editItemHeaderIdentity": {
|
||||
"message": "Edit Identity",
|
||||
"description": "Header for edit identity item type"
|
||||
},
|
||||
"editItemHeaderNote": {
|
||||
"message": "Edit Note",
|
||||
"description": "Header for edit note item type"
|
||||
},
|
||||
"editItemHeaderSshKey": {
|
||||
"message": "Edit SSH key",
|
||||
"description": "Header for edit SSH key item type"
|
||||
},
|
||||
"editItemHeaderTextSend": {
|
||||
"message": "Edit Text Send",
|
||||
"description": "Header for edit text send"
|
||||
},
|
||||
"editItemHeaderFileSend": {
|
||||
"message": "Edit File Send",
|
||||
"description": "Header for edit file send"
|
||||
},
|
||||
"viewItemHeaderLogin": {
|
||||
"message": "View Login",
|
||||
"description": "Header for view login item type"
|
||||
},
|
||||
"viewItemHeaderCard": {
|
||||
"message": "View Card",
|
||||
"description": "Header for view card item type"
|
||||
},
|
||||
"viewItemHeaderIdentity": {
|
||||
"message": "View Identity",
|
||||
"description": "Header for view identity item type"
|
||||
},
|
||||
"viewItemHeaderNote": {
|
||||
"message": "View Note",
|
||||
"description": "Header for view note item type"
|
||||
},
|
||||
"viewItemHeaderSshKey": {
|
||||
"message": "View SSH key",
|
||||
"description": "Header for view SSH key item type"
|
||||
},
|
||||
"passwordHistory": {
|
||||
"message": "Password history"
|
||||
@@ -5092,15 +5141,9 @@
|
||||
"itemLocation": {
|
||||
"message": "Item Location"
|
||||
},
|
||||
"fileSend": {
|
||||
"message": "File Send"
|
||||
},
|
||||
"fileSends": {
|
||||
"message": "File Sends"
|
||||
},
|
||||
"textSend": {
|
||||
"message": "Text Send"
|
||||
},
|
||||
"textSends": {
|
||||
"message": "Text Sends"
|
||||
},
|
||||
|
||||
@@ -2513,6 +2513,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
"label-tag",
|
||||
"placeholder",
|
||||
"label-left",
|
||||
"label-right",
|
||||
"label-top",
|
||||
"label-aria",
|
||||
"dataSetValues",
|
||||
@@ -2609,7 +2610,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a fill script to place the `cilck_on_opid`, `focus_on_opid`, and `fill_by_opid`
|
||||
* Updates a fill script to place the `click_on_opid`, `focus_on_opid`, and `fill_by_opid`
|
||||
* fill script actions associated with the provided field.
|
||||
* @param {AutofillScript} fillScript
|
||||
* @param {AutofillField} field
|
||||
|
||||
@@ -221,7 +221,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -509,7 +509,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("is structured on a page with no password fields but has other types of fields in the form", () => {
|
||||
@@ -568,7 +568,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("contains a disabled autocomplete type when multiple password fields are on the page", () => {
|
||||
it("will not exclude a field by autocomplete type when it is the only viewable password field on the page", () => {
|
||||
const field = mock<AutofillField>({
|
||||
type: "text",
|
||||
autoCompleteType: "off",
|
||||
@@ -599,7 +599,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,6 @@ export class InlineMenuFieldQualificationService
|
||||
private newPasswordAutoCompleteValue = "new-password";
|
||||
private autofillFieldKeywordsMap: AutofillKeywordsMap = new WeakMap();
|
||||
private submitButtonKeywordsMap: SubmitButtonKeywordsMap = new WeakMap();
|
||||
private autocompleteDisabledValues = new Set(["off", "false"]);
|
||||
private accountCreationFieldKeywords = [
|
||||
"register",
|
||||
"registration",
|
||||
@@ -419,10 +418,8 @@ export class InlineMenuFieldQualificationService
|
||||
}
|
||||
|
||||
// If a single username field or less is present on the page, then we can assume that the
|
||||
// provided field is for a login form. This will only be the case if the field does not
|
||||
// explicitly have its autocomplete attribute set to "off" or "false".
|
||||
|
||||
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
// provided field is for a login form.
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the field has a form parent and there are multiple visible password fields
|
||||
@@ -442,9 +439,8 @@ export class InlineMenuFieldQualificationService
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the field has a form parent and no username field exists and the field has an
|
||||
// autocomplete attribute set to "off" or "false", this is not a password field
|
||||
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
// If the field has a form parent and a username field exists this is a password field
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -512,20 +508,12 @@ export class InlineMenuFieldQualificationService
|
||||
}
|
||||
|
||||
// If the page does not contain any password fields, it might be part of a multistep login form.
|
||||
// That will only be the case if the field does not explicitly have its autocomplete attribute
|
||||
// set to "off" or "false".
|
||||
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the field is structured within a form, but no password fields are present in the form,
|
||||
// we need to consider whether the field is part of a multistep login form.
|
||||
if (passwordFieldsInPageDetails.length === 0) {
|
||||
// If the field's autocomplete is set to a disabled value, we should assume that the field is
|
||||
// not part of a login form.
|
||||
if (this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the form that contains a single field, we should assume that it is part
|
||||
// of a multistep login form.
|
||||
const fieldsWithinForm = pageDetails.fields.filter(
|
||||
@@ -561,8 +549,7 @@ export class InlineMenuFieldQualificationService
|
||||
}
|
||||
|
||||
// If no visible password fields are found, this field might be part of a multipart form.
|
||||
// Check for an invalid autocompleteType to determine if the field is part of a login form.
|
||||
return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -188,14 +188,11 @@ export class SendAddEditComponent {
|
||||
* @returns The header text.
|
||||
*/
|
||||
private getHeaderText(mode: SendFormMode, type: SendType) {
|
||||
const headerKey =
|
||||
mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
|
||||
|
||||
switch (type) {
|
||||
case SendType.Text:
|
||||
return this.i18nService.t(headerKey, this.i18nService.t("textSend"));
|
||||
case SendType.File:
|
||||
return this.i18nService.t(headerKey, this.i18nService.t("fileSend"));
|
||||
}
|
||||
const isEditMode = mode === "edit" || mode === "partial-edit";
|
||||
const translation = {
|
||||
[SendType.Text]: isEditMode ? "editItemHeaderTextSend" : "newItemHeaderTextSend",
|
||||
[SendType.File]: isEditMode ? "editItemHeaderFileSend" : "newItemHeaderFileSend",
|
||||
};
|
||||
return this.i18nService.t(translation[type]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,20 +368,15 @@ export class AddEditV2Component implements OnInit {
|
||||
}
|
||||
|
||||
setHeader(mode: CipherFormMode, type: CipherType) {
|
||||
const partOne = mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
|
||||
|
||||
switch (type) {
|
||||
case CipherType.Login:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("typeLogin"));
|
||||
case CipherType.Card:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("typeCard"));
|
||||
case CipherType.Identity:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("typeIdentity"));
|
||||
case CipherType.SecureNote:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("note"));
|
||||
case CipherType.SshKey:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("typeSshKey"));
|
||||
}
|
||||
const isEditMode = mode === "edit" || mode === "partial-edit";
|
||||
const translation = {
|
||||
[CipherType.Login]: isEditMode ? "editItemHeaderLogin" : "newItemHeaderLogin",
|
||||
[CipherType.Card]: isEditMode ? "editItemHeaderCard" : "newItemHeaderCard",
|
||||
[CipherType.Identity]: isEditMode ? "editItemHeaderIdentity" : "newItemHeaderIdentity",
|
||||
[CipherType.SecureNote]: isEditMode ? "editItemHeaderNote" : "newItemHeaderNote",
|
||||
[CipherType.SshKey]: isEditMode ? "editItemHeaderSshKey" : "newItemHeaderSshKey",
|
||||
};
|
||||
return this.i18nService.t(translation[type]);
|
||||
}
|
||||
|
||||
delete = async () => {
|
||||
|
||||
@@ -172,28 +172,28 @@ describe("ViewV2Component", () => {
|
||||
params$.next({ cipherId: mockCipher.id });
|
||||
flush(); // Resolve all promises
|
||||
|
||||
expect(component.headerText).toEqual("viewItemHeader typeLogin");
|
||||
expect(component.headerText).toEqual("viewItemHeaderLogin");
|
||||
|
||||
// Set header text for a card
|
||||
mockCipher.type = CipherType.Card;
|
||||
params$.next({ cipherId: mockCipher.id });
|
||||
flush(); // Resolve all promises
|
||||
|
||||
expect(component.headerText).toEqual("viewItemHeader typeCard");
|
||||
expect(component.headerText).toEqual("viewItemHeaderCard");
|
||||
|
||||
// Set header text for an identity
|
||||
mockCipher.type = CipherType.Identity;
|
||||
params$.next({ cipherId: mockCipher.id });
|
||||
flush(); // Resolve all promises
|
||||
|
||||
expect(component.headerText).toEqual("viewItemHeader typeIdentity");
|
||||
expect(component.headerText).toEqual("viewItemHeaderIdentity");
|
||||
|
||||
// Set header text for a secure note
|
||||
mockCipher.type = CipherType.SecureNote;
|
||||
params$.next({ cipherId: mockCipher.id });
|
||||
flush(); // Resolve all promises
|
||||
|
||||
expect(component.headerText).toEqual("viewItemHeader note");
|
||||
expect(component.headerText).toEqual("viewItemHeaderNote");
|
||||
}));
|
||||
|
||||
it("sends viewed event", fakeAsync(() => {
|
||||
|
||||
@@ -194,18 +194,14 @@ export class ViewV2Component {
|
||||
}
|
||||
|
||||
setHeader(type: CipherType) {
|
||||
switch (type) {
|
||||
case CipherType.Login:
|
||||
return this.i18nService.t("viewItemHeader", this.i18nService.t("typeLogin"));
|
||||
case CipherType.Card:
|
||||
return this.i18nService.t("viewItemHeader", this.i18nService.t("typeCard"));
|
||||
case CipherType.Identity:
|
||||
return this.i18nService.t("viewItemHeader", this.i18nService.t("typeIdentity"));
|
||||
case CipherType.SecureNote:
|
||||
return this.i18nService.t("viewItemHeader", this.i18nService.t("note"));
|
||||
case CipherType.SshKey:
|
||||
return this.i18nService.t("viewItemHeader", this.i18nService.t("typeSshkey"));
|
||||
}
|
||||
const translation = {
|
||||
[CipherType.Login]: "viewItemHeaderLogin",
|
||||
[CipherType.Card]: "viewItemHeaderCard",
|
||||
[CipherType.Identity]: "viewItemHeaderIdentity",
|
||||
[CipherType.SecureNote]: "viewItemHeaderNote",
|
||||
[CipherType.SshKey]: "viewItemHeaderSshKey",
|
||||
};
|
||||
return this.i18nService.t(translation[type]);
|
||||
}
|
||||
|
||||
async getCipherData(id: string, userId: UserId) {
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"big-integer": "1.6.52",
|
||||
"browser-hrtime": "1.1.8",
|
||||
"chalk": "4.1.2",
|
||||
"commander": "11.1.0",
|
||||
"commander": "14.0.0",
|
||||
"core-js": "3.45.0",
|
||||
"form-data": "4.0.4",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
|
||||
4
apps/desktop/desktop_native/napi/index.d.ts
vendored
4
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -228,8 +228,8 @@ export declare namespace chromium_importer {
|
||||
login?: Login
|
||||
failure?: LoginImportFailure
|
||||
}
|
||||
export function getInstalledBrowsers(): Promise<Array<string>>
|
||||
export function getAvailableProfiles(browser: string): Promise<Array<ProfileInfo>>
|
||||
export function getInstalledBrowsers(): Array<string>
|
||||
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
|
||||
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
||||
}
|
||||
export declare namespace autotype {
|
||||
|
||||
@@ -926,17 +926,22 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
}
|
||||
} else if (this.activeFilter.selectedOrganizationId) {
|
||||
this.addOrganizationId = this.activeFilter.selectedOrganizationId;
|
||||
} else {
|
||||
// clear out organizationId when the user switches to a personal vault filter
|
||||
this.addOrganizationId = null;
|
||||
}
|
||||
if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) {
|
||||
this.folderId = this.activeFilter.selectedFolderId;
|
||||
}
|
||||
|
||||
if (this.addOrganizationId && this.config) {
|
||||
this.config.initialValues = {
|
||||
...this.config.initialValues,
|
||||
organizationId: this.addOrganizationId as OrganizationId,
|
||||
};
|
||||
if (this.config == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.initialValues = {
|
||||
...this.config.initialValues,
|
||||
organizationId: this.addOrganizationId as OrganizationId,
|
||||
};
|
||||
}
|
||||
|
||||
private async canNavigateAway(action: string, cipher?: CipherView) {
|
||||
|
||||
@@ -66,6 +66,7 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
@@ -288,6 +289,7 @@ export class vNextVaultComponent implements OnInit, OnDestroy {
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
private organizationWarningsService: OrganizationWarningsService,
|
||||
private collectionService: CollectionService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
this.userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
this.filter$ = this.routedVaultFilterService.filter$;
|
||||
@@ -357,9 +359,10 @@ export class vNextVaultComponent implements OnInit, OnDestroy {
|
||||
this.allCiphers$ = combineLatest([
|
||||
this.organization$,
|
||||
this.userId$,
|
||||
this.restrictedItemTypesService.restricted$,
|
||||
this.refreshingSubject$,
|
||||
]).pipe(
|
||||
switchMap(async ([organization, userId]) => {
|
||||
switchMap(async ([organization, userId, restricted]) => {
|
||||
// If user swaps organization reset the addAccessToggle
|
||||
if (!this.showAddAccessToggle || organization) {
|
||||
this.addAccessToggle(0);
|
||||
@@ -381,6 +384,11 @@ export class vNextVaultComponent implements OnInit, OnDestroy {
|
||||
ciphers = await this.cipherService.getManyFromApiForOrganization(organization.id);
|
||||
}
|
||||
|
||||
// Filter out restricted ciphers before indexing
|
||||
ciphers = ciphers.filter(
|
||||
(cipher) => !this.restrictedItemTypesService.isCipherRestricted(cipher, restricted),
|
||||
);
|
||||
|
||||
await this.searchService.indexCiphers(userId, ciphers, organization.id);
|
||||
return ciphers;
|
||||
}),
|
||||
|
||||
@@ -134,7 +134,7 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
|
||||
component["updateTitle"]();
|
||||
|
||||
expect(component["title"]).toBe("viewItemType typelogin");
|
||||
expect(component["title"]).toBe("viewItemHeaderLogin");
|
||||
});
|
||||
|
||||
it("sets card title", () => {
|
||||
@@ -142,7 +142,7 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
|
||||
component["updateTitle"]();
|
||||
|
||||
expect(component["title"]).toBe("viewItemType typecard");
|
||||
expect(component["title"]).toBe("viewItemHeaderCard");
|
||||
});
|
||||
|
||||
it("sets identity title", () => {
|
||||
@@ -150,7 +150,7 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
|
||||
component["updateTitle"]();
|
||||
|
||||
expect(component["title"]).toBe("viewItemType typeidentity");
|
||||
expect(component["title"]).toBe("viewItemHeaderIdentity");
|
||||
});
|
||||
|
||||
it("sets note title", () => {
|
||||
@@ -158,7 +158,7 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
|
||||
component["updateTitle"]();
|
||||
|
||||
expect(component["title"]).toBe("viewItemType note");
|
||||
expect(component["title"]).toBe("viewItemHeaderNote");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,22 +73,20 @@ export class EmergencyViewDialogComponent {
|
||||
};
|
||||
|
||||
private updateTitle() {
|
||||
const partOne = "viewItemType";
|
||||
|
||||
const type = this.cipher.type;
|
||||
|
||||
switch (type) {
|
||||
case CipherType.Login:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase());
|
||||
this.title = this.i18nService.t("viewItemHeaderLogin");
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase());
|
||||
this.title = this.i18nService.t("viewItemHeaderCard");
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase());
|
||||
this.title = this.i18nService.t("viewItemHeaderIdentity");
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase());
|
||||
this.title = this.i18nService.t("viewItemHeaderNote");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,36 +521,39 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
return await this.cipherService.decrypt(config.originalCipher, activeUserId);
|
||||
}
|
||||
|
||||
private updateTitle() {
|
||||
let partOne: string;
|
||||
private updateTitle(): void {
|
||||
const mode = this.formConfig.mode || this.params.mode;
|
||||
const type = this.cipher?.type ?? this.formConfig.cipherType;
|
||||
const translation: { [key: string]: { [key: number]: string } } = {
|
||||
view: {
|
||||
[CipherType.Login]: "viewItemHeaderLogin",
|
||||
[CipherType.Card]: "viewItemHeaderCard",
|
||||
[CipherType.Identity]: "viewItemHeaderIdentity",
|
||||
[CipherType.SecureNote]: "viewItemHeaderNote",
|
||||
[CipherType.SshKey]: "viewItemHeaderSshKey",
|
||||
},
|
||||
new: {
|
||||
[CipherType.Login]: "newItemHeaderLogin",
|
||||
[CipherType.Card]: "newItemHeaderCard",
|
||||
[CipherType.Identity]: "newItemHeaderIdentity",
|
||||
[CipherType.SecureNote]: "newItemHeaderNote",
|
||||
[CipherType.SshKey]: "newItemHeaderSshKey",
|
||||
},
|
||||
edit: {
|
||||
[CipherType.Login]: "editItemHeaderLogin",
|
||||
[CipherType.Card]: "editItemHeaderCard",
|
||||
[CipherType.Identity]: "editItemHeaderIdentity",
|
||||
[CipherType.SecureNote]: "editItemHeaderNote",
|
||||
[CipherType.SshKey]: "editItemHeaderSshKey",
|
||||
},
|
||||
};
|
||||
|
||||
if (this.params.mode === "view") {
|
||||
partOne = "viewItemType";
|
||||
} else if (this.formConfig.mode === "edit" || this.formConfig.mode === "partial-edit") {
|
||||
partOne = "editItemHeader";
|
||||
} else {
|
||||
partOne = "newItemHeader";
|
||||
}
|
||||
const effectiveMode =
|
||||
mode === "partial-edit" || mode === "edit" ? "edit" : translation[mode] ? mode : "new";
|
||||
|
||||
const type = this.cipher?.type ?? this.formConfig.cipherType ?? CipherType.Login;
|
||||
const fullTranslation = translation[effectiveMode][type];
|
||||
|
||||
switch (type) {
|
||||
case CipherType.Login:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin"));
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard"));
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity"));
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("note"));
|
||||
break;
|
||||
case CipherType.SshKey:
|
||||
this.title = this.i18nService.t(partOne, this.i18nService.t("typeSshKey"));
|
||||
break;
|
||||
}
|
||||
this.title = this.i18nService.t(fullTranslation);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -138,19 +138,15 @@ export class AddEditComponentV2 implements OnInit {
|
||||
* @returns The header text.
|
||||
*/
|
||||
setHeader(mode: CipherFormMode, type: CipherType) {
|
||||
const partOne = mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
|
||||
switch (type) {
|
||||
case CipherType.Login:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase());
|
||||
case CipherType.Card:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase());
|
||||
case CipherType.Identity:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase());
|
||||
case CipherType.SecureNote:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase());
|
||||
case CipherType.SshKey:
|
||||
return this.i18nService.t(partOne, this.i18nService.t("typeSshKey").toLowerCase());
|
||||
}
|
||||
const isEditMode = mode === "edit" || mode === "partial-edit";
|
||||
const translation = {
|
||||
[CipherType.Login]: isEditMode ? "editItemHeaderLogin" : "newItemHeaderLogin",
|
||||
[CipherType.Card]: isEditMode ? "editItemHeaderCard" : "newItemHeaderCard",
|
||||
[CipherType.Identity]: isEditMode ? "editItemHeaderIdentity" : "newItemHeaderIdentity",
|
||||
[CipherType.SecureNote]: isEditMode ? "editItemHeaderNote" : "newItemHeaderNote",
|
||||
[CipherType.SshKey]: isEditMode ? "editItemHeaderSshKey" : "newItemHeaderSshKey",
|
||||
};
|
||||
return this.i18nService.t(translation[type]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -194,15 +194,15 @@ export class ViewComponent implements OnInit {
|
||||
|
||||
switch (this.cipher.type) {
|
||||
case CipherType.Login:
|
||||
return this.i18nService.t("viewItemType", this.i18nService.t("typeLogin").toLowerCase());
|
||||
return this.i18nService.t("viewItemHeaderLogin");
|
||||
case CipherType.SecureNote:
|
||||
return this.i18nService.t("viewItemType", this.i18nService.t("note").toLowerCase());
|
||||
return this.i18nService.t("viewItemHeaderCard");
|
||||
case CipherType.Card:
|
||||
return this.i18nService.t("viewItemType", this.i18nService.t("typeCard").toLowerCase());
|
||||
return this.i18nService.t("viewItemHeaderIdentity");
|
||||
case CipherType.Identity:
|
||||
return this.i18nService.t("viewItemType", this.i18nService.t("typeIdentity").toLowerCase());
|
||||
return this.i18nService.t("viewItemHeaderNote");
|
||||
case CipherType.SshKey:
|
||||
return this.i18nService.t("viewItemType", this.i18nService.t("typeSshKey").toLowerCase());
|
||||
return this.i18nService.t("viewItemHeaderSshKey");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -734,32 +734,81 @@
|
||||
"viewItem": {
|
||||
"message": "View item"
|
||||
},
|
||||
"newItemHeader": {
|
||||
"message": "New $TYPE$",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1",
|
||||
"example": "login"
|
||||
}
|
||||
}
|
||||
"newItemHeaderLogin": {
|
||||
"message": "New Login",
|
||||
"description": "Header for new login item type"
|
||||
},
|
||||
"editItemHeader": {
|
||||
"message": "Edit $TYPE$",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1",
|
||||
"example": "login"
|
||||
}
|
||||
}
|
||||
"newItemHeaderCard": {
|
||||
"message": "New Card",
|
||||
"description": "Header for new card item type"
|
||||
},
|
||||
"viewItemType": {
|
||||
"message": "View $ITEMTYPE$",
|
||||
"placeholders": {
|
||||
"itemtype": {
|
||||
"content": "$1",
|
||||
"example": "login"
|
||||
}
|
||||
}
|
||||
"newItemHeaderIdentity": {
|
||||
"message": "New Identity",
|
||||
"description": "Header for new identity item type"
|
||||
},
|
||||
"newItemHeaderNote": {
|
||||
"message": "New Note",
|
||||
"description": "Header for new note item type"
|
||||
},
|
||||
"newItemHeaderSshKey": {
|
||||
"message": "New SSH key",
|
||||
"description": "Header for new SSH key item type"
|
||||
},
|
||||
"newItemHeaderTextSend": {
|
||||
"message": "New Text Send",
|
||||
"description": "Header for new text send"
|
||||
},
|
||||
"newItemHeaderFileSend": {
|
||||
"message": "New File Send",
|
||||
"description": "Header for new file send"
|
||||
},
|
||||
"editItemHeaderLogin": {
|
||||
"message": "Edit Login",
|
||||
"description": "Header for edit login item type"
|
||||
},
|
||||
"editItemHeaderCard": {
|
||||
"message": "Edit Card",
|
||||
"description": "Header for edit card item type"
|
||||
},
|
||||
"editItemHeaderIdentity": {
|
||||
"message": "Edit Identity",
|
||||
"description": "Header for edit identity item type"
|
||||
},
|
||||
"editItemHeaderNote": {
|
||||
"message": "Edit Note",
|
||||
"description": "Header for edit note item type"
|
||||
},
|
||||
"editItemHeaderSshKey": {
|
||||
"message": "Edit SSH key",
|
||||
"description": "Header for edit SSH key item type"
|
||||
},
|
||||
"editItemHeaderTextSend": {
|
||||
"message": "Edit Text Send",
|
||||
"description": "Header for edit text send"
|
||||
},
|
||||
"editItemHeaderFileSend": {
|
||||
"message": "Edit File Send",
|
||||
"description": "Header for edit file send"
|
||||
},
|
||||
"viewItemHeaderLogin": {
|
||||
"message": "View Login",
|
||||
"description": "Header for view login item type"
|
||||
},
|
||||
"viewItemHeaderCard": {
|
||||
"message": "View Card",
|
||||
"description": "Header for view card item type"
|
||||
},
|
||||
"viewItemHeaderIdentity": {
|
||||
"message": "View Identity",
|
||||
"description": "Header for view identity item type"
|
||||
},
|
||||
"viewItemHeaderNote": {
|
||||
"message": "View Note",
|
||||
"description": "Header for view note item type"
|
||||
},
|
||||
"viewItemHeaderSshKey": {
|
||||
"message": "View SSH key",
|
||||
"description": "Header for view SSH key item type"
|
||||
},
|
||||
"new": {
|
||||
"message": "New",
|
||||
@@ -9670,6 +9719,9 @@
|
||||
"failedToSaveIntegration": {
|
||||
"message": "Failed to save integration. Please try again later."
|
||||
},
|
||||
"failedToDeleteIntegration": {
|
||||
"message": "Failed to delete integration. Please try again later."
|
||||
},
|
||||
"deviceIdMissing": {
|
||||
"message": "Device ID is missing"
|
||||
},
|
||||
@@ -10214,15 +10266,9 @@
|
||||
"learnMoreAboutApi": {
|
||||
"message": "Learn more about Bitwarden's API"
|
||||
},
|
||||
"fileSend": {
|
||||
"message": "File Send"
|
||||
},
|
||||
"fileSends": {
|
||||
"message": "File Sends"
|
||||
},
|
||||
"textSend": {
|
||||
"message": "Text Send"
|
||||
},
|
||||
"textSends": {
|
||||
"message": "Text Sends"
|
||||
},
|
||||
|
||||
@@ -20,7 +20,6 @@ export type Integration = {
|
||||
*/
|
||||
newBadgeExpiration?: string;
|
||||
description?: string;
|
||||
isConnected?: boolean;
|
||||
canSetupConnection?: boolean;
|
||||
configuration?: string;
|
||||
template?: string;
|
||||
|
||||
@@ -156,6 +156,34 @@ export class HecOrganizationIntegrationService {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteHec(
|
||||
organizationId: OrganizationId,
|
||||
OrganizationIntegrationId: OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId,
|
||||
) {
|
||||
if (organizationId != this.organizationId$.getValue()) {
|
||||
throw new Error("Organization ID mismatch");
|
||||
}
|
||||
// delete the configuration first due to foreign key constraint
|
||||
await this.integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration(
|
||||
organizationId,
|
||||
OrganizationIntegrationId,
|
||||
OrganizationIntegrationConfigurationId,
|
||||
);
|
||||
|
||||
// delete the integration
|
||||
await this.integrationApiService.deleteOrganizationIntegration(
|
||||
organizationId,
|
||||
OrganizationIntegrationId,
|
||||
);
|
||||
|
||||
// update the local observable
|
||||
const updatedIntegrations = this._integrations$
|
||||
.getValue()
|
||||
.filter((i) => i.id !== OrganizationIntegrationId);
|
||||
this._integrations$.next(updatedIntegrations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a OrganizationIntegration for an OrganizationIntegrationId
|
||||
* @param integrationId id of the integration
|
||||
|
||||
@@ -13,12 +13,13 @@ import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { openHecConnectDialog } from "../integration-dialog";
|
||||
import { HecConnectDialogResultStatus, openHecConnectDialog } from "../integration-dialog";
|
||||
|
||||
import { IntegrationCardComponent } from "./integration-card.component";
|
||||
|
||||
jest.mock("../integration-dialog", () => ({
|
||||
openHecConnectDialog: jest.fn(),
|
||||
HecConnectDialogResultStatus: { Edited: "edit", Delete: "delete" },
|
||||
}));
|
||||
|
||||
describe("IntegrationCardComponent", () => {
|
||||
@@ -272,7 +273,7 @@ describe("IntegrationCardComponent", () => {
|
||||
it("should call updateHec if isUpdateAvailable is true", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: true,
|
||||
success: HecConnectDialogResultStatus.Edited,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
@@ -304,7 +305,7 @@ describe("IntegrationCardComponent", () => {
|
||||
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: true,
|
||||
success: HecConnectDialogResultStatus.Edited,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
@@ -327,10 +328,66 @@ describe("IntegrationCardComponent", () => {
|
||||
expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show toast on error", async () => {
|
||||
it("should call deleteHec when a delete is requested", async () => {
|
||||
component.organizationId = "org-id" as any;
|
||||
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: true,
|
||||
success: HecConnectDialogResultStatus.Delete,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
}),
|
||||
});
|
||||
|
||||
mockIntegrationService.deleteHec.mockResolvedValue(undefined);
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.deleteHec).toHaveBeenCalledWith(
|
||||
"org-id",
|
||||
"integration-id",
|
||||
"config-id",
|
||||
);
|
||||
expect(mockIntegrationService.saveHec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not call deleteHec if no existing configuration", async () => {
|
||||
component.integrationSettings = {
|
||||
organizationIntegration: null,
|
||||
name: OrganizationIntegrationServiceType.CrowdStrike,
|
||||
} as any;
|
||||
component.organizationId = "org-id" as any;
|
||||
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Delete,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
}),
|
||||
});
|
||||
|
||||
mockIntegrationService.deleteHec.mockResolvedValue(undefined);
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.deleteHec).not.toHaveBeenCalledWith(
|
||||
"org-id",
|
||||
"integration-id",
|
||||
"config-id",
|
||||
OrganizationIntegrationServiceType.CrowdStrike,
|
||||
"test-url",
|
||||
"token",
|
||||
"index",
|
||||
);
|
||||
expect(mockIntegrationService.updateHec).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should show toast on error while saving", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Edited,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
@@ -349,5 +406,28 @@ describe("IntegrationCardComponent", () => {
|
||||
message: mockI18nService.t("failedToSaveIntegration"),
|
||||
});
|
||||
});
|
||||
|
||||
it("should show toast on error while deleting", async () => {
|
||||
(openHecConnectDialog as jest.Mock).mockReturnValue({
|
||||
closed: of({
|
||||
success: HecConnectDialogResultStatus.Delete,
|
||||
url: "test-url",
|
||||
bearerToken: "token",
|
||||
index: "index",
|
||||
}),
|
||||
});
|
||||
|
||||
jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true);
|
||||
mockIntegrationService.deleteHec.mockRejectedValue(new Error("fail"));
|
||||
|
||||
await component.setupConnection();
|
||||
|
||||
expect(mockIntegrationService.deleteHec).toHaveBeenCalled();
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: mockI18nService.t("failedToDeleteIntegration"),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,11 @@ import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { openHecConnectDialog } from "../integration-dialog/index";
|
||||
import {
|
||||
HecConnectDialogResult,
|
||||
HecConnectDialogResultStatus,
|
||||
openHecConnectDialog,
|
||||
} from "../integration-dialog/index";
|
||||
|
||||
@Component({
|
||||
selector: "app-integration-card",
|
||||
@@ -142,32 +146,20 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.isUpdateAvailable) {
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
const orgIntegrationConfigurationId =
|
||||
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
if (result.success === HecConnectDialogResultStatus.Delete) {
|
||||
await this.deleteHec();
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("failedToDeleteIntegration"),
|
||||
});
|
||||
}
|
||||
|
||||
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
}
|
||||
|
||||
await this.hecOrganizationIntegrationService.updateHec(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
orgIntegrationConfigurationId,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceType,
|
||||
result.url,
|
||||
result.bearerToken,
|
||||
result.index,
|
||||
);
|
||||
} else {
|
||||
await this.hecOrganizationIntegrationService.saveHec(
|
||||
this.organizationId,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceType,
|
||||
result.url,
|
||||
result.bearerToken,
|
||||
result.index,
|
||||
);
|
||||
try {
|
||||
if (result.success === HecConnectDialogResultStatus.Edited) {
|
||||
await this.saveHec(result);
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
@@ -175,7 +167,55 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
title: "",
|
||||
message: this.i18nService.t("failedToSaveIntegration"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async saveHec(result: HecConnectDialogResult) {
|
||||
if (this.isUpdateAvailable) {
|
||||
// retrieve org integration and configuration ids
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
const orgIntegrationConfigurationId =
|
||||
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
|
||||
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
}
|
||||
|
||||
// update existing integration and configuration
|
||||
await this.hecOrganizationIntegrationService.updateHec(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
orgIntegrationConfigurationId,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceType,
|
||||
result.url,
|
||||
result.bearerToken,
|
||||
result.index,
|
||||
);
|
||||
} else {
|
||||
// create new integration and configuration
|
||||
await this.hecOrganizationIntegrationService.saveHec(
|
||||
this.organizationId,
|
||||
this.integrationSettings.name as OrganizationIntegrationServiceType,
|
||||
result.url,
|
||||
result.bearerToken,
|
||||
result.index,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteHec() {
|
||||
const orgIntegrationId = this.integrationSettings.organizationIntegration?.id;
|
||||
const orgIntegrationConfigurationId =
|
||||
this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id;
|
||||
|
||||
if (!orgIntegrationId || !orgIntegrationConfigurationId) {
|
||||
throw Error("Organization Integration ID or Configuration ID is missing");
|
||||
}
|
||||
|
||||
await this.hecOrganizationIntegrationService.deleteHec(
|
||||
this.organizationId,
|
||||
orgIntegrationId,
|
||||
orgIntegrationConfigurationId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,19 @@
|
||||
<button type="button" bitButton bitDialogClose buttonType="secondary" [disabled]="loading">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
|
||||
@if (canDelete) {
|
||||
<div class="tw-ml-auto">
|
||||
<button
|
||||
bitIconButton="bwi-trash"
|
||||
type="button"
|
||||
buttonType="danger"
|
||||
label="'delete' | i18n"
|
||||
[appA11yTitle]="'delete' | i18n"
|
||||
[bitAction]="delete"
|
||||
></button>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ConnectHecDialogComponent,
|
||||
HecConnectDialogParams,
|
||||
HecConnectDialogResult,
|
||||
HecConnectDialogResultStatus,
|
||||
openHecConnectDialog,
|
||||
} from "./connect-dialog-hec.component";
|
||||
|
||||
@@ -65,7 +66,6 @@ describe("ConnectDialogHecComponent", () => {
|
||||
imageDarkMode: "test-image-dark.png",
|
||||
newBadgeExpiration: "2024-12-31",
|
||||
description: "Test Description",
|
||||
isConnected: false,
|
||||
canSetupConnection: true,
|
||||
type: IntegrationType.EVENT,
|
||||
} as Integration;
|
||||
@@ -155,8 +155,7 @@ describe("ConnectDialogHecComponent", () => {
|
||||
bearerToken: "token",
|
||||
index: "1",
|
||||
service: "Test Service",
|
||||
success: true,
|
||||
error: null,
|
||||
success: HecConnectDialogResultStatus.Edited,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,10 +17,17 @@ export interface HecConnectDialogResult {
|
||||
bearerToken: string;
|
||||
index: string;
|
||||
service: string;
|
||||
success: boolean;
|
||||
error: string | null;
|
||||
success: HecConnectDialogResultStatusType | null;
|
||||
}
|
||||
|
||||
export const HecConnectDialogResultStatus = {
|
||||
Edited: "edit",
|
||||
Delete: "delete",
|
||||
} as const;
|
||||
|
||||
export type HecConnectDialogResultStatusType =
|
||||
(typeof HecConnectDialogResultStatus)[keyof typeof HecConnectDialogResultStatus];
|
||||
|
||||
@Component({
|
||||
templateUrl: "./connect-dialog-hec.component.html",
|
||||
imports: [SharedModule],
|
||||
@@ -40,6 +47,7 @@ export class ConnectHecDialogComponent implements OnInit {
|
||||
@Inject(DIALOG_DATA) protected connectInfo: HecConnectDialogParams,
|
||||
protected formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef<HecConnectDialogResult>,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -62,23 +70,51 @@ export class ConnectHecDialogComponent implements OnInit {
|
||||
return !!this.hecConfig;
|
||||
}
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
const formJson = this.formGroup.getRawValue();
|
||||
get canDelete(): boolean {
|
||||
return !!this.hecConfig;
|
||||
}
|
||||
|
||||
const result: HecConnectDialogResult = {
|
||||
integrationSettings: this.connectInfo.settings,
|
||||
url: formJson.url || "",
|
||||
bearerToken: formJson.bearerToken || "",
|
||||
index: formJson.index || "",
|
||||
service: formJson.service || "",
|
||||
success: true,
|
||||
error: null,
|
||||
};
|
||||
submit = async (): Promise<void> => {
|
||||
if (this.formGroup.invalid) {
|
||||
this.formGroup.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
const result = this.getHecConnectDialogResult(HecConnectDialogResultStatus.Edited);
|
||||
|
||||
this.dialogRef.close(result);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
delete = async (): Promise<void> => {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteItem" },
|
||||
content: {
|
||||
key: "deleteItemConfirmation",
|
||||
},
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const result = this.getHecConnectDialogResult(HecConnectDialogResultStatus.Delete);
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
};
|
||||
|
||||
private getHecConnectDialogResult(
|
||||
status: HecConnectDialogResultStatusType,
|
||||
): HecConnectDialogResult {
|
||||
const formJson = this.formGroup.getRawValue();
|
||||
|
||||
return {
|
||||
integrationSettings: this.connectInfo.settings,
|
||||
url: formJson.url || "",
|
||||
bearerToken: formJson.bearerToken || "",
|
||||
index: formJson.index || "",
|
||||
service: formJson.service || "",
|
||||
success: status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function openHecConnectDialog(
|
||||
|
||||
@@ -253,7 +253,6 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
description: "crowdstrikeEventIntegrationDesc",
|
||||
isConnected: false,
|
||||
canSetupConnection: true,
|
||||
};
|
||||
|
||||
@@ -265,6 +264,11 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
this.hecOrganizationIntegrationService.integrations$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((integrations) => {
|
||||
// reset all integrations to null first - in case one was deleted
|
||||
this.integrationsList.forEach((i) => {
|
||||
i.organizationIntegration = null;
|
||||
});
|
||||
|
||||
integrations.map((integration) => {
|
||||
const item = this.integrationsList.find((i) => i.name === integration.serviceType);
|
||||
if (item) {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service";
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-api.service";
|
||||
import { OrganizationIntegrationConfigurationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-configuration-api.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { safeProvider } from "@bitwarden/ui-common";
|
||||
|
||||
import { IntegrationCardComponent } from "../../dirt/organization-integrations/integration-card/integration-card.component";
|
||||
import { IntegrationGridComponent } from "../../dirt/organization-integrations/integration-grid/integration-grid.component";
|
||||
import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
|
||||
@@ -14,6 +20,23 @@ import { IntegrationsComponent } from "./integrations.component";
|
||||
IntegrationCardComponent,
|
||||
IntegrationGridComponent,
|
||||
],
|
||||
providers: [
|
||||
safeProvider({
|
||||
provide: HecOrganizationIntegrationService,
|
||||
useClass: HecOrganizationIntegrationService,
|
||||
deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OrganizationIntegrationApiService,
|
||||
useClass: OrganizationIntegrationApiService,
|
||||
deps: [ApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OrganizationIntegrationConfigurationApiService,
|
||||
useClass: OrganizationIntegrationConfigurationApiService,
|
||||
deps: [ApiService],
|
||||
}),
|
||||
],
|
||||
declarations: [IntegrationsComponent],
|
||||
})
|
||||
export class IntegrationsModule {}
|
||||
|
||||
110
libs/common/src/enums/event-category.enum.ts
Normal file
110
libs/common/src/enums/event-category.enum.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { EventType } from "./event-type.enum";
|
||||
|
||||
export const EventCategory = {
|
||||
UserEvents: "userEvents",
|
||||
ItemEvents: "itemEvents",
|
||||
CollectionEvents: "collectionEvents",
|
||||
GroupEvents: "groupEvents",
|
||||
OrganizationMemberEvents: "organizationMemberEvents",
|
||||
OrganizationEvents: "organizationEvents",
|
||||
ProviderEvents: "providerEvents",
|
||||
} as const;
|
||||
|
||||
export type EventCategory = (typeof EventCategory)[keyof typeof EventCategory];
|
||||
|
||||
export const EventCategoryEventTypes: Record<EventCategory, EventType[]> = {
|
||||
[EventCategory.UserEvents]: [
|
||||
EventType.User_LoggedIn,
|
||||
EventType.User_ChangedPassword,
|
||||
EventType.User_Updated2fa,
|
||||
EventType.User_Disabled2fa,
|
||||
EventType.User_Recovered2fa,
|
||||
EventType.User_FailedLogIn,
|
||||
EventType.User_FailedLogIn2fa,
|
||||
EventType.User_ClientExportedVault,
|
||||
EventType.User_UpdatedTempPassword,
|
||||
EventType.User_MigratedKeyToKeyConnector,
|
||||
EventType.User_RequestedDeviceApproval,
|
||||
EventType.User_TdeOffboardingPasswordSet,
|
||||
],
|
||||
[EventCategory.ItemEvents]: [
|
||||
EventType.Cipher_Created,
|
||||
EventType.Cipher_Updated,
|
||||
EventType.Cipher_Deleted,
|
||||
EventType.Cipher_AttachmentCreated,
|
||||
EventType.Cipher_AttachmentDeleted,
|
||||
EventType.Cipher_Shared,
|
||||
EventType.Cipher_UpdatedCollections,
|
||||
EventType.Cipher_ClientViewed,
|
||||
EventType.Cipher_ClientToggledPasswordVisible,
|
||||
EventType.Cipher_ClientToggledHiddenFieldVisible,
|
||||
EventType.Cipher_ClientToggledCardCodeVisible,
|
||||
EventType.Cipher_ClientCopiedPassword,
|
||||
EventType.Cipher_ClientCopiedHiddenField,
|
||||
EventType.Cipher_ClientCopiedCardCode,
|
||||
EventType.Cipher_ClientAutofilled,
|
||||
EventType.Cipher_SoftDeleted,
|
||||
EventType.Cipher_Restored,
|
||||
EventType.Cipher_ClientToggledCardNumberVisible,
|
||||
EventType.Cipher_ClientToggledTOTPSeedVisible,
|
||||
],
|
||||
[EventCategory.CollectionEvents]: [
|
||||
EventType.Collection_Created,
|
||||
EventType.Collection_Updated,
|
||||
EventType.Collection_Deleted,
|
||||
],
|
||||
[EventCategory.GroupEvents]: [
|
||||
EventType.Group_Created,
|
||||
EventType.Group_Updated,
|
||||
EventType.Group_Deleted,
|
||||
],
|
||||
[EventCategory.OrganizationMemberEvents]: [
|
||||
EventType.OrganizationUser_Invited,
|
||||
EventType.OrganizationUser_Confirmed,
|
||||
EventType.OrganizationUser_Updated,
|
||||
EventType.OrganizationUser_Removed,
|
||||
EventType.OrganizationUser_UpdatedGroups,
|
||||
EventType.OrganizationUser_UnlinkedSso,
|
||||
EventType.OrganizationUser_ResetPassword_Enroll,
|
||||
EventType.OrganizationUser_ResetPassword_Withdraw,
|
||||
EventType.OrganizationUser_AdminResetPassword,
|
||||
EventType.OrganizationUser_ResetSsoLink,
|
||||
EventType.OrganizationUser_FirstSsoLogin,
|
||||
EventType.OrganizationUser_Revoked,
|
||||
EventType.OrganizationUser_Restored,
|
||||
EventType.OrganizationUser_ApprovedAuthRequest,
|
||||
EventType.OrganizationUser_RejectedAuthRequest,
|
||||
EventType.OrganizationUser_Deleted,
|
||||
EventType.OrganizationUser_Left,
|
||||
],
|
||||
[EventCategory.OrganizationEvents]: [
|
||||
EventType.Organization_Updated,
|
||||
EventType.Organization_PurgedVault,
|
||||
EventType.Organization_ClientExportedVault,
|
||||
EventType.Organization_VaultAccessed,
|
||||
EventType.Organization_EnabledSso,
|
||||
EventType.Organization_DisabledSso,
|
||||
EventType.Organization_EnabledKeyConnector,
|
||||
EventType.Organization_DisabledKeyConnector,
|
||||
EventType.Organization_SponsorshipsSynced,
|
||||
EventType.Organization_CollectionManagementUpdated,
|
||||
EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled,
|
||||
EventType.Organization_CollectionManagement_LimitCollectionCreationDisabled,
|
||||
EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled,
|
||||
EventType.Organization_CollectionManagement_LimitCollectionDeletionDisabled,
|
||||
EventType.Organization_CollectionManagement_LimitItemDeletionEnabled,
|
||||
EventType.Organization_CollectionManagement_LimitItemDeletionDisabled,
|
||||
EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled,
|
||||
EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsDisabled,
|
||||
],
|
||||
[EventCategory.ProviderEvents]: [
|
||||
EventType.ProviderUser_Invited,
|
||||
EventType.ProviderUser_Confirmed,
|
||||
EventType.ProviderUser_Updated,
|
||||
EventType.ProviderUser_Removed,
|
||||
EventType.ProviderOrganization_Created,
|
||||
EventType.ProviderOrganization_Added,
|
||||
EventType.ProviderOrganization_Removed,
|
||||
EventType.ProviderOrganization_VaultAccessed,
|
||||
],
|
||||
};
|
||||
@@ -41,6 +41,7 @@ export enum FeatureFlag {
|
||||
|
||||
/* DIRT */
|
||||
EventBasedOrganizationIntegrations = "event-based-organization-integrations",
|
||||
PM22887_RiskInsightsActivityTab = "pm-22887-risk-insights-activity-tab",
|
||||
|
||||
/* Vault */
|
||||
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
|
||||
@@ -84,6 +85,7 @@ export const DefaultFeatureFlagValue = {
|
||||
|
||||
/* DIRT */
|
||||
[FeatureFlag.EventBasedOrganizationIntegrations]: FALSE,
|
||||
[FeatureFlag.PM22887_RiskInsightsActivityTab]: FALSE,
|
||||
|
||||
/* Vault */
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
</a>
|
||||
|
||||
<div class="tw-text-center tw-mb-4 sm:tw-mb-6 tw-mx-auto" [ngClass]="maxWidthClass">
|
||||
<div *ngIf="!hideIcon()" class="tw-size-20 sm:tw-size-24 tw-mx-auto tw-content-center">
|
||||
<!-- In some scenarios this icon's size is not limited by container width correctly -->
|
||||
<!-- Targeting the SVG here to try and ensure it never grows too large in even the media queries are not working as expected -->
|
||||
<div
|
||||
*ngIf="!hideIcon()"
|
||||
class="tw-size-20 sm:tw-size-24 [&_svg]:tw-w-full [&_svg]:tw-max-w-24 tw-mx-auto tw-content-center"
|
||||
>
|
||||
<bit-icon [icon]="icon()"></bit-icon>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@ import { Meta } from "@storybook/addon-docs";
|
||||
|
||||
<style>
|
||||
{`
|
||||
/* undo global app style for ol */
|
||||
ol {
|
||||
list-style: revert;
|
||||
}
|
||||
|
||||
.subheading {
|
||||
--mediumdark: '#999999';
|
||||
font-weight: 900;
|
||||
@@ -171,6 +176,11 @@ what would be helpful to you if you were consuming this component for the first
|
||||
- You'll need to mock any services required for the component (team-owned components often have
|
||||
more of this mocking to do)
|
||||
|
||||
### Running Storybook locally
|
||||
|
||||
To view your changes locally, cd into `/clients`, run `npm ci` if you haven't already, and run
|
||||
`npm run storybook`.
|
||||
|
||||
---
|
||||
|
||||
<div className="subheading">References</div>
|
||||
|
||||
@@ -4,9 +4,27 @@ import { Meta } from "@storybook/addon-docs";
|
||||
|
||||
# Migrating to the Component Library
|
||||
|
||||
## Background
|
||||
|
||||
Bitwarden is in the process of migrating client code to fully use the Component Library.
|
||||
|
||||
Previously, the clients used a mix of multiple UI frameworks: Bootstrap, custom "box" styles, and
|
||||
this component library, which is built on top of Tailwind.
|
||||
|
||||
Bootstrap is now removed from the clients repo in favor of using Tailwind Preflight for global reset
|
||||
styles.
|
||||
|
||||
Tailwind is used in client code where layout-level html is written in conjunction with Component
|
||||
Library components.
|
||||
|
||||
Some custom css still lingers in all the clients and is planned to be removed. Do not write more
|
||||
custom css.
|
||||
|
||||
## Performing a migration
|
||||
|
||||
You have been tasked with migrating a component to use the CL. What does that entail?
|
||||
|
||||
## Getting Started
|
||||
### Getting Started
|
||||
|
||||
Before progressing here, please ensure that...
|
||||
|
||||
@@ -15,13 +33,6 @@ Before progressing here, please ensure that...
|
||||
- You are familiar with [Angular reactive forms](https://angular.io/guide/reactive-forms).
|
||||
- You are familiar with [Tailwind](https://tailwindcss.com/docs/utility-first).
|
||||
|
||||
## Background
|
||||
|
||||
The design of Bitwarden is in flux. At the time of writing, the frontend codebase uses a mix of
|
||||
multiple UI frameworks: Bootstrap, custom "box" styles, and this component library, which is built
|
||||
on top of Tailwind. In short, the "CL migration" is a move to only use the CL and remove everything
|
||||
else.
|
||||
|
||||
This is very important work. Centralizing around a shared design system will:
|
||||
|
||||
- improve user experience by utilizing consistent patterns
|
||||
@@ -29,11 +40,11 @@ This is very important work. Centralizing around a shared design system will:
|
||||
- improve dev & design velocity by having a central location to make UI/UX changes that impact the
|
||||
entire project
|
||||
|
||||
## Success Criteria
|
||||
### Success Criteria
|
||||
|
||||
Follow these steps to fully migrate a component.
|
||||
|
||||
### Use Storybook
|
||||
#### Use Storybook
|
||||
|
||||
Don't recreate the wheel.
|
||||
|
||||
@@ -42,7 +53,7 @@ usecase. Don't waste effort styling a button or building a popover menu from scr
|
||||
have those. If a component isn't flexible enough or doesn't exist for your usecase, contact the
|
||||
Component Library team.
|
||||
|
||||
### Use Tailwind
|
||||
#### Use Tailwind
|
||||
|
||||
Only use Tailwind for styling. No Bootstrap or other custom CSS is allowed.
|
||||
|
||||
@@ -72,7 +83,7 @@ without this prefix, it probably shouldn't be there.
|
||||
```
|
||||
</div>
|
||||
|
||||
### Use Reactive Forms
|
||||
#### Use Reactive Forms
|
||||
|
||||
The CL has form components that integrate with Angular's reactive forms: `bit-form-field`,
|
||||
`bitSubmit`, `bit-form-control`, etc. All forms should be migrated from template-drive forms to
|
||||
@@ -97,7 +108,7 @@ reactive forms to make use of these components. Review the
|
||||
```
|
||||
</div>
|
||||
|
||||
### Dialogs
|
||||
#### Dialogs
|
||||
|
||||
Legacy Bootstrap modals use the `ModalService`. These should be converted to use the `DialogService`
|
||||
and it's [related CL components](?path=/docs/component-library-dialogs--docs). Components that are
|
||||
@@ -161,11 +172,11 @@ fully migrated should have no reference to the `ModalService`.
|
||||
|
||||
<div class="tw-bg-success-600/10 tw-p-4">`FooDialogComponent.open(this.dialogService);`</div>
|
||||
|
||||
## Examples
|
||||
### Examples
|
||||
|
||||
The following examples come from accross the Bitwarden codebase.
|
||||
|
||||
### 1.) AboutComponent
|
||||
#### 1.) AboutComponent
|
||||
|
||||
Codeowner: Platform
|
||||
|
||||
@@ -176,7 +187,7 @@ This migration updates a `ModalService` component to the `DialogService`.
|
||||
**Note:** Most of the internal markup of this component was unchanged, aside from the removal of
|
||||
defunct Bootstrap classes.
|
||||
|
||||
### 2.) Auth
|
||||
#### 2.) Auth
|
||||
|
||||
Codeowner: Auth
|
||||
|
||||
@@ -190,7 +201,7 @@ This PR also does some general refactoring, the main relevant change can be seen
|
||||
|
||||
Updates a dialog, similar to example 1, but also adds CL form components and Angular Reactive Forms.
|
||||
|
||||
### 3.) AC
|
||||
#### 3.) AC
|
||||
|
||||
Codeowner: Admin Console
|
||||
|
||||
@@ -198,7 +209,7 @@ https://github.com/bitwarden/clients/pull/5417
|
||||
|
||||
Migrates dialog, form, buttons, and a table.
|
||||
|
||||
### 4.) Vault
|
||||
#### 4.) Vault
|
||||
|
||||
Codeowner: Vault
|
||||
|
||||
@@ -209,6 +220,6 @@ through the use of inheritance. This PR updates the _web_ template of a cross-cl
|
||||
use Tailwind and the CL, and updates the base component implementation to use reactive forms,
|
||||
without updating the desktop or browser templates.
|
||||
|
||||
## Questions
|
||||
### Questions
|
||||
|
||||
Please direct any development questions to the Component Library team. Thank you!
|
||||
|
||||
@@ -136,5 +136,17 @@ describe("BitwardenPasswordProtectedImporter", () => {
|
||||
jDoc.data = null;
|
||||
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
|
||||
});
|
||||
|
||||
it("returns invalidFilePassword errorMessage if decryptString throws", async () => {
|
||||
encryptService.decryptString.mockImplementation(() => {
|
||||
throw new Error("SDK error");
|
||||
});
|
||||
i18nService.t.mockReturnValue("invalidFilePassword");
|
||||
|
||||
const result = await importer.parse(JSON.stringify(jDoc));
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorMessage).toBe("invalidFilePassword");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,14 +90,12 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im
|
||||
|
||||
const encKeyValidation = new EncString(jdoc.encKeyValidation_DO_NOT_EDIT);
|
||||
|
||||
const encKeyValidationDecrypt = await this.encryptService.decryptString(
|
||||
encKeyValidation,
|
||||
this.key,
|
||||
);
|
||||
if (encKeyValidationDecrypt === null) {
|
||||
try {
|
||||
await this.encryptService.decryptString(encKeyValidation, this.key);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private cannotParseFile(jdoc: BitwardenPasswordProtectedFileFormat): boolean {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
appAutofocus
|
||||
buttonType="primary"
|
||||
class="tw-mb-3"
|
||||
[disabled]="unlockingViaBiometrics || !biometricsAvailable"
|
||||
|
||||
@@ -153,15 +153,12 @@ export class SendAddEditDialogComponent {
|
||||
* @returns The header text.
|
||||
*/
|
||||
private getHeaderText(mode: SendFormMode, type: SendType) {
|
||||
const headerKey =
|
||||
mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
|
||||
|
||||
switch (type) {
|
||||
case SendType.Text:
|
||||
return this.i18nService.t(headerKey, this.i18nService.t("textSend"));
|
||||
case SendType.File:
|
||||
return this.i18nService.t(headerKey, this.i18nService.t("fileSend"));
|
||||
}
|
||||
const isEditMode = mode === "edit" || mode === "partial-edit";
|
||||
const translation = {
|
||||
[SendType.Text]: isEditMode ? "editItemHeaderTextSend" : "newItemHeaderTextSend",
|
||||
[SendType.File]: isEditMode ? "editItemHeaderFileSend" : "newItemHeaderFileSend",
|
||||
};
|
||||
return this.i18nService.t(translation[type]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -40,7 +40,7 @@
|
||||
"buffer": "6.0.3",
|
||||
"bufferutil": "4.0.9",
|
||||
"chalk": "4.1.2",
|
||||
"commander": "11.1.0",
|
||||
"commander": "14.0.0",
|
||||
"core-js": "3.45.0",
|
||||
"form-data": "4.0.4",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
@@ -203,7 +203,7 @@
|
||||
"big-integer": "1.6.52",
|
||||
"browser-hrtime": "1.1.8",
|
||||
"chalk": "4.1.2",
|
||||
"commander": "11.1.0",
|
||||
"commander": "14.0.0",
|
||||
"core-js": "3.45.0",
|
||||
"form-data": "4.0.4",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
@@ -18366,12 +18366,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz",
|
||||
"integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/common-path-prefix": {
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
"buffer": "6.0.3",
|
||||
"bufferutil": "4.0.9",
|
||||
"chalk": "4.1.2",
|
||||
"commander": "11.1.0",
|
||||
"commander": "14.0.0",
|
||||
"core-js": "3.45.0",
|
||||
"form-data": "4.0.4",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
|
||||
Reference in New Issue
Block a user