1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-21 20:04:02 +00:00

[PM-30891] - Create My Items On Restore (#18454)

* Added encrypted default collection name to new feature flagged restore user methods/endpoint.

* corrected filter to use null check with imperative code
This commit is contained in:
Jared McCannon
2026-01-29 13:56:35 -06:00
committed by jaasen-livefront
parent 1311b0bf4c
commit 08a98d2c5e
12 changed files with 378 additions and 36 deletions

View File

@@ -1,10 +1,21 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Inject } from "@angular/core";
import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import {
OrganizationUserApiService,
OrganizationUserService,
} from "@bitwarden/admin-console/common";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { getById } from "@bitwarden/common/platform/misc";
import { DIALOG_DATA, DialogService } from "@bitwarden/components";
import { BulkUserDetails } from "./bulk-status.component";
@@ -34,10 +45,15 @@ export class BulkRestoreRevokeComponent {
error: string;
showNoMasterPasswordWarning = false;
nonCompliantMembers: boolean = false;
organization$: Observable<Organization>;
constructor(
protected i18nService: I18nService,
private organizationUserApiService: OrganizationUserApiService,
private organizationUserService: OrganizationUserService,
private accountService: AccountService,
private organizationService: OrganizationService,
private configService: ConfigService,
@Inject(DIALOG_DATA) protected data: BulkRestoreDialogParams,
) {
this.isRevoking = data.isRevoking;
@@ -46,6 +62,18 @@ export class BulkRestoreRevokeComponent {
this.showNoMasterPasswordWarning = this.users.some(
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
);
this.organization$ = accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => organizationService.organizations$(userId)),
getById(this.organizationId),
map((organization) => {
if (organization == null) {
throw new Error("Organization not found");
}
return organization;
}),
);
}
get bulkTitle() {
@@ -83,9 +111,22 @@ export class BulkRestoreRevokeComponent {
userIds,
);
} else {
return await this.organizationUserApiService.restoreManyOrganizationUsers(
this.organizationId,
userIds,
return await firstValueFrom(
combineLatest([
this.configService.getFeatureFlag$(FeatureFlag.DefaultUserCollectionRestore),
this.organization$,
]).pipe(
switchMap(([enabled, organization]) => {
if (enabled) {
return this.organizationUserService.bulkRestoreUsers(organization, userIds);
} else {
return this.organizationUserApiService.restoreManyOrganizationUsers(
this.organizationId,
userIds,
);
}
}),
),
);
}
}

View File

@@ -17,11 +17,9 @@ import {
import {
CollectionAdminService,
OrganizationUserApiService,
OrganizationUserService,
} from "@bitwarden/admin-console/common";
import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import {
OrganizationUserStatusType,
OrganizationUserType,
@@ -36,8 +34,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { getById } from "@bitwarden/common/platform/misc";
import {
DIALOG_DATA,
DialogConfig,
@@ -197,14 +197,19 @@ export class MemberDialogComponent implements OnDestroy {
private toastService: ToastService,
private configService: ConfigService,
private deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
private organizationUserService: OrganizationUserService,
) {
this.organization$ = accountService.activeAccount$.pipe(
switchMap((account) =>
organizationService
.organizations$(account?.id)
.pipe(getOrganizationById(this.params.organizationId))
.pipe(shareReplay({ refCount: true, bufferSize: 1 })),
),
getUserId,
switchMap((userId) => organizationService.organizations$(userId)),
getById(this.params.organizationId),
map((organization) => {
if (organization == null) {
throw new Error("Organization not found");
}
return organization;
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
let userDetails$;
@@ -633,9 +638,26 @@ export class MemberDialogComponent implements OnDestroy {
return;
}
await this.organizationUserApiService.restoreOrganizationUser(
this.params.organizationId,
this.params.organizationUserId,
await firstValueFrom(
combineLatest([
this.configService.getFeatureFlag$(FeatureFlag.DefaultUserCollectionRestore),
this.organization$,
this.editParams$,
]).pipe(
switchMap(([enabled, organization, params]) => {
if (enabled) {
return this.organizationUserService.restoreUser(
organization,
params.organizationUserId,
);
} else {
return this.organizationUserApiService.restoreOrganizationUser(
params.organizationId,
params.organizationUserId,
);
}
}),
),
);
this.toastService.showToast({

View File

@@ -1,6 +1,6 @@
import { TestBed } from "@angular/core/testing";
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";
import { of, throwError } from "rxjs";
import {
OrganizationUserApiService,
@@ -17,6 +17,7 @@ import {
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
@@ -31,6 +32,7 @@ describe("MemberActionsService", () => {
let service: MemberActionsService;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let organizationUserService: MockProxy<OrganizationUserService>;
let configService: MockProxy<ConfigService>;
let organizationMetadataService: MockProxy<OrganizationMetadataServiceAbstraction>;
const organizationId = newGuid() as OrganizationId;
@@ -42,6 +44,7 @@ describe("MemberActionsService", () => {
beforeEach(() => {
organizationUserApiService = mock<OrganizationUserApiService>();
organizationUserService = mock<OrganizationUserService>();
configService = mock<ConfigService>();
organizationMetadataService = mock<OrganizationMetadataServiceAbstraction>();
mockOrganization = {
@@ -65,6 +68,7 @@ describe("MemberActionsService", () => {
MemberActionsService,
{ provide: OrganizationUserApiService, useValue: organizationUserApiService },
{ provide: OrganizationUserService, useValue: organizationUserService },
{ provide: ConfigService, useValue: configService },
{
provide: OrganizationMetadataServiceAbstraction,
useValue: organizationMetadataService,
@@ -174,25 +178,64 @@ describe("MemberActionsService", () => {
});
describe("restoreUser", () => {
it("should successfully restore a user", async () => {
organizationUserApiService.restoreOrganizationUser.mockResolvedValue(undefined);
describe("when feature flag is enabled", () => {
beforeEach(() => {
configService.getFeatureFlag$.mockReturnValue(of(true));
});
const result = await service.restoreUser(mockOrganization, userIdToManage);
it("should call organizationUserService.restoreUser", async () => {
organizationUserService.restoreUser.mockReturnValue(of(undefined));
expect(result).toEqual({ success: true });
expect(organizationUserApiService.restoreOrganizationUser).toHaveBeenCalledWith(
organizationId,
userIdToManage,
);
const result = await service.restoreUser(mockOrganization, userIdToManage);
expect(result).toEqual({ success: true });
expect(organizationUserService.restoreUser).toHaveBeenCalledWith(
mockOrganization,
userIdToManage,
);
expect(organizationUserApiService.restoreOrganizationUser).not.toHaveBeenCalled();
});
it("should handle errors from organizationUserService.restoreUser", async () => {
const errorMessage = "Restore failed";
organizationUserService.restoreUser.mockReturnValue(
throwError(() => new Error(errorMessage)),
);
const result = await service.restoreUser(mockOrganization, userIdToManage);
expect(result).toEqual({ success: false, error: errorMessage });
});
});
it("should handle restore errors", async () => {
const errorMessage = "Restore failed";
organizationUserApiService.restoreOrganizationUser.mockRejectedValue(new Error(errorMessage));
describe("when feature flag is disabled", () => {
beforeEach(() => {
configService.getFeatureFlag$.mockReturnValue(of(false));
});
const result = await service.restoreUser(mockOrganization, userIdToManage);
it("should call organizationUserApiService.restoreOrganizationUser", async () => {
organizationUserApiService.restoreOrganizationUser.mockResolvedValue(undefined);
expect(result).toEqual({ success: false, error: errorMessage });
const result = await service.restoreUser(mockOrganization, userIdToManage);
expect(result).toEqual({ success: true });
expect(organizationUserApiService.restoreOrganizationUser).toHaveBeenCalledWith(
organizationId,
userIdToManage,
);
expect(organizationUserService.restoreUser).not.toHaveBeenCalled();
});
it("should handle errors", async () => {
const errorMessage = "Restore failed";
organizationUserApiService.restoreOrganizationUser.mockRejectedValue(
new Error(errorMessage),
);
const result = await service.restoreUser(mockOrganization, userIdToManage);
expect(result).toEqual({ success: false, error: errorMessage });
});
});
});

View File

@@ -1,5 +1,5 @@
import { inject, Injectable, signal } from "@angular/core";
import { lastValueFrom, firstValueFrom } from "rxjs";
import { lastValueFrom, firstValueFrom, switchMap } from "rxjs";
import {
OrganizationUserApiService,
@@ -10,13 +10,15 @@ import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import {
OrganizationUserType,
OrganizationUserStatusType,
OrganizationUserType,
} from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { assertNonNullish } from "@bitwarden/common/auth/utils";
import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DialogService } from "@bitwarden/components";
@@ -43,6 +45,7 @@ export interface BulkActionResult {
export class MemberActionsService {
private organizationUserApiService = inject(OrganizationUserApiService);
private organizationUserService = inject(OrganizationUserService);
private configService = inject(ConfigService);
private organizationMetadataService = inject(OrganizationMetadataServiceAbstraction);
private apiService = inject(ApiService);
private dialogService = inject(DialogService);
@@ -116,7 +119,21 @@ export class MemberActionsService {
async restoreUser(organization: Organization, userId: string): Promise<MemberActionResult> {
this.startProcessing();
try {
await this.organizationUserApiService.restoreOrganizationUser(organization.id, userId);
await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.DefaultUserCollectionRestore).pipe(
switchMap((enabled) => {
if (enabled) {
return this.organizationUserService.restoreUser(organization, userId);
} else {
return this.organizationUserApiService.restoreOrganizationUser(
organization.id,
userId,
);
}
}),
),
);
this.organizationMetadataService.refreshMetadataCache();
return { success: true };
} catch (error) {