diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index e9602ae96c0..b600c6a89d9 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5163,5 +5163,8 @@ }, "updateDesktopAppOrDisableFingerprintDialogMessage": { "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." + }, + "changeAtRiskPassword": { + "message": "Change at-risk password" } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index 209691869f0..b9eae380ca0 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -37,8 +37,16 @@ import { IconButtonModule, SearchModule, ToastService, + CalloutModule, } from "@bitwarden/components"; -import { CipherViewComponent, CopyCipherFieldService } from "@bitwarden/vault"; +import { + ChangeLoginPasswordService, + CipherViewComponent, + CopyCipherFieldService, + DefaultChangeLoginPasswordService, + DefaultTaskService, + TaskService, +} from "@bitwarden/vault"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; @@ -82,10 +90,13 @@ type LoadAction = CipherViewComponent, AsyncActionsModule, PopOutComponent, + CalloutModule, ], providers: [ { provide: ViewPasswordHistoryService, useClass: BrowserViewPasswordHistoryService }, { provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService }, + { provide: TaskService, useClass: DefaultTaskService }, + { provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService }, ], }) export class ViewV2Component { diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 726b5c4b316..de7bc973f25 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3604,5 +3604,8 @@ }, "updateBrowserOrDisableFingerprintDialogMessage": { "message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings." + }, + "changeAtRiskPassword": { + "message": "Change at-risk password" } } diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts index c5114c0be6a..0021d938f82 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts @@ -7,7 +7,9 @@ import { mock } from "jest-mock-extended"; import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; @@ -15,6 +17,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; +import { ChangeLoginPasswordService, TaskService } from "@bitwarden/vault"; import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"; @@ -52,7 +55,34 @@ describe("EmergencyViewDialogComponent", () => { { provide: DIALOG_DATA, useValue: { cipher: mockCipher } }, { provide: AccountService, useValue: accountService }, ], - }).compileComponents(); + }) + .overrideComponent(EmergencyViewDialogComponent, { + remove: { + providers: [ + { provide: PlatformUtilsService, useValue: PlatformUtilsService }, + { + provide: ChangeLoginPasswordService, + useValue: ChangeLoginPasswordService, + }, + { provide: ConfigService, useValue: ConfigService }, + ], + }, + add: { + providers: [ + { + provide: TaskService, + useValue: mock(), + }, + { provide: PlatformUtilsService, useValue: mock() }, + { + provide: ChangeLoginPasswordService, + useValue: mock(), + }, + { provide: ConfigService, useValue: mock() }, + ], + }, + }) + .compileComponents(); fixture = TestBed.createComponent(EmergencyViewDialogComponent); component = fixture.componentInstance; diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts index 68423c50d88..0ca892b40bf 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts @@ -9,7 +9,7 @@ import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; -import { CipherViewComponent } from "@bitwarden/vault"; +import { CipherViewComponent, DefaultTaskService, TaskService } from "@bitwarden/vault"; import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service"; @@ -33,6 +33,7 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService { providers: [ { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, { provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop }, + { provide: TaskService, useClass: DefaultTaskService }, ], }) export class EmergencyViewDialogComponent { diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 59275eb4e7c..881903e79e5 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -36,6 +36,7 @@ import { ToastService, } from "@bitwarden/components"; import { + ChangeLoginPasswordService, CipherAttachmentsComponent, CipherFormComponent, CipherFormConfig, @@ -43,6 +44,9 @@ import { CipherFormModule, CipherViewComponent, DecryptionFailureDialogComponent, + DefaultChangeLoginPasswordService, + DefaultTaskService, + TaskService, } from "@bitwarden/vault"; import { SharedModule } from "../../../shared/shared.module"; @@ -136,6 +140,8 @@ export enum VaultItemDialogResult { { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, { provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }, RoutedVaultFilterService, + { provide: TaskService, useClass: DefaultTaskService }, + { provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService }, ], }) export class VaultItemDialogComponent implements OnInit, OnDestroy { diff --git a/apps/web/src/app/vault/individual-vault/view.component.spec.ts b/apps/web/src/app/vault/individual-vault/view.component.spec.ts index 9bea7f14eb5..d1117258124 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.spec.ts @@ -12,6 +12,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; @@ -21,6 +22,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { ChangeLoginPasswordService, DefaultTaskService, TaskService } from "@bitwarden/vault"; import { ViewCipherDialogParams, ViewCipherDialogResult, ViewComponent } from "./view.component"; @@ -82,7 +84,33 @@ describe("ViewComponent", () => { }, }, ], - }).compileComponents(); + }) + .overrideComponent(ViewComponent, { + remove: { + providers: [ + { provide: TaskService, useClass: DefaultTaskService }, + { provide: PlatformUtilsService, useValue: PlatformUtilsService }, + { + provide: ChangeLoginPasswordService, + useValue: ChangeLoginPasswordService, + }, + ], + }, + add: { + providers: [ + { + provide: TaskService, + useValue: mock(), + }, + { provide: PlatformUtilsService, useValue: mock() }, + { + provide: ChangeLoginPasswordService, + useValue: mock(), + }, + ], + }, + }) + .compileComponents(); fixture = TestBed.createComponent(ViewComponent); component = fixture.componentInstance; diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index baae6f28bf1..7a2cf3bb2f4 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -26,7 +26,7 @@ import { DialogService, ToastService, } from "@bitwarden/components"; -import { CipherViewComponent } from "@bitwarden/vault"; +import { CipherViewComponent, DefaultTaskService, TaskService } from "@bitwarden/vault"; import { SharedModule } from "../../shared/shared.module"; import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service"; @@ -74,6 +74,7 @@ export interface ViewCipherDialogCloseResult { providers: [ { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, + { provide: TaskService, useClass: DefaultTaskService }, ], }) export class ViewComponent implements OnInit { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index da237a8a3ab..3ca78730a9a 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10503,6 +10503,9 @@ "assignedExceedsAvailable": { "message": "Assigned seats exceed available seats." }, + "changeAtRiskPassword": { + "message": "Change at-risk password" + }, "removeUnlockWithPinPolicyTitle": { "message": "Remove Unlock with PIN" }, diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html index f0ebeecdf40..def98b2fe96 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.html +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -3,6 +3,19 @@ {{ "cardExpiredMessage" | i18n }} + + + + + {{ "changeAtRiskPassword" | i18n }} + + + +

- + a?.id)); + activeUserId$ = getUserId(this.accountService.activeAccount$); /** * Optional list of collections the cipher is assigned to. If none are provided, they will be fetched using the @@ -68,12 +75,18 @@ export class CipherViewComponent implements OnChanges, OnDestroy { folder$: Observable | undefined; private destroyed$: Subject = new Subject(); cardIsExpired: boolean = false; + hadPendingChangePasswordTask: boolean = false; + isSecurityTasksEnabled$ = this.configService.getFeatureFlag$(FeatureFlag.SecurityTasks); constructor( private organizationService: OrganizationService, private collectionService: CollectionService, private folderService: FolderService, private accountService: AccountService, + private defaultTaskService: TaskService, + private platformUtilsService: PlatformUtilsService, + private changeLoginPasswordService: ChangeLoginPasswordService, + private configService: ConfigService, ) {} async ngOnChanges() { @@ -137,7 +150,11 @@ export class CipherViewComponent implements OnChanges, OnDestroy { ); } - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const userId = await firstValueFrom(this.activeUserId$); + + if (this.cipher.edit && this.cipher.viewPassword) { + await this.checkPendingChangePasswordTasks(userId); + } if (this.cipher.organizationId && userId) { this.organization$ = this.organizationService @@ -147,15 +164,29 @@ export class CipherViewComponent implements OnChanges, OnDestroy { } if (this.cipher.folderId) { - const activeUserId = await firstValueFrom(this.activeUserId$); - - if (!activeUserId) { - return; - } - this.folder$ = this.folderService - .getDecrypted$(this.cipher.folderId, activeUserId) + .getDecrypted$(this.cipher.folderId, userId) .pipe(takeUntil(this.destroyed$)); } } + + async checkPendingChangePasswordTasks(userId: UserId): Promise { + const tasks = await firstValueFrom(this.defaultTaskService.pendingTasks$(userId)); + + this.hadPendingChangePasswordTask = tasks?.some((task) => { + return ( + task.cipherId === this.cipher?.id && task.type === SecurityTaskType.UpdateAtRiskCredential + ); + }); + } + + launchChangePassword = async () => { + if (this.cipher != null) { + const url = await this.changeLoginPasswordService.getChangePasswordUrl(this.cipher); + if (url == null) { + return; + } + this.platformUtilsService.launchUri(url); + } + }; } diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index 8503604bf7c..6de6fb6848d 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -89,6 +89,12 @@ (click)="logCopyEvent()" > + + + {{ "changeAtRiskPassword" | i18n }} + + +

{ { provide: PlatformUtilsService, useValue: mock() }, { provide: ToastService, useValue: mock() }, { provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } }, + { provide: ConfigService, useValue: mock() }, ], }).compileComponents(); diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts index c95b2040fd2..27d81f32ee6 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { CommonModule, DatePipe } from "@angular/common"; -import { Component, inject, Input } from "@angular/core"; +import { Component, EventEmitter, inject, Input, Output } from "@angular/core"; import { Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -10,6 +10,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -17,6 +18,7 @@ import { SectionComponent, SectionHeaderComponent, TypographyModule, + LinkModule, IconButtonModule, BadgeModule, ColorPasswordModule, @@ -46,10 +48,14 @@ type TotpCodeValues = { ColorPasswordModule, BitTotpCountdownComponent, ReadOnlyCipherCardComponent, + LinkModule, ], }) export class LoginCredentialsViewComponent { @Input() cipher: CipherView; + @Input() activeUserId: UserId; + @Input() hadPendingChangePasswordTask: boolean; + @Output() handleChangePassword = new EventEmitter(); isPremium$: Observable = this.accountService.activeAccount$.pipe( switchMap((account) => @@ -59,6 +65,7 @@ export class LoginCredentialsViewComponent { showPasswordCount: boolean = false; passwordRevealed: boolean = false; totpCodeCopyObj: TotpCodeValues; + private datePipe = inject(DatePipe); constructor( @@ -111,4 +118,8 @@ export class LoginCredentialsViewComponent { this.cipher.organizationId, ); } + + launchChangePasswordEvent(): void { + this.handleChangePassword.emit(); + } } diff --git a/libs/vault/src/services/default-change-login-password.service.ts b/libs/vault/src/services/default-change-login-password.service.ts index 25648318c14..82f272f1ede 100644 --- a/libs/vault/src/services/default-change-login-password.service.ts +++ b/libs/vault/src/services/default-change-login-password.service.ts @@ -35,7 +35,7 @@ export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordSer ]); if (!reliable || wellKnownChangeUrl == null) { - return cipher.login.uri; + return url.origin; } return wellKnownChangeUrl;