1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-21 11:54:02 +00:00
Files
browser/libs/common/src/vault/tasks/services/default-task.service.ts
Jason Ng 0340a881ae [PM-20040] all tasks complete banner (#16033)
* saved WIP

* created at risk password callout service to hold state for callout data. wip

* update at-risk-password-callout to use states for tracking showing and dismissing success banner

* adding spec file for new serive

* update styles to match figma

* minor wording changes

* fix undefined lint error in at risk password callout

* moved service to libs

* added another route guard so when user clears all at risk items they are directed back to the vault page

* small cleanup in at risk callout component and at risk pw guard

* clean up code in at risk password callout component

* update state to memory

* refactor for readability at risk password callout component

* move state update logic from component to at risk password callout service

* fix: bypass router cache on back() in popout

* Revert "fix: bypass router cache on back() in popout"

This reverts commit 23f9312434.

* refactor updatePendingTasksState call

* refactor at risk password callout component and service. remove signals, implement logic through observables. Completed value for tasks utilized.

* clean up completedTasks in at risk password callout service

* add updated state value to prevent banner among diff clients

* move hasInteracted call to page component to avoid looping

* remove excess call in service

* update icon null logic in banner component

* update the callout to use a new banner

* fix classes

* updating banners in at risk password callout component

* anchor tag

* move at-risk callout to above nudges

* update `showCompletedTasksBanner$` variable naming

---------

Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Nick Krantz <nick@livefront.com>
2025-10-22 11:37:58 -05:00

199 lines
6.4 KiB
TypeScript

import {
combineLatest,
filter,
map,
merge,
Observable,
of,
Subscription,
switchMap,
distinctUntilChanged,
} from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { NotificationType } from "@bitwarden/common/enums";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { MessageListener } from "@bitwarden/common/platform/messaging";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
import { StateProvider } from "@bitwarden/common/platform/state";
import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid";
import {
filterOutNullish,
perUserCache$,
} from "@bitwarden/common/vault/utils/observable-utilities";
import { TaskService } from "../abstractions/task.service";
import { SecurityTaskStatus } from "../enums";
import { SecurityTask, SecurityTaskData, SecurityTaskResponse } from "../models";
import { SECURITY_TASKS } from "../state/security-task.state";
const getUnlockedUserIds = map<Record<UserId, AuthenticationStatus>, UserId[]>((authStatuses) =>
Object.entries(authStatuses ?? {})
.filter(([, status]) => status >= AuthenticationStatus.Unlocked)
.map(([userId]) => userId as UserId),
);
export class DefaultTaskService implements TaskService {
constructor(
private stateProvider: StateProvider,
private apiService: ApiService,
private organizationService: OrganizationService,
private authService: AuthService,
private notificationService: ServerNotificationsService,
private messageListener: MessageListener,
) {}
tasksEnabled$ = perUserCache$((userId) => {
return this.organizationService.organizations$(userId).pipe(
map((orgs) => orgs.some((o) => o.useRiskInsights)),
distinctUntilChanged(),
);
});
tasks$ = perUserCache$((userId) => {
return this.tasksEnabled$(userId).pipe(
switchMap((enabled) => {
if (!enabled) {
return of([]);
}
return this.taskState(userId).state$.pipe(
switchMap(async (tasks) => {
if (tasks == null) {
await this.fetchTasksFromApi(userId);
return null;
}
return tasks;
}),
filterOutNullish(),
map((tasks) => tasks.map((t) => new SecurityTask(t))),
);
}),
);
});
pendingTasks$ = perUserCache$((userId) => {
return this.tasks$(userId).pipe(
map((tasks) => tasks.filter((t) => t.status === SecurityTaskStatus.Pending)),
);
});
completedTasks$ = perUserCache$((userId) => {
return this.tasks$(userId).pipe(
map((tasks) => tasks.filter((t) => t.status === SecurityTaskStatus.Completed)),
);
});
async refreshTasks(userId: UserId): Promise<void> {
await this.fetchTasksFromApi(userId);
}
async clear(userId: UserId): Promise<void> {
await this.updateTaskState(userId, []);
}
async markAsComplete(taskId: SecurityTaskId, userId: UserId): Promise<void> {
await this.apiService.send("PATCH", `/tasks/${taskId}/complete`, null, true, false);
await this.refreshTasks(userId);
}
/**
* Fetches the tasks from the API and updates the local state
* @param userId
* @private
*/
private async fetchTasksFromApi(userId: UserId): Promise<void> {
const r = await this.apiService.send("GET", "/tasks", null, true, true);
const response = new ListResponse(r, SecurityTaskResponse);
const taskData = response.data.map((t) => new SecurityTaskData(t));
await this.updateTaskState(userId, taskData);
}
/**
* Returns the local state for the tasks
* @param userId
* @private
*/
private taskState(userId: UserId) {
return this.stateProvider.getUser(userId, SECURITY_TASKS);
}
/**
* Updates the local state with the provided tasks and returns the updated state
* @param userId
* @param tasks
* @private
*/
private updateTaskState(
userId: UserId,
tasks: SecurityTaskData[],
): Promise<SecurityTaskData[] | null> {
return this.taskState(userId).update(() => tasks);
}
/**
* Helper observable that filters the list of unlocked user IDs to only those with tasks enabled.
* @private
*/
private getOnlyTaskEnabledUsers = switchMap<UserId[], Observable<UserId[]>>((unlockedUserIds) => {
if (unlockedUserIds.length === 0) {
return of([]);
}
return combineLatest(
unlockedUserIds.map((userId) =>
this.tasksEnabled$(userId).pipe(map((enabled) => (enabled ? userId : null))),
),
).pipe(map((userIds) => userIds.filter((userId) => userId !== null) as UserId[]));
});
/**
* Helper observable that emits whenever a security task notification is received for a user in the provided list.
* @private
*/
private securityTaskNotifications$(filterByUserIds: UserId[]) {
return this.notificationService.notifications$.pipe(
filter(
([notification, userId]) =>
notification.type === NotificationType.RefreshSecurityTasks &&
filterByUserIds.includes(userId),
),
map(([, userId]) => userId),
);
}
/**
* Helper observable that emits whenever a sync is completed for a user in the provided list.
*/
private syncCompletedMessage$(filterByUserIds: UserId[]) {
return this.messageListener.allMessages$.pipe(
filter((msg) => msg.command === "syncCompleted" && !!msg.successfully && !!msg.userId),
map((msg) => msg.userId as UserId),
filter((userId) => filterByUserIds.includes(userId)),
);
}
/**
* Creates a subscription for pending security task server notifications or completed syncs for unlocked users.
*/
listenForTaskNotifications(): Subscription {
return this.authService.authStatuses$
.pipe(
getUnlockedUserIds,
this.getOnlyTaskEnabledUsers,
filter((allowedUserIds) => allowedUserIds.length > 0),
switchMap((allowedUserIds) =>
merge(
this.securityTaskNotifications$(allowedUserIds),
this.syncCompletedMessage$(allowedUserIds),
),
),
switchMap((userId) => this.refreshTasks(userId)),
)
.subscribe();
}
}