1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 12:13:45 +00:00

Merge remote-tracking branch 'origin' into auth/pm-18720/change-password-component-non-dialog-v3

This commit is contained in:
Patrick Pimentel
2025-06-10 16:51:21 -06:00
521 changed files with 7401 additions and 6359 deletions

View File

@@ -11,7 +11,6 @@ import { ButtonModule } from "@bitwarden/components";
*/
@Component({
selector: "app-authentication-timeout",
standalone: true,
imports: [CommonModule, JslibModule, ButtonModule, RouterModule],
template: `
<p class="tw-text-center">

View File

@@ -85,11 +85,11 @@ import { IconComponent } from "./vault/components/icon.component";
TextDragDirective,
CopyClickDirective,
A11yTitleDirective,
AutofocusDirective,
],
declarations: [
A11yInvalidDirective,
ApiActionDirective,
AutofocusDirective,
BoxRowDirective,
DeprecatedCalloutComponent,
CopyTextDirective,

View File

@@ -0,0 +1,36 @@
import { mock } from "jest-mock-extended";
import { Subject } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DocumentLangSetter } from "./document-lang.setter";
describe("DocumentLangSetter", () => {
const document = mock<Document>();
const i18nService = mock<I18nService>();
const sut = new DocumentLangSetter(document, i18nService);
describe("start", () => {
it("reacts to locale changes while start called with a non-closed subscription", async () => {
const localeSubject = new Subject<string>();
i18nService.locale$ = localeSubject;
localeSubject.next("en");
expect(document.documentElement.lang).toBeFalsy();
const sub = sut.start();
localeSubject.next("es");
expect(document.documentElement.lang).toBe("es");
sub.unsubscribe();
localeSubject.next("ar");
expect(document.documentElement.lang).toBe("es");
});
});
});

View File

@@ -0,0 +1,26 @@
import { Subscription } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
/**
* A service for managing the setting of the `lang="<locale>" attribute on the
* main document for the application.
*/
export class DocumentLangSetter {
constructor(
private readonly document: Document,
private readonly i18nService: I18nService,
) {}
/**
* Starts listening to an upstream source for the best locale for the user
* and applies it to the application document.
* @returns A subscription that can be unsubscribed if you wish to stop
* applying lang attribute updates to the application document.
*/
start(): Subscription {
return this.i18nService.locale$.subscribe((locale) => {
this.document.documentElement.lang = locale;
});
}
}

View File

@@ -0,0 +1 @@
export { DocumentLangSetter } from "./document-lang.setter";

View File

@@ -21,6 +21,7 @@ import { SafeInjectionToken } from "@bitwarden/ui-common";
export { SafeInjectionToken } from "@bitwarden/ui-common";
export const WINDOW = new SafeInjectionToken<Window>("WINDOW");
export const DOCUMENT = new SafeInjectionToken<Document>("DOCUMENT");
export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken<
AbstractStorageService & ObservableStorageService
>("OBSERVABLE_MEMORY_STORAGE");

View File

@@ -337,6 +337,7 @@ import {
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
import { DocumentLangSetter } from "../platform/i18n";
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
import { LoggingErrorHandler } from "../platform/services/logging-error-handler";
import { AngularThemingService } from "../platform/services/theming/angular-theming.service";
@@ -349,6 +350,7 @@ import { NoopViewCacheService } from "../platform/view-cache/internal";
import {
CLIENT_TYPE,
DEFAULT_VAULT_TIMEOUT,
DOCUMENT,
ENV_ADDITIONAL_REGIONS,
HTTP_OPERATIONS,
INTRAPROCESS_MESSAGING_SUBJECT,
@@ -378,6 +380,7 @@ const safeProviders: SafeProvider[] = [
safeProvider(ModalService),
safeProvider(PasswordRepromptService),
safeProvider({ provide: WINDOW, useValue: window }),
safeProvider({ provide: DOCUMENT, useValue: document }),
safeProvider({
provide: LOCALE_ID as SafeInjectionToken<string>,
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
@@ -1460,12 +1463,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: CipherAuthorizationService,
useClass: DefaultCipherAuthorizationService,
deps: [
CollectionService,
OrganizationServiceAbstraction,
AccountServiceAbstraction,
ConfigService,
],
deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction],
}),
safeProvider({
provide: AuthRequestApiService,
@@ -1512,7 +1510,6 @@ const safeProviders: SafeProvider[] = [
StateProvider,
ApiServiceAbstraction,
OrganizationServiceAbstraction,
ConfigService,
AuthServiceAbstraction,
NotificationsService,
MessageListener,
@@ -1544,6 +1541,11 @@ const safeProviders: SafeProvider[] = [
useClass: MasterPasswordApiService,
deps: [ApiServiceAbstraction, LogService],
}),
safeProvider({
provide: DocumentLangSetter,
useClass: DocumentLangSetter,
deps: [DOCUMENT, I18nServiceAbstraction],
}),
safeProvider({
provide: CipherEncryptionService,
useClass: DefaultCipherEncryptionService,

View File

@@ -25,7 +25,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
import { UserId } from "@bitwarden/common/types/guid";
import {
CipherService,
EncryptionContext,
@@ -348,7 +348,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(
this.cipher,
[this.collectionId as CollectionId],
this.isAdminConsoleAction,
);

View File

@@ -40,7 +40,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { CipherId, CollectionId, UserId } from "@bitwarden/common/types/guid";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
@@ -521,9 +521,7 @@ export class ViewComponent implements OnDestroy, OnInit {
);
this.showPremiumRequiredTotp =
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
this.collectionId as CollectionId,
]);
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher);
this.canRestoreCipher$ = this.cipherAuthorizationService.canRestoreCipher$(this.cipher);
if (this.cipher.folderId) {

View File

@@ -1,42 +0,0 @@
import { Injectable, inject } from "@angular/core";
import { Observable, combineLatest, from, of } from "rxjs";
import { catchError, map } from "rxjs/operators";
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
import { NudgeStatus, NudgeType } from "../nudges.service";
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
@Injectable({ providedIn: "root" })
export class DownloadBitwardenNudgeService extends DefaultSingleNudgeService {
private vaultProfileService = inject(VaultProfileService);
private logService = inject(LogService);
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
catchError(() => {
this.logService.error("Failed to load profile date:");
// Default to today to ensure the nudge is shown
return of(new Date());
}),
);
return combineLatest([
profileDate$,
this.getNudgeStatus$(nudgeType, userId),
of(Date.now() - THIRTY_DAYS_MS),
]).pipe(
map(([profileCreationDate, status, profileCutoff]) => {
const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff;
return {
hasBadgeDismissed: status.hasBadgeDismissed || profileOlderThanCutoff,
hasSpotlightDismissed: status.hasSpotlightDismissed || profileOlderThanCutoff,
};
}),
);
}
}

View File

@@ -30,10 +30,7 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
this.collectionService.decryptedCollections$,
]).pipe(
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
const filteredCiphers = ciphers?.filter((cipher) => {
return cipher.deletedDate == null;
});
const vaultHasContents = !(filteredCiphers == null || filteredCiphers.length === 0);
const vaultHasContents = !(ciphers == null || ciphers.length === 0);
if (orgs == null || orgs.length === 0) {
return nudgeStatus.hasBadgeDismissed || nudgeStatus.hasSpotlightDismissed
? of(nudgeStatus)
@@ -47,18 +44,22 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
const hasManageCollections = collections.some(
(c) => c.manage && orgIds.has(c.organizationId),
);
// Do not show nudge when
// user has previously dismissed nudge
// OR
// user belongs to an organization and cannot create collections || manage collections
if (
nudgeStatus.hasBadgeDismissed ||
nudgeStatus.hasSpotlightDismissed ||
hasManageCollections ||
canCreateCollections
) {
// When the user has dismissed the nudge or spotlight, return the nudge status directly
if (nudgeStatus.hasBadgeDismissed || nudgeStatus.hasSpotlightDismissed) {
return of(nudgeStatus);
}
// When the user belongs to an organization and cannot create collections or manage collections,
// hide the nudge and spotlight
if (!hasManageCollections && !canCreateCollections) {
return of({
hasSpotlightDismissed: true,
hasBadgeDismissed: true,
});
}
// Otherwise, return the nudge status based on the vault contents
return of({
hasSpotlightDismissed: vaultHasContents,
hasBadgeDismissed: vaultHasContents,

View File

@@ -1,6 +1,6 @@
export * from "./autofill-nudge.service";
export * from "./account-security-nudge.service";
export * from "./has-items-nudge.service";
export * from "./download-bitwarden-nudge.service";
export * from "./empty-vault-nudge.service";
export * from "./vault-settings-import-nudge.service";
export * from "./new-item-nudge.service";
export * from "./new-account-nudge.service";

View File

@@ -12,16 +12,16 @@ import { NudgeStatus, NudgeType } from "../nudges.service";
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
/**
* Custom Nudge Service to use for the Autofill Nudge in the Vault
* Custom Nudge Service to check if account is older than 30 days
*/
@Injectable({
providedIn: "root",
})
export class AutofillNudgeService extends DefaultSingleNudgeService {
export class NewAccountNudgeService extends DefaultSingleNudgeService {
vaultProfileService = inject(VaultProfileService);
logService = inject(LogService);
nudgeStatus$(_: NudgeType, userId: UserId): Observable<NudgeStatus> {
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
catchError(() => {
this.logService.error("Error getting profile creation date");
@@ -32,7 +32,7 @@ export class AutofillNudgeService extends DefaultSingleNudgeService {
return combineLatest([
profileDate$,
this.getNudgeStatus$(NudgeType.AutofillNudge, userId),
this.getNudgeStatus$(nudgeType, userId),
of(Date.now() - THIRTY_DAYS_MS),
]).pipe(
map(([profileCreationDate, status, profileCutoff]) => {

View File

@@ -0,0 +1,74 @@
import { inject, Injectable } from "@angular/core";
import { combineLatest, Observable, of, switchMap } 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
import { CollectionService } from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
import { NudgeStatus, NudgeType } from "../nudges.service";
/**
* Custom Nudge Service for the vault settings import badge.
*/
@Injectable({
providedIn: "root",
})
export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService {
cipherService = inject(CipherService);
organizationService = inject(OrganizationService);
collectionService = inject(CollectionService);
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
return combineLatest([
this.getNudgeStatus$(nudgeType, userId),
this.cipherService.cipherViews$(userId),
this.organizationService.organizations$(userId),
this.collectionService.decryptedCollections$,
]).pipe(
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
const vaultHasMoreThanOneItem = (ciphers?.length ?? 0) > 1;
const { hasBadgeDismissed, hasSpotlightDismissed } = nudgeStatus;
// When the user has no organizations, return the nudge status directly
if ((orgs?.length ?? 0) === 0) {
return hasBadgeDismissed || hasSpotlightDismissed
? of(nudgeStatus)
: of({
hasSpotlightDismissed: vaultHasMoreThanOneItem,
hasBadgeDismissed: vaultHasMoreThanOneItem,
});
}
const orgIds = new Set(orgs.map((org) => org.id));
const canCreateCollections = orgs.some((org) => org.canCreateNewCollections);
const hasManageCollections = collections.some(
(c) => c.manage && orgIds.has(c.organizationId),
);
// When the user has dismissed the nudge or spotlight, return the nudge status directly
if (hasBadgeDismissed || hasSpotlightDismissed) {
return of(nudgeStatus);
}
// When the user belongs to an organization and cannot create collections or manage collections,
// hide the nudge and spotlight
if (!hasManageCollections && !canCreateCollections) {
return of({
hasSpotlightDismissed: true,
hasBadgeDismissed: true,
});
}
// Otherwise, return the nudge status based on the vault contents
return of({
hasSpotlightDismissed: vaultHasMoreThanOneItem,
hasBadgeDismissed: vaultHasMoreThanOneItem,
});
}),
);
}
}

View File

@@ -19,7 +19,8 @@ import { FakeStateProvider, mockAccountServiceWith } from "../../../../../libs/c
import {
HasItemsNudgeService,
EmptyVaultNudgeService,
DownloadBitwardenNudgeService,
NewAccountNudgeService,
VaultSettingsImportNudgeService,
} from "./custom-nudges-services";
import { DefaultSingleNudgeService } from "./default-single-nudge.service";
import { NudgesService, NudgeType } from "./nudges.service";
@@ -33,7 +34,7 @@ describe("Vault Nudges Service", () => {
getFeatureFlag: jest.fn().mockReturnValue(true),
};
const nudgeServices = [EmptyVaultNudgeService, DownloadBitwardenNudgeService];
const nudgeServices = [EmptyVaultNudgeService, NewAccountNudgeService];
beforeEach(async () => {
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
@@ -57,13 +58,17 @@ describe("Vault Nudges Service", () => {
useValue: mock<HasItemsNudgeService>(),
},
{
provide: DownloadBitwardenNudgeService,
useValue: mock<DownloadBitwardenNudgeService>(),
provide: NewAccountNudgeService,
useValue: mock<NewAccountNudgeService>(),
},
{
provide: EmptyVaultNudgeService,
useValue: mock<EmptyVaultNudgeService>(),
},
{
provide: VaultSettingsImportNudgeService,
useValue: mock<VaultSettingsImportNudgeService>(),
},
{
provide: ApiService,
useValue: mock<ApiService>(),

View File

@@ -5,14 +5,15 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { UserKeyDefinition, NUDGES_DISK } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
import {
NewAccountNudgeService,
HasItemsNudgeService,
EmptyVaultNudgeService,
AutofillNudgeService,
DownloadBitwardenNudgeService,
NewItemNudgeService,
AccountSecurityNudgeService,
VaultSettingsImportNudgeService,
} from "./custom-nudges-services";
import { DefaultSingleNudgeService, SingleNudgeService } from "./default-single-nudge.service";
@@ -24,24 +25,23 @@ export type NudgeStatus = {
/**
* Enum to list the various nudge types, to be used by components/badges to show/hide the nudge
*/
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum NudgeType {
/** Nudge to show when user has no items in their vault
* Add future nudges here
*/
EmptyVaultNudge = "empty-vault-nudge",
HasVaultItems = "has-vault-items",
AutofillNudge = "autofill-nudge",
AccountSecurity = "account-security",
DownloadBitwarden = "download-bitwarden",
NewLoginItemStatus = "new-login-item-status",
NewCardItemStatus = "new-card-item-status",
NewIdentityItemStatus = "new-identity-item-status",
NewNoteItemStatus = "new-note-item-status",
NewSshItemStatus = "new-ssh-item-status",
GeneratorNudgeStatus = "generator-nudge-status",
}
export const NudgeType = {
/** Nudge to show when user has no items in their vault */
EmptyVaultNudge: "empty-vault-nudge",
VaultSettingsImportNudge: "vault-settings-import-nudge",
HasVaultItems: "has-vault-items",
AutofillNudge: "autofill-nudge",
AccountSecurity: "account-security",
DownloadBitwarden: "download-bitwarden",
NewLoginItemStatus: "new-login-item-status",
NewCardItemStatus: "new-card-item-status",
NewIdentityItemStatus: "new-identity-item-status",
NewNoteItemStatus: "new-note-item-status",
NewSshItemStatus: "new-ssh-item-status",
GeneratorNudgeStatus: "generator-nudge-status",
} as const;
export type NudgeType = UnionOfValues<typeof NudgeType>;
export const NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
Partial<Record<NudgeType, NudgeStatus>>
@@ -55,6 +55,7 @@ export const NUDGE_DISMISSED_DISK_KEY = new UserKeyDefinition<
})
export class NudgesService {
private newItemNudgeService = inject(NewItemNudgeService);
private newAcctNudgeService = inject(NewAccountNudgeService);
/**
* Custom nudge services to use for specific nudge types
@@ -64,9 +65,11 @@ export class NudgesService {
private customNudgeServices: Partial<Record<NudgeType, SingleNudgeService>> = {
[NudgeType.HasVaultItems]: inject(HasItemsNudgeService),
[NudgeType.EmptyVaultNudge]: inject(EmptyVaultNudgeService),
[NudgeType.VaultSettingsImportNudge]: inject(VaultSettingsImportNudgeService),
[NudgeType.AccountSecurity]: inject(AccountSecurityNudgeService),
[NudgeType.AutofillNudge]: inject(AutofillNudgeService),
[NudgeType.DownloadBitwarden]: inject(DownloadBitwardenNudgeService),
[NudgeType.AutofillNudge]: this.newAcctNudgeService,
[NudgeType.DownloadBitwarden]: this.newAcctNudgeService,
[NudgeType.GeneratorNudgeStatus]: this.newAcctNudgeService,
[NudgeType.NewLoginItemStatus]: this.newItemNudgeService,
[NudgeType.NewCardItemStatus]: this.newItemNudgeService,
[NudgeType.NewIdentityItemStatus]: this.newItemNudgeService,