1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-20041] Marking Task as complete (#14980)

* When saving a cipher, mark any associated security tasks as complete

* fix test error from encryption refactor

* hide security tasks that are associated with deleted ciphers (#15247)

* account for deleted ciphers for atRiskPasswordDescriptions
This commit is contained in:
Nick Krantz
2025-06-27 16:04:51 -05:00
committed by GitHub
parent 7a1bb81c5f
commit 700f54357c
5 changed files with 268 additions and 11 deletions

View File

@@ -1,10 +1,11 @@
import { CommonModule } from "@angular/common";
import { Component, inject } from "@angular/core";
import { RouterModule } from "@angular/router";
import { map, switchMap } from "rxjs";
import { combineLatest, map, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
import { AnchorLinkDirective, CalloutModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -16,10 +17,26 @@ import { I18nPipe } from "@bitwarden/ui-common";
})
export class AtRiskPasswordCalloutComponent {
private taskService = inject(TaskService);
private cipherService = inject(CipherService);
private activeAccount$ = inject(AccountService).activeAccount$.pipe(getUserId);
protected pendingTasks$ = this.activeAccount$.pipe(
switchMap((userId) => this.taskService.pendingTasks$(userId)),
map((tasks) => tasks.filter((t) => t.type === SecurityTaskType.UpdateAtRiskCredential)),
switchMap((userId) =>
combineLatest([
this.taskService.pendingTasks$(userId),
this.cipherService.cipherViews$(userId),
]),
),
map(([tasks, ciphers]) =>
tasks.filter((t) => {
const associatedCipher = ciphers.find((c) => c.id === t.cipherId);
return (
t.type === SecurityTaskType.UpdateAtRiskCredential &&
associatedCipher &&
!associatedCipher.isDeleted
);
}),
),
);
}

View File

@@ -203,6 +203,20 @@ describe("AtRiskPasswordsComponent", () => {
expect(items).toHaveLength(1);
expect(items[0].name).toBe("Item 1");
});
it("should not show tasks associated with deleted ciphers", async () => {
mockCiphers$.next([
{
id: "cipher",
organizationId: "org",
name: "Item 1",
isDeleted: true,
} as CipherView,
]);
const items = await firstValueFrom(component["atRiskItems$"]);
expect(items).toHaveLength(0);
});
});
describe("pageDescription$", () => {
@@ -245,6 +259,19 @@ describe("AtRiskPasswordsComponent", () => {
type: SecurityTaskType.UpdateAtRiskCredential,
} as SecurityTask,
]);
mockCiphers$.next([
{
id: "cipher",
organizationId: "org",
name: "Item 1",
} as CipherView,
{
id: "cipher2",
organizationId: "org2",
name: "Item 2",
} as CipherView,
]);
const description = await firstValueFrom(component["pageDescription$"]);
expect(description).toBe("atRiskPasswordsDescMultiOrgPlural");
});

View File

@@ -155,32 +155,35 @@ export class AtRiskPasswordsComponent implements OnInit {
(t) =>
t.type === SecurityTaskType.UpdateAtRiskCredential &&
t.cipherId != null &&
ciphers[t.cipherId] != null,
ciphers[t.cipherId] != null &&
!ciphers[t.cipherId].isDeleted,
)
.map((t) => ciphers[t.cipherId!]),
),
);
protected pageDescription$ = this.activeUserData$.pipe(
switchMap(({ tasks, userId }) => {
const orgIds = new Set(tasks.map((t) => t.organizationId));
protected pageDescription$ = combineLatest([this.activeUserData$, this.atRiskItems$]).pipe(
switchMap(([{ userId }, atRiskCiphers]) => {
const orgIds = new Set(
atRiskCiphers.filter((c) => c.organizationId).map((c) => c.organizationId),
) as Set<string>;
if (orgIds.size === 1) {
const [orgId] = orgIds;
return this.organizationService.organizations$(userId).pipe(
getOrganizationById(orgId),
map((org) =>
this.i18nService.t(
tasks.length === 1
atRiskCiphers.length === 1
? "atRiskPasswordDescSingleOrg"
: "atRiskPasswordsDescSingleOrgPlural",
org?.name,
tasks.length,
atRiskCiphers.length,
),
),
);
}
return of(this.i18nService.t("atRiskPasswordsDescMultiOrgPlural", tasks.length));
return of(this.i18nService.t("atRiskPasswordsDescMultiOrgPlural", atRiskCiphers.length));
}),
);