1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-27 18:13:29 +00:00

Merge remote-tracking branch 'origin' into auth/pm-19877/notification-processing

This commit is contained in:
Patrick Pimentel
2025-07-17 09:32:56 -04:00
27 changed files with 246 additions and 299 deletions

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
}

View File

@@ -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) => {

View File

@@ -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);
}),
),

View File

@@ -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(

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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 }),
);
}

View File

@@ -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";