mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 03:33:30 +00:00
Merge remote-tracking branch 'origin' into auth/pm-19877/notification-processing
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<ng-container *ngIf="show">
|
||||
<ng-container [ngSwitch]="displayMode">
|
||||
<ng-container *ngSwitchCase="'personalOwnershipPolicy'">
|
||||
<ng-container *ngSwitchCase="'organizationDataOwnershipPolicy'">
|
||||
<div class="filter-heading" [ngClass]="{ active: !hasActiveFilter }">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -103,48 +103,11 @@
|
||||
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned && organization"
|
||||
class="tw-shrink-0"
|
||||
>
|
||||
<!-- "New" menu is always shown unless the user cannot create a cipher and cannot create a collection-->
|
||||
<ng-container *ngIf="canCreateCipher || canCreateCollection">
|
||||
<div appListDropdown>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
[bitMenuTriggerFor]="addOptions"
|
||||
id="newItemDropdown"
|
||||
appA11yTitle="{{ 'new' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
||||
<ng-container *ngIf="canCreateCipher">
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
|
||||
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeLogin" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
|
||||
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeCard" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
|
||||
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeIdentity" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
|
||||
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
|
||||
{{ "note" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="canCreateCollection">
|
||||
<bit-menu-divider *ngIf="canCreateCipher"></bit-menu-divider>
|
||||
<button type="button" bitMenuItem (click)="addCollection()">
|
||||
<i class="bwi bwi-fw bwi-collection-shared" aria-hidden="true"></i>
|
||||
{{ "collection" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-menu>
|
||||
</div>
|
||||
</ng-container>
|
||||
<vault-new-cipher-menu
|
||||
[canCreateCipher]="canCreateCipher"
|
||||
[canCreateCollection]="canCreateCollection"
|
||||
(cipherAdded)="addCipher($event)"
|
||||
(collectionAdded)="addCollection()"
|
||||
/>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
SearchModule,
|
||||
SimpleDialogOptions,
|
||||
} from "@bitwarden/components";
|
||||
import { NewCipherMenuComponent } from "@bitwarden/vault";
|
||||
|
||||
import { HeaderModule } from "../../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
@@ -45,6 +46,7 @@ import { CollectionDialogTabType } from "../../shared/components/collection-dial
|
||||
HeaderModule,
|
||||
SearchModule,
|
||||
JslibModule,
|
||||
NewCipherMenuComponent,
|
||||
],
|
||||
})
|
||||
export class VaultHeaderComponent {
|
||||
|
||||
@@ -16,8 +16,7 @@ export default {
|
||||
component: ReportCardComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [JslibModule, BadgeModule, IconModule, RouterTestingModule],
|
||||
declarations: [PremiumBadgeComponent],
|
||||
imports: [JslibModule, BadgeModule, IconModule, RouterTestingModule, PremiumBadgeComponent],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
|
||||
|
||||
@@ -18,8 +18,8 @@ export default {
|
||||
component: ReportListComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [JslibModule, BadgeModule, RouterTestingModule, IconModule],
|
||||
declarations: [PremiumBadgeComponent, ReportCardComponent],
|
||||
imports: [JslibModule, BadgeModule, RouterTestingModule, IconModule, PremiumBadgeComponent],
|
||||
declarations: [ReportCardComponent],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
|
||||
|
||||
@@ -42,10 +42,8 @@ import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { HeaderModule } from "../layouts/header/header.module";
|
||||
import { PremiumBadgeComponent } from "../vault/components/premium-badge.component";
|
||||
import { FolderAddEditComponent } from "../vault/individual-vault/folder-add-edit.component";
|
||||
import { OrganizationBadgeModule } from "../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "../vault/individual-vault/pipes/pipes.module";
|
||||
import { PurgeVaultComponent } from "../vault/settings/purge-vault.component";
|
||||
|
||||
import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component";
|
||||
import { SharedModule } from "./shared.module";
|
||||
@@ -68,6 +66,7 @@ import { SharedModule } from "./shared.module";
|
||||
OrganizationLayoutComponent,
|
||||
VerifyRecoverDeleteOrgComponent,
|
||||
VaultTimeoutInputComponent,
|
||||
PremiumBadgeComponent,
|
||||
],
|
||||
declarations: [
|
||||
AcceptFamilySponsorshipComponent,
|
||||
@@ -76,7 +75,6 @@ import { SharedModule } from "./shared.module";
|
||||
EmergencyAccessConfirmComponent,
|
||||
EmergencyAccessTakeoverComponent,
|
||||
EmergencyAccessViewComponent,
|
||||
FolderAddEditComponent,
|
||||
OrgEventsComponent,
|
||||
OrgExposedPasswordsReportComponent,
|
||||
OrgInactiveTwoFactorReportComponent,
|
||||
@@ -84,8 +82,6 @@ import { SharedModule } from "./shared.module";
|
||||
OrgUnsecuredWebsitesReportComponent,
|
||||
OrgUserConfirmComponent,
|
||||
OrgWeakPasswordsReportComponent,
|
||||
PremiumBadgeComponent,
|
||||
PurgeVaultComponent,
|
||||
RecoverDeleteComponent,
|
||||
RecoverTwoFactorComponent,
|
||||
RemovePasswordComponent,
|
||||
@@ -106,7 +102,6 @@ import { SharedModule } from "./shared.module";
|
||||
EmergencyAccessConfirmComponent,
|
||||
EmergencyAccessTakeoverComponent,
|
||||
EmergencyAccessViewComponent,
|
||||
FolderAddEditComponent,
|
||||
OrganizationLayoutComponent,
|
||||
OrgEventsComponent,
|
||||
OrgExposedPasswordsReportComponent,
|
||||
@@ -116,7 +111,6 @@ import { SharedModule } from "./shared.module";
|
||||
OrgUserConfirmComponent,
|
||||
OrgWeakPasswordsReportComponent,
|
||||
PremiumBadgeComponent,
|
||||
PurgeVaultComponent,
|
||||
RecoverDeleteComponent,
|
||||
RecoverTwoFactorComponent,
|
||||
RemovePasswordComponent,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { BadgeModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-premium-badge",
|
||||
@@ -9,7 +11,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
{{ "premium" | i18n }}
|
||||
</button>
|
||||
`,
|
||||
standalone: false,
|
||||
imports: [JslibModule, BadgeModule],
|
||||
})
|
||||
export class PremiumBadgeComponent {
|
||||
constructor(private messagingService: MessagingService) {}
|
||||
|
||||
@@ -510,7 +510,7 @@ export class VaultItemsComponent {
|
||||
|
||||
private compareNames(a: VaultItem, b: VaultItem): number {
|
||||
const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name;
|
||||
return getName(a).localeCompare(getName(b));
|
||||
return getName(a)?.localeCompare(getName(b)) ?? -1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<form [bitSubmit]="submitAndClose" [formGroup]="formGroup">
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>
|
||||
{{ title }}
|
||||
</span>
|
||||
<span bitDialogContent>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||
<input bitInput id="name" formControlName="name" />
|
||||
</bit-form-field>
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" bitFormButton type="submit">
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<div class="tw-m-0 tw-ml-auto">
|
||||
<button
|
||||
buttonType="danger"
|
||||
bitIconButton="bwi-trash"
|
||||
bitFormButton
|
||||
type="button"
|
||||
appA11yTitle="{{ 'delete' | i18n }}"
|
||||
*ngIf="editMode"
|
||||
[bitAction]="deleteAndClose"
|
||||
></button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -1,139 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/vault/components/folder-add-edit.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
selector: "app-folder-add-edit",
|
||||
templateUrl: "folder-add-edit.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class FolderAddEditComponent extends BaseFolderAddEditComponent {
|
||||
protected override componentName = "app-folder-add-edit";
|
||||
constructor(
|
||||
folderService: FolderService,
|
||||
folderApiService: FolderApiServiceAbstraction,
|
||||
protected accountSerivce: AccountService,
|
||||
protected keyService: KeyService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
logService: LogService,
|
||||
dialogService: DialogService,
|
||||
formBuilder: FormBuilder,
|
||||
protected toastService: ToastService,
|
||||
protected dialogRef: DialogRef<FolderAddEditDialogResult>,
|
||||
@Inject(DIALOG_DATA) params: FolderAddEditDialogParams,
|
||||
) {
|
||||
super(
|
||||
folderService,
|
||||
folderApiService,
|
||||
accountSerivce,
|
||||
keyService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
dialogService,
|
||||
formBuilder,
|
||||
toastService,
|
||||
);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
params?.folderId ? (this.folderId = params.folderId) : null;
|
||||
}
|
||||
|
||||
deleteAndClose = async () => {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteFolder" },
|
||||
content: { key: "deleteFolderConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.folderApiService.delete(this.folder.id, await firstValueFrom(this.activeUserId$));
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("deletedFolder"),
|
||||
});
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
this.dialogRef.close(FolderAddEditDialogResult.Deleted);
|
||||
};
|
||||
|
||||
submitAndClose = async () => {
|
||||
this.folder.name = this.formGroup.controls.name.value;
|
||||
if (this.folder.name == null || this.folder.name === "") {
|
||||
this.formGroup.controls.name.markAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeAccountId = await firstValueFrom(this.activeUserId$);
|
||||
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeAccountId);
|
||||
const folder = await this.folderService.encrypt(this.folder, userKey);
|
||||
this.formPromise = this.folderApiService.save(folder, activeAccountId);
|
||||
await this.formPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t(this.editMode ? "editedFolder" : "addedFolder"),
|
||||
});
|
||||
this.onSavedFolder.emit(this.folder);
|
||||
this.dialogRef.close(FolderAddEditDialogResult.Saved);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FolderAddEditDialogParams {
|
||||
folderId: string;
|
||||
}
|
||||
|
||||
export const FolderAddEditDialogResult = {
|
||||
Deleted: "deleted",
|
||||
Canceled: "canceled",
|
||||
Saved: "saved",
|
||||
} as const;
|
||||
|
||||
export type FolderAddEditDialogResult = UnionOfValues<typeof FolderAddEditDialogResult>;
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a FolderAddEdit dialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Optional configuration for the dialog
|
||||
*/
|
||||
export function openFolderAddEditDialog(
|
||||
dialogService: DialogService,
|
||||
config?: DialogConfig<FolderAddEditDialogParams>,
|
||||
) {
|
||||
return dialogService.open<FolderAddEditDialogResult, FolderAddEditDialogParams>(
|
||||
FolderAddEditComponent,
|
||||
config,
|
||||
);
|
||||
}
|
||||
@@ -68,35 +68,13 @@
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="filter.type !== 'trash'" class="tw-shrink-0">
|
||||
<div appListDropdown>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
[bitMenuTriggerFor]="addOptions"
|
||||
id="newItemDropdown"
|
||||
appA11yTitle="{{ 'new' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
||||
@for (item of cipherMenuItems$ | async; track item.type) {
|
||||
<button type="button" bitMenuItem (click)="addCipher(item.type)">
|
||||
<i class="bwi {{ item.icon }}" slot="start" aria-hidden="true"></i>
|
||||
{{ item.labelKey | i18n }}
|
||||
</button>
|
||||
}
|
||||
<bit-menu-divider />
|
||||
<button type="button" bitMenuItem (click)="addFolder()">
|
||||
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
||||
{{ "folder" | i18n }}
|
||||
</button>
|
||||
<button *ngIf="canCreateCollections" type="button" bitMenuItem (click)="addCollection()">
|
||||
<i class="bwi bwi-fw bwi-collection-shared" aria-hidden="true"></i>
|
||||
{{ "collection" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
</div>
|
||||
<vault-new-cipher-menu
|
||||
[canCreateCipher]="true"
|
||||
[canCreateFolder]="true"
|
||||
[canCreateCollection]="canCreateCollections"
|
||||
(cipherAdded)="addCipher($event)"
|
||||
(folderAdded)="addFolder()"
|
||||
(collectionAdded)="addCollection()"
|
||||
/>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, map, shareReplay } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
Unassigned,
|
||||
@@ -18,13 +18,13 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import {
|
||||
BreadcrumbsModule,
|
||||
DialogService,
|
||||
MenuModule,
|
||||
SimpleDialogOptions,
|
||||
} from "@bitwarden/components";
|
||||
import { NewCipherMenuComponent } from "@bitwarden/vault";
|
||||
|
||||
import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog";
|
||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
HeaderModule,
|
||||
PipesModule,
|
||||
JslibModule,
|
||||
NewCipherMenuComponent,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
@@ -54,21 +55,6 @@ export class VaultHeaderComponent {
|
||||
protected All = All;
|
||||
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||
protected CipherType = CipherType;
|
||||
protected allCipherMenuItems = [
|
||||
{ type: CipherType.Login, icon: "bwi-globe", labelKey: "typeLogin" },
|
||||
{ type: CipherType.Card, icon: "bwi-credit-card", labelKey: "typeCard" },
|
||||
{ type: CipherType.Identity, icon: "bwi-id-card", labelKey: "typeIdentity" },
|
||||
{ type: CipherType.SecureNote, icon: "bwi-sticky-note", labelKey: "note" },
|
||||
{ type: CipherType.SshKey, icon: "bwi-key", labelKey: "typeSshKey" },
|
||||
];
|
||||
protected cipherMenuItems$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedTypes) => {
|
||||
return this.allCipherMenuItems.filter((item) => {
|
||||
return !restrictedTypes.some((restrictedType) => restrictedType.cipherType === item.type);
|
||||
});
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
/**
|
||||
* Boolean to determine the loading state of the header.
|
||||
@@ -109,7 +95,6 @@ export class VaultHeaderComponent {
|
||||
private dialogService: DialogService,
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,14 +18,16 @@ import {
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { UserVerificationModule } from "../../auth/shared/components/user-verification";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
export interface PurgeVaultDialogData {
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-purge-vault",
|
||||
templateUrl: "purge-vault.component.html",
|
||||
standalone: false,
|
||||
imports: [SharedModule, UserVerificationModule],
|
||||
})
|
||||
export class PurgeVaultComponent {
|
||||
organizationId: string = null;
|
||||
|
||||
@@ -647,6 +647,9 @@
|
||||
"typeSecureNote": {
|
||||
"message": "Secure note"
|
||||
},
|
||||
"typeNote": {
|
||||
"message": "Note"
|
||||
},
|
||||
"typeSshKey": {
|
||||
"message": "SSH key"
|
||||
},
|
||||
@@ -8928,7 +8931,7 @@
|
||||
},
|
||||
"uriMatchDefaultStrategyHint": {
|
||||
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
|
||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||
},
|
||||
"regExAdvancedOptionWarning": {
|
||||
"message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.",
|
||||
|
||||
@@ -74,7 +74,10 @@ export class AccountSecurityNudgeService extends DefaultSingleNudgeService {
|
||||
hasSpotlightDismissed: status.hasSpotlightDismissed || hideNudge,
|
||||
};
|
||||
|
||||
if (isPinSet || biometricUnlockEnabled || hasOrgWithRemovePinPolicyOn) {
|
||||
if (
|
||||
(isPinSet || biometricUnlockEnabled || hasOrgWithRemovePinPolicyOn) &&
|
||||
!status.hasSpotlightDismissed
|
||||
) {
|
||||
await this.setNudgeStatus(nudgeType, acctSecurityNudgeStatus, userId);
|
||||
}
|
||||
return acctSecurityNudgeStatus;
|
||||
|
||||
@@ -44,7 +44,11 @@ export class HasItemsNudgeService extends DefaultSingleNudgeService {
|
||||
return cipher.deletedDate == null;
|
||||
});
|
||||
|
||||
if (profileOlderThanCutoff && filteredCiphers.length > 0) {
|
||||
if (
|
||||
profileOlderThanCutoff &&
|
||||
filteredCiphers.length > 0 &&
|
||||
!nudgeStatus.hasSpotlightDismissed
|
||||
) {
|
||||
const dismissedStatus = {
|
||||
hasSpotlightDismissed: true,
|
||||
hasBadgeDismissed: true,
|
||||
|
||||
@@ -49,7 +49,7 @@ export class NewItemNudgeService extends DefaultSingleNudgeService {
|
||||
|
||||
const ciphersBoolean = ciphers.some((cipher) => cipher.type === currentType);
|
||||
|
||||
if (ciphersBoolean) {
|
||||
if (ciphersBoolean && !nudgeStatus.hasSpotlightDismissed) {
|
||||
const dismissedStatus = {
|
||||
hasSpotlightDismissed: true,
|
||||
hasBadgeDismissed: true,
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
/**
|
||||
* The authentication status of the user
|
||||
*
|
||||
* See `AuthService.authStatusFor$` for details on how we determine the user's `AuthenticationStatus`
|
||||
*/
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum AuthenticationStatus {
|
||||
/**
|
||||
* User is not authenticated
|
||||
* - The user does not have an active account userId and/or an access token in state
|
||||
*/
|
||||
LoggedOut = 0,
|
||||
|
||||
/**
|
||||
* User is authenticated but not decrypted
|
||||
* - The user has an access token, but no user key in state
|
||||
* - Vault data cannot be decrypted (because there is no user key)
|
||||
*/
|
||||
Locked = 1,
|
||||
|
||||
/**
|
||||
* User is authenticated and decrypted
|
||||
* - The user has an access token and a user key in state
|
||||
* - Vault data can be decrypted (via user key)
|
||||
*/
|
||||
Unlocked = 2,
|
||||
}
|
||||
|
||||
@@ -419,11 +419,13 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
userId: UserId,
|
||||
): Promise<[CipherView[], CipherView[]] | null> {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) {
|
||||
const decryptStartTime = new Date().getTime();
|
||||
const decryptStartTime = performance.now();
|
||||
const decrypted = await this.decryptCiphersWithSdk(ciphers, userId);
|
||||
this.logService.info(
|
||||
`[CipherService] Decrypting ${decrypted.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`,
|
||||
);
|
||||
|
||||
this.logService.measure(decryptStartTime, "Vault", "CipherService", "decrypt complete", [
|
||||
["Items", ciphers.length],
|
||||
]);
|
||||
|
||||
// With SDK, failed ciphers are not returned
|
||||
return [decrypted, []];
|
||||
}
|
||||
@@ -442,7 +444,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
},
|
||||
{} as Record<string, Cipher[]>,
|
||||
);
|
||||
const decryptStartTime = new Date().getTime();
|
||||
const decryptStartTime = performance.now();
|
||||
const allCipherViews = (
|
||||
await Promise.all(
|
||||
Object.entries(grouped).map(async ([orgId, groupedCiphers]) => {
|
||||
@@ -462,9 +464,11 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
)
|
||||
.flat()
|
||||
.sort(this.getLocaleSortingFunction());
|
||||
this.logService.info(
|
||||
`[CipherService] Decrypting ${allCipherViews.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`,
|
||||
);
|
||||
|
||||
this.logService.measure(decryptStartTime, "Vault", "CipherService", "decrypt complete", [
|
||||
["Items", ciphers.length],
|
||||
]);
|
||||
|
||||
// Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt
|
||||
return allCipherViews.reduce(
|
||||
(acc, c) => {
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable, Subject, firstValueFrom, map, shareReplay, switchMap, merge } from "rxjs";
|
||||
import {
|
||||
Observable,
|
||||
Subject,
|
||||
firstValueFrom,
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
merge,
|
||||
filter,
|
||||
combineLatest,
|
||||
} from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -69,8 +79,12 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
||||
|
||||
const observable = merge(
|
||||
this.forceFolderViews[userId],
|
||||
this.encryptedFoldersState(userId).state$.pipe(
|
||||
switchMap((folderData) => {
|
||||
combineLatest([
|
||||
this.encryptedFoldersState(userId).state$,
|
||||
this.keyService.userKey$(userId),
|
||||
]).pipe(
|
||||
filter(([folderData, userKey]) => folderData != null && userKey != null),
|
||||
switchMap(([folderData, _]) => {
|
||||
return this.decryptFolders(userId, folderData);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -129,12 +129,15 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
}
|
||||
|
||||
async isSearchable(userId: UserId, query: string): Promise<boolean> {
|
||||
const time = performance.now();
|
||||
query = SearchService.normalizeSearchQuery(query);
|
||||
const index = await this.getIndexForSearch(userId);
|
||||
const notSearchable =
|
||||
query == null ||
|
||||
(index == null && query.length < this.searchableMinLength) ||
|
||||
(index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0);
|
||||
|
||||
this.logService.measure(time, "Vault", "SearchService", "isSearchable");
|
||||
return !notSearchable;
|
||||
}
|
||||
|
||||
@@ -147,7 +150,7 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexingStartTime = new Date().getTime();
|
||||
const indexingStartTime = performance.now();
|
||||
await this.setIsIndexing(userId, true);
|
||||
await this.setIndexedEntityIdForSearch(userId, indexedEntityId as IndexedEntityId);
|
||||
const builder = new lunr.Builder();
|
||||
@@ -188,11 +191,10 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
await this.setIndexForSearch(userId, index.toJSON() as SerializedLunrIndex);
|
||||
|
||||
await this.setIsIndexing(userId, false);
|
||||
this.logService.info(
|
||||
`[SearchService] Building search index of ${ciphers.length} ciphers took ${
|
||||
new Date().getTime() - indexingStartTime
|
||||
}ms`,
|
||||
);
|
||||
|
||||
this.logService.measure(indexingStartTime, "Vault", "SearchService", "index complete", [
|
||||
["Items", ciphers.length],
|
||||
]);
|
||||
}
|
||||
|
||||
async searchCiphers(
|
||||
|
||||
@@ -577,6 +577,9 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
throw new Error("No active user.");
|
||||
}
|
||||
|
||||
// Add a mark to indicate that the user has unlocked their vault. A good starting point for measuring unlock performance.
|
||||
this.logService.mark("Vault unlocked");
|
||||
|
||||
await this.keyService.setUserKey(key, this.activeAccount.id);
|
||||
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
|
||||
@@ -54,4 +54,43 @@ export class ConsoleLogService implements LogService {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
measure(
|
||||
start: DOMHighResTimeStamp,
|
||||
trackGroup: string,
|
||||
track: string,
|
||||
name?: string,
|
||||
properties?: [string, any][],
|
||||
): PerformanceMeasure {
|
||||
const measureName = `[${track}]: ${name}`;
|
||||
|
||||
const measure = performance.measure(measureName, {
|
||||
start: start,
|
||||
detail: {
|
||||
devtools: {
|
||||
dataType: "track-entry",
|
||||
track,
|
||||
trackGroup,
|
||||
properties,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.info(`${measureName} took ${measure.duration}`, properties);
|
||||
return measure;
|
||||
}
|
||||
|
||||
mark(name: string): PerformanceMark {
|
||||
const mark = performance.mark(name, {
|
||||
detail: {
|
||||
devtools: {
|
||||
dataType: "marker",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.info(mark.name, new Date().toISOString());
|
||||
|
||||
return mark;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,28 @@ export abstract class LogService {
|
||||
abstract warning(message?: any, ...optionalParams: any[]): void;
|
||||
abstract error(message?: any, ...optionalParams: any[]): void;
|
||||
abstract write(level: LogLevel, message?: any, ...optionalParams: any[]): void;
|
||||
|
||||
/**
|
||||
* Helper wrapper around `performance.measure` to log a measurement. Should also debug-log the data.
|
||||
*
|
||||
* @param start Start time of the measurement.
|
||||
* @param trackGroup A track-group for the measurement, should generally be the team owning the domain.
|
||||
* @param track A track for the measurement, should generally be the class name.
|
||||
* @param measureName A descriptive name for the measurement.
|
||||
* @param properties Additional properties to include.
|
||||
*/
|
||||
abstract measure(
|
||||
start: DOMHighResTimeStamp,
|
||||
trackGroup: string,
|
||||
track: string,
|
||||
measureName: string,
|
||||
properties?: [string, any][],
|
||||
): PerformanceMeasure;
|
||||
|
||||
/**
|
||||
* Helper wrapper around `performance.mark` to log a mark. Should also debug-log the data.
|
||||
*
|
||||
* @param name Name of the mark to create.
|
||||
*/
|
||||
abstract mark(name: string): PerformanceMark;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<ng-container *ngIf="canCreateCipher || canCreateCollection || canCreateFolder">
|
||||
<div>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
[bitMenuTriggerFor]="addOptions"
|
||||
id="newItemDropdown"
|
||||
[appA11yTitle]="'new' | i18n"
|
||||
>
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}
|
||||
</button>
|
||||
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
||||
@for (item of cipherMenuItems$ | async; track item.type) {
|
||||
<button type="button" bitMenuItem (click)="cipherAdded.emit(item.type)">
|
||||
<i class="bwi {{ item.icon }}" slot="start" aria-hidden="true"></i>
|
||||
{{ item.labelKey | i18n }}
|
||||
</button>
|
||||
}
|
||||
<bit-menu-divider *ngIf="canCreateCipher"></bit-menu-divider>
|
||||
<button *ngIf="canCreateFolder" type="button" bitMenuItem (click)="folderAdded.emit()">
|
||||
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
||||
{{ "folder" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="canCreateCollection"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="collectionAdded.emit()"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-collection-shared" aria-hidden="true"></i>
|
||||
{{ "collection" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,38 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, input, output } from "@angular/core";
|
||||
import { map, shareReplay } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
import { ButtonModule, MenuModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@Component({
|
||||
selector: "vault-new-cipher-menu",
|
||||
templateUrl: "new-cipher-menu.component.html",
|
||||
imports: [ButtonModule, CommonModule, MenuModule, I18nPipe, JslibModule],
|
||||
})
|
||||
export class NewCipherMenuComponent {
|
||||
canCreateCipher = input(false);
|
||||
canCreateFolder = input(false);
|
||||
canCreateCollection = input(false);
|
||||
folderAdded = output();
|
||||
collectionAdded = output();
|
||||
cipherAdded = output<CipherType>();
|
||||
|
||||
constructor(private restrictedItemTypesService: RestrictedItemTypesService) {}
|
||||
|
||||
/**
|
||||
* Returns an observable that emits the cipher menu items, filtered by the restricted types.
|
||||
*/
|
||||
cipherMenuItems$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedTypes) => {
|
||||
return CIPHER_MENU_ITEMS.filter((item) => {
|
||||
return !restrictedTypes.some((restrictedType) => restrictedType.cipherType === item.type);
|
||||
});
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export { DecryptionFailureDialogComponent } from "./components/decryption-failur
|
||||
export { openPasswordHistoryDialog } from "./components/password-history/password-history.component";
|
||||
export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component";
|
||||
export * from "./components/carousel";
|
||||
export * from "./components/new-cipher-menu/new-cipher-menu.component";
|
||||
|
||||
export * as VaultIcons from "./icons";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user