1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-28 18:43:26 +00:00

Merge branch 'PM-19741' into 202505-notifications-refactor

This commit is contained in:
Miles Blackwood
2025-06-05 11:54:06 -04:00
2059 changed files with 74443 additions and 47217 deletions

View File

@@ -50,11 +50,15 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
}
get showBulkConfirmUsers(): boolean {
return this.dataSource.acceptedUserCount > 0;
return this.dataSource
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Accepted);
}
get showBulkReinviteUsers(): boolean {
return this.dataSource.invitedUserCount > 0;
return this.dataSource
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Invited);
}
abstract userType: typeof OrganizationUserType | typeof ProviderUserType;

View File

@@ -43,6 +43,8 @@ export interface BulkCollectionsDialogParams {
collections: CollectionView[];
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum BulkCollectionsDialogResult {
Saved = "saved",
Canceled = "canceled",

View File

@@ -10,6 +10,7 @@ import { GroupView } from "../../core";
@Component({
selector: "app-group-badge",
templateUrl: "group-name-badge.component.html",
standalone: false,
})
export class GroupNameBadgeComponent implements OnChanges {
@Input() selectedGroups: SelectionReadOnlyRequest[];

View File

@@ -37,6 +37,31 @@ export function getNestedCollectionTree(
return nodes;
}
export function getNestedCollectionTree_vNext(
collections: (CollectionView | CollectionAdminView)[],
): TreeNode<CollectionView | CollectionAdminView>[] {
if (!collections) {
return [];
}
// Collections need to be cloned because ServiceUtils.nestedTraverse actively
// modifies the names of collections.
// These changes risk affecting collections store in StateService.
const clonedCollections = collections
.sort((a, b) => a.name.localeCompare(b.name))
.map(cloneCollection);
const nodes: TreeNode<CollectionView | CollectionAdminView>[] = [];
clonedCollections.forEach((collection) => {
const parts =
collection.name != null
? collection.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter)
: [];
ServiceUtils.nestedTraverse_vNext(nodes, 0, parts, collection, null, NestingDelimiter);
});
return nodes;
}
export function getFlatCollectionTree(
nodes: TreeNode<CollectionAdminView>[],
): CollectionAdminView[];

View File

@@ -26,6 +26,7 @@ import { CollectionFilter } from "../../../../vault/individual-vault/vault-filte
selector: "app-organization-vault-filter",
templateUrl:
"../../../../vault/individual-vault/vault-filter/components/vault-filter.component.html",
standalone: false,
})
export class VaultFilterComponent
extends BaseVaultFilterComponent
@@ -105,14 +106,14 @@ export class VaultFilterComponent
id: "AllCollections",
name: "collections",
type: "all",
icon: "bwi-collection",
icon: "bwi-collection-shared",
},
[
{
id: "AllCollections",
name: "Collections",
type: "all",
icon: "bwi-collection",
icon: "bwi-collection-shared",
},
],
),

View File

@@ -14,7 +14,7 @@
<ng-container>
<bit-breadcrumb
*ngFor="let collection of collections"
icon="bwi-collection"
icon="bwi-collection-shared"
[route]="[]"
[queryParams]="{ collectionId: collection.id }"
queryParamsHandling="merge"
@@ -35,7 +35,6 @@
[bitMenuTriggerFor]="editCollectionMenu"
size="small"
type="button"
aria-haspopup="true"
></button>
<bit-menu #editCollectionMenu>
<ng-container *ngIf="canEditCollection">
@@ -115,7 +114,7 @@
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
<i class="bwi bwi-plus" aria-hidden="true"></i>
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
@@ -140,7 +139,7 @@
<ng-container *ngIf="canCreateCollection">
<bit-menu-divider *ngIf="canCreateCipher"></bit-menu-divider>
<button type="button" bitMenuItem (click)="addCollection()">
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
<i class="bwi bwi-fw bwi-collection-shared" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</ng-container>

View File

@@ -117,7 +117,7 @@ export class VaultHeaderComponent {
}
get icon() {
return this.filter.collectionId !== undefined ? "bwi-collection" : "";
return this.filter.collectionId !== undefined ? "bwi-collection-shared" : "";
}
protected get showBreadcrumbs() {

View File

@@ -1,4 +1,15 @@
<ng-container *ngIf="freeTrial$ | async as freeTrial">
<app-free-trial-warning
*ngIf="useOrganizationWarningsService$ | async"
[organization]="organization"
(clicked)="navigateToPaymentMethod()"
>
</app-free-trial-warning>
<app-reseller-renewal-warning
*ngIf="useOrganizationWarningsService$ | async"
[organization]="organization"
>
</app-reseller-renewal-warning>
<ng-container *ngIf="freeTrialWhenWarningsServiceDisabled$ | async as freeTrial">
<bit-banner
id="free-trial-banner"
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
@@ -19,7 +30,7 @@
</a>
</bit-banner>
</ng-container>
<ng-container *ngIf="resellerWarning$ | async as resellerWarning">
<ng-container *ngIf="resellerWarningWhenWarningsServiceDisabled$ | async as resellerWarning">
<bit-banner
id="reseller-warning-banner"
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
@@ -142,6 +153,3 @@
</div>
</div>
</div>
<ng-template #attachments></ng-template>
<ng-template #cipherAddEdit></ng-template>
<ng-template #collectionsModal></ng-template>

View File

@@ -13,6 +13,7 @@ import {
Subject,
} from "rxjs";
import {
catchError,
concatMap,
debounceTime,
distinctUntilChanged,
@@ -23,7 +24,6 @@ import {
switchMap,
takeUntil,
tap,
catchError,
} from "rxjs/operators";
import {
@@ -44,6 +44,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -61,8 +62,8 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import {
DialogRef,
BannerModule,
DialogRef,
DialogService,
Icons,
NoItemsModule,
@@ -77,6 +78,8 @@ import {
DecryptionFailureDialogComponent,
PasswordRepromptService,
} from "@bitwarden/vault";
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/services/organization-warnings.service";
import { ResellerRenewalWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/reseller-renewal-warning.component";
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
import {
@@ -85,6 +88,7 @@ import {
} from "../../../billing/services/reseller-warning.service";
import { TrialFlowService } from "../../../billing/services/trial-flow.service";
import { FreeTrial } from "../../../billing/types/free-trial";
import { FreeTrialWarningComponent } from "../../../billing/warnings/free-trial-warning.component";
import { SharedModule } from "../../../shared";
import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections";
import {
@@ -121,13 +125,19 @@ import {
BulkCollectionsDialogResult,
} from "./bulk-collections-dialog";
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
import { getNestedCollectionTree, getFlatCollectionTree } from "./utils";
import {
getNestedCollectionTree,
getFlatCollectionTree,
getNestedCollectionTree_vNext,
} from "./utils";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
const BroadcasterSubscriptionId = "OrgVaultComponent";
const SearchTextDebounceInterval = 200;
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum AddAccessStatusType {
All = 0,
AddAccess = 1,
@@ -145,6 +155,8 @@ enum AddAccessStatusType {
SharedModule,
BannerModule,
NoItemsModule,
FreeTrialWarningComponent,
ResellerRenewalWarningComponent,
],
providers: [
RoutedVaultFilterService,
@@ -174,8 +186,9 @@ export class VaultComponent implements OnInit, OnDestroy {
protected showCollectionAccessRestricted: boolean;
private hasSubscription$ = new BehaviorSubject<boolean>(false);
protected currentSearchText$: Observable<string>;
protected freeTrial$: Observable<FreeTrial>;
protected resellerWarning$: Observable<ResellerWarning | null>;
protected useOrganizationWarningsService$: Observable<boolean>;
protected freeTrialWhenWarningsServiceDisabled$: Observable<FreeTrial>;
protected resellerWarningWhenWarningsServiceDisabled$: Observable<ResellerWarning | null>;
protected prevCipherId: string | null = null;
protected userId: UserId;
/**
@@ -255,6 +268,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private resellerWarningService: ResellerWarningService,
private accountService: AccountService,
private billingNotificationService: BillingNotificationService,
private organizationWarningsService: OrganizationWarningsService,
) {}
async ngOnInit() {
@@ -352,8 +366,7 @@ export class VaultComponent implements OnInit, OnDestroy {
if (this.organization.canEditAllCiphers) {
return collections;
}
// The user is only allowed to add/edit items to assigned collections that are not readonly
return collections.filter((c) => c.assigned && !c.readOnly);
return collections.filter((c) => c.assigned);
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
@@ -411,9 +424,16 @@ export class VaultComponent implements OnInit, OnDestroy {
}),
);
const nestedCollections$ = allCollections$.pipe(
map((collections) => getNestedCollectionTree(collections)),
shareReplay({ refCount: true, bufferSize: 1 }),
const nestedCollections$ = combineLatest([
allCollections$,
this.configService.getFeatureFlag$(FeatureFlag.OptimizeNestedTraverseTypescript),
]).pipe(
map(
([collections, shouldOptimize]) =>
(shouldOptimize
? getNestedCollectionTree_vNext(collections)
: getNestedCollectionTree(collections)) as TreeNode<CollectionAdminView>[],
),
);
const collections$ = combineLatest([
@@ -628,9 +648,23 @@ export class VaultComponent implements OnInit, OnDestroy {
)
.subscribe();
this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe();
// Billing Warnings
this.useOrganizationWarningsService$ = this.configService.getFeatureFlag$(
FeatureFlag.UseOrganizationWarningsService,
);
this.freeTrial$ = combineLatest([
this.useOrganizationWarningsService$
.pipe(
switchMap((enabled) =>
enabled
? this.organizationWarningsService.showInactiveSubscriptionDialog$(this.organization)
: this.unpaidSubscriptionDialog$,
),
takeUntil(this.destroy$),
)
.subscribe();
const freeTrial$ = combineLatest([
organization$,
this.hasSubscription$.pipe(filter((hasSubscription) => hasSubscription !== null)),
]).pipe(
@@ -655,7 +689,12 @@ export class VaultComponent implements OnInit, OnDestroy {
filter((result) => result !== null),
);
this.resellerWarning$ = organization$.pipe(
this.freeTrialWhenWarningsServiceDisabled$ = this.useOrganizationWarningsService$.pipe(
filter((enabled) => !enabled),
switchMap(() => freeTrial$),
);
const resellerWarning$ = organization$.pipe(
filter((org) => org.isOwner),
switchMap((org) =>
from(this.billingApiService.getOrganizationBillingMetadata(org.id)).pipe(
@@ -665,6 +704,12 @@ export class VaultComponent implements OnInit, OnDestroy {
map(({ org, metadata }) => this.resellerWarningService.getWarning(org, metadata)),
);
this.resellerWarningWhenWarningsServiceDisabled$ = this.useOrganizationWarningsService$.pipe(
filter((enabled) => !enabled),
switchMap(() => resellerWarning$),
);
// End Billing Warnings
firstSetup$
.pipe(
switchMap(() => this.refresh$),
@@ -820,6 +865,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const dialogRef = AttachmentsV2Component.open(this.dialogService, {
cipherId: cipher.id as CipherId,
organizationId: cipher.organizationId as OrganizationId,
admin: true,
});
const result = await firstValueFrom(dialogRef.closed);

View File

@@ -9,6 +9,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
@Component({
selector: "app-org-info",
templateUrl: "organization-information.component.html",
standalone: false,
})
export class OrganizationInformationComponent implements OnInit {
@Input() nameOnly = false;

View File

@@ -21,16 +21,19 @@ import { isEnterpriseOrgGuard } from "./is-enterprise-org.guard";
@Component({
template: "<h1>This is the home screen!</h1>",
standalone: false,
})
export class HomescreenComponent {}
@Component({
template: "<h1>This component can only be accessed by a enterprise organization!</h1>",
standalone: false,
})
export class IsEnterpriseOrganizationComponent {}
@Component({
template: "<h1>This is the organization upgrade screen!</h1>",
standalone: false,
})
export class OrganizationUpgradeScreenComponent {}

View File

@@ -20,16 +20,19 @@ import { isPaidOrgGuard } from "./is-paid-org.guard";
@Component({
template: "<h1>This is the home screen!</h1>",
standalone: false,
})
export class HomescreenComponent {}
@Component({
template: "<h1>This component can only be accessed by a paid organization!</h1>",
standalone: false,
})
export class PaidOrganizationOnlyComponent {}
@Component({
template: "<h1>This is the organization upgrade screen!</h1>",
standalone: false,
})
export class OrganizationUpgradeScreenComponent {}

View File

@@ -19,16 +19,19 @@ import { organizationRedirectGuard } from "./org-redirect.guard";
@Component({
template: "<h1>This is the home screen!</h1>",
standalone: false,
})
export class HomescreenComponent {}
@Component({
template: "<h1>This is the admin console!</h1>",
standalone: false,
})
export class AdminConsoleComponent {}
@Component({
template: "<h1> This is a subroute of the admin console!</h1>",
standalone: false,
})
export class AdminConsoleSubrouteComponent {}

View File

@@ -6,6 +6,7 @@
icon="bwi-filter"
*ngIf="organization.useRiskInsights"
[text]="'accessIntelligence' | i18n"
route="access-intelligence"
>
<bit-nav-item
[text]="'riskInsights' | i18n"
@@ -13,7 +14,7 @@
></bit-nav-item>
</bit-nav-group>
<bit-nav-item
icon="bwi-collection"
icon="bwi-collection-shared"
[text]="'collections' | i18n"
route="vault"
*ngIf="canShowVaultTab(organization)"
@@ -115,7 +116,7 @@
*ngIf="canAccessExport$ | async"
></bit-nav-item>
<bit-nav-item
[text]="domainVerificationNavigationTextKey | i18n"
[text]="'claimedDomains' | i18n"
route="settings/domain-verification"
*ngIf="organization?.canManageDomainVerification"
></bit-nav-item>
@@ -138,22 +139,6 @@
</app-side-nav>
<ng-container *ngIf="organization$ | async as organization">
<bit-banner
*ngIf="showAccountDeprovisioningBanner$ | async"
(onClose)="bannerService.hideBanner(organization)"
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
>
{{ "accountDeprovisioningNotification" | i18n }}
<a
href="https://bitwarden.com/help/claimed-accounts"
bitLink
linkType="contrast"
target="_blank"
rel="noreferrer"
>
{{ "learnMore" | i18n }}
</a>
</bit-banner>
<bit-banner
*ngIf="organization.isProviderUser"
[showClose]="false"

View File

@@ -23,7 +23,6 @@ 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 { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -35,8 +34,6 @@ import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher
import { WebLayoutModule } from "../../../layouts/web-layout.module";
import { AdminConsoleLogo } from "../../icons/admin-console-logo";
import { AccountDeprovisioningBannerService } from "./services/account-deprovisioning-banner.service";
@Component({
selector: "app-organization-layout",
templateUrl: "organization-layout.component.html",
@@ -55,7 +52,6 @@ export class OrganizationLayoutComponent implements OnInit {
protected readonly logo = AdminConsoleLogo;
protected orgFilter = (org: Organization) => canAccessOrgAdmin(org);
protected domainVerificationNavigationTextKey: string;
protected integrationPageEnabled$: Observable<boolean>;
@@ -66,7 +62,6 @@ export class OrganizationLayoutComponent implements OnInit {
organizationIsUnmanaged$: Observable<boolean>;
enterpriseOrganization$: Observable<boolean>;
showAccountDeprovisioningBanner$: Observable<boolean>;
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;
protected showSponsoredFamiliesDropdown$: Observable<boolean>;
protected canShowPoliciesTab$: Observable<boolean>;
@@ -78,7 +73,6 @@ export class OrganizationLayoutComponent implements OnInit {
private configService: ConfigService,
private policyService: PolicyService,
private providerService: ProviderService,
protected bannerService: AccountDeprovisioningBannerService,
private accountService: AccountService,
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
@@ -101,20 +95,6 @@ export class OrganizationLayoutComponent implements OnInit {
this.showSponsoredFamiliesDropdown$ =
this.freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(this.organization$);
this.showAccountDeprovisioningBanner$ = combineLatest([
this.bannerService.showBanner$,
this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioningBanner),
this.organization$,
]).pipe(
map(
([dismissedOrgs, featureFlagEnabled, organization]) =>
organization.productTierType === ProductTierType.Enterprise &&
organization.isAdmin &&
!dismissedOrgs?.includes(organization.id) &&
featureFlagEnabled,
),
);
this.canAccessExport$ = this.organization$.pipe(map((org) => org.canAccessExport));
this.showPaymentAndHistory$ = this.organization$.pipe(
@@ -146,12 +126,6 @@ export class OrganizationLayoutComponent implements OnInit {
this.integrationPageEnabled$ = this.organization$.pipe(map((org) => org.canAccessIntegrations));
this.domainVerificationNavigationTextKey = (await this.configService.getFeatureFlag(
FeatureFlag.AccountDeprovisioning,
))
? "claimedDomains"
: "domainVerification";
this.canShowPoliciesTab$ = this.organization$.pipe(
switchMap((organization) =>
this.organizationBillingService

View File

@@ -1,75 +0,0 @@
import { firstValueFrom } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { AccountDeprovisioningBannerService } from "./account-deprovisioning-banner.service";
describe("Account Deprovisioning Banner Service", () => {
const userId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
let stateProvider: FakeStateProvider;
let bannerService: AccountDeprovisioningBannerService;
beforeEach(async () => {
accountService = mockAccountServiceWith(userId);
stateProvider = new FakeStateProvider(accountService);
bannerService = new AccountDeprovisioningBannerService(stateProvider);
});
it("updates state with single org", async () => {
const fakeOrg = new Organization();
fakeOrg.id = "123";
await bannerService.hideBanner(fakeOrg);
const state = await firstValueFrom(bannerService.showBanner$);
expect(state).toEqual([fakeOrg.id]);
});
it("updates state with multiple orgs", async () => {
const fakeOrg1 = new Organization();
fakeOrg1.id = "123";
const fakeOrg2 = new Organization();
fakeOrg2.id = "234";
const fakeOrg3 = new Organization();
fakeOrg3.id = "987";
await bannerService.hideBanner(fakeOrg1);
await bannerService.hideBanner(fakeOrg2);
await bannerService.hideBanner(fakeOrg3);
const state = await firstValueFrom(bannerService.showBanner$);
expect(state).toContain(fakeOrg1.id);
expect(state).toContain(fakeOrg2.id);
expect(state).toContain(fakeOrg3.id);
});
it("does not add the same org id multiple times", async () => {
const fakeOrg = new Organization();
fakeOrg.id = "123";
await bannerService.hideBanner(fakeOrg);
await bannerService.hideBanner(fakeOrg);
const state = await firstValueFrom(bannerService.showBanner$);
expect(state).toEqual([fakeOrg.id]);
});
it("does not add null to the state", async () => {
await bannerService.hideBanner(null as unknown as Organization);
await bannerService.hideBanner(undefined as unknown as Organization);
const state = await firstValueFrom(bannerService.showBanner$);
expect(state).toBeNull();
});
});

View File

@@ -1,40 +0,0 @@
import { Injectable } from "@angular/core";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import {
ACCOUNT_DEPROVISIONING_BANNER_DISK,
StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
export const SHOW_BANNER_KEY = new UserKeyDefinition<string[]>(
ACCOUNT_DEPROVISIONING_BANNER_DISK,
"accountDeprovisioningBanner",
{
deserializer: (b) => b,
clearOn: [],
},
);
@Injectable({ providedIn: "root" })
export class AccountDeprovisioningBannerService {
private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY);
showBanner$ = this._showBanner.state$;
constructor(private stateProvider: StateProvider) {}
async hideBanner(organization: Organization) {
await this._showBanner.update((state) => {
if (!organization) {
return state;
}
if (!state) {
return [organization.id];
} else if (!state.includes(organization.id)) {
return [...state, organization.id];
}
return state;
});
}
}

View File

@@ -48,6 +48,7 @@ const EVENT_SYSTEM_USER_TO_TRANSLATION: Record<EventSystemUser, string> = {
@Component({
selector: "app-org-events",
templateUrl: "events.component.html",
standalone: false,
})
export class EventsComponent extends BaseEventsComponent implements OnInit, OnDestroy {
exportFileName = "org-events";

View File

@@ -23,7 +23,7 @@
<input bitInput appAutofocus type="text" formControlName="name" />
<bit-hint>{{ "characterMaximum" | i18n: 100 }}</bit-hint>
</bit-form-field>
<bit-form-field *ngIf="isExternalIdVisible$ | async">
<bit-form-field *ngIf="isExternalIdVisible">
<bit-label>{{ "externalId" | i18n }}</bit-label>
<input bitInput type="text" formControlName="externalId" />
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>

View File

@@ -28,7 +28,6 @@ import {
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -58,6 +57,8 @@ import { AddEditGroupDetail } from "./../core/views/add-edit-group-detail";
/**
* Indices for the available tabs in the dialog
*/
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum GroupAddEditTabType {
Info = 0,
Members = 1,
@@ -82,6 +83,8 @@ export interface GroupAddEditDialogParams {
initialTab?: GroupAddEditTabType;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum GroupAddEditDialogResultType {
Saved = "saved",
Canceled = "canceled",
@@ -106,6 +109,7 @@ export const openGroupAddEditDialog = (
@Component({
selector: "app-group-add-edit",
templateUrl: "group-add-edit.component.html",
standalone: false,
})
export class GroupAddEditComponent implements OnInit, OnDestroy {
private organization$ = this.accountService.activeAccount$.pipe(
@@ -142,6 +146,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
return this.params.organizationId;
}
protected get isExternalIdVisible(): boolean {
return !!this.groupForm.get("externalId")?.value;
}
protected get editMode(): boolean {
return this.groupId != null;
}
@@ -222,10 +230,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
this.groupDetails$,
]).pipe(map(([allowAdminAccess, groupDetails]) => !allowAdminAccess && groupDetails != null));
protected isExternalIdVisible$ = this.configService
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
.pipe(map((isEnabled) => !isEnabled || !!this.groupForm.get("externalId")?.value));
constructor(
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
private dialogRef: DialogRef<GroupAddEditDialogResultType>,

View File

@@ -93,7 +93,8 @@
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "members" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(g, ModalTabType.Collections)">
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
<i aria-hidden="true" class="bwi bwi-collection-shared"></i>
{{ "collections" | i18n }}
</button>
<button type="button" bitMenuItem (click)="delete(g)">
<span class="tw-text-danger"

View File

@@ -75,6 +75,7 @@ const groupsFilter = (filter: string) => {
@Component({
templateUrl: "groups.component.html",
standalone: false,
})
export class GroupsComponent {
loading = true;

View File

@@ -1,23 +0,0 @@
<form [formGroup]="confirmForm" [bitSubmit]="submit">
<bit-dialog
dialogSize="large"
[loading]="loading"
[title]="'trustOrganization' | i18n"
[subtitle]="params.name"
>
<ng-container bitDialogContent>
<bit-callout type="warning">{{ "orgTrustWarning" | i18n }}</bit-callout>
<p bitTypography="body1">
{{ "fingerprintPhrase" | i18n }} <code>{{ fingerprint }}</code>
</p>
</ng-container>
<ng-container bitDialogFooter>
<button type="submit" buttonType="primary" bitButton bitFormButton>
<span>{{ "trust" | i18n }}</span>
</button>
<button bitButton bitFormButton buttonType="secondary" type="button" bitDialogClose>
{{ "doNotTrust" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -1,69 +0,0 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, OnInit, Inject } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
type OrganizationTrustDialogData = {
/** display name of the organization */
name: string;
/** identifies the organization */
orgId: string;
/** org public key */
publicKey: Uint8Array;
};
@Component({
selector: "organization-trust",
templateUrl: "organization-trust.component.html",
})
export class OrganizationTrustComponent implements OnInit {
loading = true;
fingerprint: string = "";
confirmForm = this.formBuilder.group({});
constructor(
@Inject(DIALOG_DATA) protected params: OrganizationTrustDialogData,
private formBuilder: FormBuilder,
private keyService: KeyService,
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
private logService: LogService,
private dialogRef: DialogRef<boolean>,
) {}
async ngOnInit() {
try {
const fingerprint = await this.keyService.getFingerprint(
this.params.orgId,
this.params.publicKey,
);
if (fingerprint != null) {
this.fingerprint = fingerprint.join("-");
}
} catch (e) {
this.logService.error(e);
}
this.loading = false;
}
submit = async () => {
if (this.loading) {
return;
}
this.dialogRef.close(true);
};
/**
* Strongly typed helper to open a OrganizationTrustComponent
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param data The data to pass to the dialog
*/
static open(dialogService: DialogService, data: OrganizationTrustDialogData) {
return dialogService.open<boolean, OrganizationTrustDialogData>(OrganizationTrustComponent, {
data,
});
}
}

View File

@@ -18,6 +18,7 @@ export type UserConfirmDialogData = {
@Component({
selector: "app-user-confirm",
templateUrl: "user-confirm.component.html",
standalone: false,
})
export class UserConfirmComponent implements OnInit {
name: string;

View File

@@ -33,6 +33,7 @@ type BulkConfirmDialogParams = {
@Component({
templateUrl: "bulk-confirm-dialog.component.html",
standalone: false,
})
export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
organizationId: string;

View File

@@ -1,12 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Inject } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/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 { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
@@ -21,6 +18,7 @@ type BulkDeleteDialogParams = {
@Component({
templateUrl: "bulk-delete-dialog.component.html",
standalone: false,
})
export class BulkDeleteDialogComponent {
organizationId: string;
@@ -35,7 +33,6 @@ export class BulkDeleteDialogComponent {
@Inject(DIALOG_DATA) protected dialogParams: BulkDeleteDialogParams,
protected i18nService: I18nService,
private organizationUserApiService: OrganizationUserApiService,
private configService: ConfigService,
private deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
) {
this.organizationId = dialogParams.organizationId;
@@ -43,11 +40,7 @@ export class BulkDeleteDialogComponent {
}
async submit() {
if (
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioning))
) {
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organizationId);
}
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organizationId);
try {
this.loading = true;

View File

@@ -22,6 +22,7 @@ export type BulkEnableSecretsManagerDialogData = {
@Component({
templateUrl: `bulk-enable-sm-dialog.component.html`,
standalone: false,
})
export class BulkEnableSecretsManagerDialogComponent implements OnInit {
protected dataSource = new TableDataSource<OrganizationUserView>();

View File

@@ -21,6 +21,7 @@ type BulkRemoveDialogParams = {
@Component({
templateUrl: "bulk-remove-dialog.component.html",
standalone: false,
})
export class BulkRemoveDialogComponent extends BaseBulkRemoveComponent {
organizationId: string;

View File

@@ -1,12 +1,6 @@
<bit-dialog
dialogSize="large"
*ngIf="{ enabled: accountDeprovisioningEnabled$ | async } as accountDeprovisioning"
>
<bit-dialog dialogSize="large">
<ng-container bitDialogTitle>
<span *ngIf="accountDeprovisioning.enabled; else nonMemberTitle">{{ bulkMemberTitle }}</span>
<ng-template #nonMemberTitle>
{{ bulkTitle }}
</ng-template>
<span>{{ bulkTitle }}</span>
</ng-container>
<div bitDialogContent>
@@ -20,7 +14,7 @@
<bit-callout
type="danger"
*ngIf="nonCompliantMembers && accountDeprovisioning.enabled"
*ngIf="nonCompliantMembers"
title="{{ 'nonCompliantMembersTitle' | i18n }}"
>
{{ "nonCompliantMembersError" | i18n }}
@@ -50,7 +44,7 @@
<bit-table>
<ng-container header>
<tr>
<th bitCell>{{ (accountDeprovisioning.enabled ? "member" : "user") | i18n }}</th>
<th bitCell>{{ "member" | i18n }}</th>
<th bitCell class="tw-w-1/2" *ngIf="this.showNoMasterPasswordWarning">
{{ "details" | i18n }}
</th>
@@ -82,7 +76,7 @@
<ng-container header>
<tr>
<th bitCell class="tw-w-1/2">
{{ (accountDeprovisioning.enabled ? "member" : "user") | i18n }}
{{ "member" | i18n }}
</th>
<th bitCell class="tw-w-1/2">{{ "status" | i18n }}</th>
</tr>
@@ -113,7 +107,7 @@
[bitAction]="submit"
buttonType="primary"
>
{{ accountDeprovisioning.enabled ? bulkMemberTitle : bulkTitle }}
{{ bulkTitle }}
</button>
<button type="button" bitButton buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}

View File

@@ -1,12 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Inject } from "@angular/core";
import { Observable } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/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 { DIALOG_DATA, DialogService } from "@bitwarden/components";
@@ -21,6 +18,7 @@ type BulkRestoreDialogParams = {
@Component({
selector: "app-bulk-restore-revoke",
templateUrl: "bulk-restore-revoke.component.html",
standalone: false,
})
export class BulkRestoreRevokeComponent {
isRevoking: boolean;
@@ -34,12 +32,10 @@ export class BulkRestoreRevokeComponent {
error: string;
showNoMasterPasswordWarning = false;
nonCompliantMembers: boolean = false;
accountDeprovisioningEnabled$: Observable<boolean>;
constructor(
protected i18nService: I18nService,
private organizationUserApiService: OrganizationUserApiService,
private configService: ConfigService,
@Inject(DIALOG_DATA) protected data: BulkRestoreDialogParams,
) {
this.isRevoking = data.isRevoking;
@@ -48,17 +44,9 @@ export class BulkRestoreRevokeComponent {
this.showNoMasterPasswordWarning = this.users.some(
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
);
this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
}
get bulkTitle() {
const titleKey = this.isRevoking ? "revokeUsers" : "restoreUsers";
return this.i18nService.t(titleKey);
}
get bulkMemberTitle() {
const titleKey = this.isRevoking ? "revokeMembers" : "restoreMembers";
return this.i18nService.t(titleKey);
}

View File

@@ -41,6 +41,7 @@ type BulkStatusDialogData = {
@Component({
selector: "app-bulk-status",
templateUrl: "bulk-status.component.html",
standalone: false,
})
export class BulkStatusComponent implements OnInit {
users: BulkStatusEntry[];

View File

@@ -177,13 +177,13 @@
</bit-label>
</bit-form-control>
</ng-container>
<bit-form-field *ngIf="isExternalIdVisible$ | async">
<bit-form-field *ngIf="isExternalIdVisible">
<bit-label>{{ "externalId" | i18n }}</bit-label>
<input bitInput type="text" formControlName="externalId" />
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
</bit-form-field>
<bit-form-field *ngIf="isSsoExternalIdVisible$ | async">
<bit-form-field *ngIf="isSsoExternalIdVisible">
<bit-label>{{ "ssoExternalId" | i18n }}</bit-label>
<input bitInput type="text" formControlName="ssoExternalId" />
<bit-hint>{{ "ssoExternalIdDesc" | i18n }}</bit-hint>
@@ -275,11 +275,7 @@
{{ "revoke" | i18n }}
</button>
<button
*ngIf="
this.editMode &&
(!(accountDeprovisioningEnabled$ | async) ||
!(editParams$ | async)?.managedByOrganization)
"
*ngIf="this.editMode && !(editParams$ | async)?.managedByOrganization"
type="button"
buttonType="danger"
bitButton
@@ -290,11 +286,7 @@
{{ "remove" | i18n }}
</button>
<button
*ngIf="
this.editMode &&
(accountDeprovisioningEnabled$ | async) &&
(editParams$ | async)?.managedByOrganization
"
*ngIf="this.editMode && (editParams$ | async)?.managedByOrganization"
type="button"
buttonType="danger"
bitButton

View File

@@ -64,6 +64,8 @@ import { commaSeparatedEmails } from "./validators/comma-separated-emails.valida
import { inputEmailLimitValidator } from "./validators/input-email-limit.validator";
import { orgSeatLimitReachedValidator } from "./validators/org-seat-limit-reached.validator";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum MemberDialogTab {
Role = 0,
Groups = 1,
@@ -92,6 +94,8 @@ export interface EditMemberDialogParams extends CommonMemberDialogParams {
export type MemberDialogParams = EditMemberDialogParams | AddMemberDialogParams;
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum MemberDialogResult {
Saved = "saved",
Canceled = "canceled",
@@ -102,6 +106,7 @@ export enum MemberDialogResult {
@Component({
templateUrl: "member-dialog.component.html",
standalone: false,
})
export class MemberDialogComponent implements OnDestroy {
loading = true;
@@ -152,32 +157,20 @@ export class MemberDialogComponent implements OnDestroy {
manageResetPassword: false,
});
protected accountDeprovisioningEnabled$: Observable<boolean> = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
get isExternalIdVisible(): boolean {
return !!this.formGroup.get("externalId")?.value;
}
protected isExternalIdVisible$ = this.configService
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
.pipe(
map((isEnabled) => {
return !isEnabled || !!this.formGroup.get("externalId")?.value;
}),
);
protected isSsoExternalIdVisible$ = this.configService
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
.pipe(
map((isEnabled) => {
return isEnabled && !!this.formGroup.get("ssoExternalId")?.value;
}),
);
private destroy$ = new Subject<void>();
get isSsoExternalIdVisible(): boolean {
return !!this.formGroup.get("ssoExternalId")?.value;
}
get customUserTypeSelected(): boolean {
return this.formGroup.value.type === OrganizationUserType.Custom;
}
private destroy$ = new Subject<void>();
isEditDialogParams(
params: EditMemberDialogParams | AddMemberDialogParams,
): params is EditMemberDialogParams {
@@ -460,7 +453,13 @@ export class MemberDialogComponent implements OnDestroy {
return Object.assign(p, partialPermissions);
}
handleDependentPermissions() {
async handleDependentPermissions() {
const separateCustomRolePermissions = await this.configService.getFeatureFlag(
FeatureFlag.SeparateCustomRolePermissions,
);
if (separateCustomRolePermissions) {
return;
}
// Manage Password Reset (Account Recovery) must have Manage Users enabled
if (
this.permissionsGroup.value.manageResetPassword &&
@@ -667,11 +666,9 @@ export class MemberDialogComponent implements OnDestroy {
const showWarningDialog = combineLatest([
this.organization$,
this.deleteManagedMemberWarningService.warningAcknowledged(this.params.organizationId),
this.accountDeprovisioningEnabled$,
]).pipe(
map(
([organization, acknowledged, featureFlagEnabled]) =>
featureFlagEnabled &&
([organization, acknowledged]) =>
organization.canManageUsers &&
organization.productTierType === ProductTierType.Enterprise &&
!acknowledged,
@@ -714,9 +711,8 @@ export class MemberDialogComponent implements OnDestroy {
message: this.i18nService.t("organizationUserDeleted", this.params.name),
});
if (await firstValueFrom(this.accountDeprovisioningEnabled$)) {
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.params.organizationId);
}
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.params.organizationId);
this.close(MemberDialogResult.Deleted);
};

View File

@@ -10,6 +10,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
@Component({
selector: "app-nested-checkbox",
templateUrl: "nested-checkbox.component.html",
standalone: false,
})
export class NestedCheckboxComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();

View File

@@ -50,6 +50,8 @@ export type ResetPasswordDialogData = {
organizationId: string;
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum ResetPasswordDialogResult {
Ok = "ok",
}
@@ -57,6 +59,7 @@ export enum ResetPasswordDialogResult {
@Component({
selector: "app-reset-password",
templateUrl: "reset-password.component.html",
standalone: false,
})
/**
* Used in a dialog for initiating the account recovery process against a

View File

@@ -5,7 +5,14 @@
[placeholder]="'searchMembers' | i18n"
></bit-search>
<button type="button" bitButton buttonType="primary" (click)="invite()" [disabled]="!firstLoaded">
<button
type="button"
bitButton
buttonType="primary"
(click)="invite()"
[disabled]="!firstLoaded"
*ngIf="showUserManagementControls$ | async"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "inviteMember" | i18n }}
</button>
@@ -16,6 +23,7 @@
[selected]="status"
(selectedChange)="statusToggle.next($event)"
[attr.aria-label]="'memberStatusFilter' | i18n"
*ngIf="showUserManagementControls$ | async"
>
<bit-toggle [value]="null">
{{ "all" | i18n }}
@@ -71,7 +79,7 @@
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th bitCell class="tw-w-20">
<th bitCell class="tw-w-20" *ngIf="showUserManagementControls$ | async">
<input
type="checkbox"
bitCheckbox
@@ -94,6 +102,7 @@
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
*ngIf="showUserManagementControls$ | async"
></button>
<bit-menu #headerMenu>
@@ -174,74 +183,143 @@
alignContent="middle"
[ngClass]="rowHeightClass"
>
<td bitCell (click)="dataSource.checkUser(u)">
<td
bitCell
(click)="dataSource.checkUser(u)"
*ngIf="showUserManagementControls$ | async"
>
<input type="checkbox" bitCheckbox [(ngModel)]="$any(u).checked" />
</td>
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
<div class="tw-flex tw-items-center">
<bit-avatar
size="small"
[text]="u | userName"
[id]="u.userId"
[color]="u.avatarColor"
class="tw-mr-3"
></bit-avatar>
<div class="tw-flex tw-flex-col">
<div class="tw-flex tw-flex-row tw-gap-2">
<button type="button" bitLink>
{{ u.name ?? u.email }}
</button>
<span
bitBadge
class="tw-text-xs"
variant="secondary"
*ngIf="u.status === userStatusType.Invited"
>
{{ "invited" | i18n }}
</span>
<span
bitBadge
class="tw-text-xs"
variant="warning"
*ngIf="u.status === userStatusType.Accepted"
>
{{ "needsConfirmation" | i18n }}
</span>
<span
bitBadge
class="tw-text-xs"
variant="secondary"
*ngIf="u.status === userStatusType.Revoked"
>
{{ "revoked" | i18n }}
</span>
</div>
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
{{ u.email }}
<ng-container *ngIf="showUserManagementControls$ | async; else readOnlyUserInfo">
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
<div class="tw-flex tw-items-center">
<bit-avatar
size="small"
[text]="u | userName"
[id]="u.userId"
[color]="u.avatarColor"
class="tw-mr-3"
></bit-avatar>
<div class="tw-flex tw-flex-col">
<div class="tw-flex tw-flex-row tw-gap-2">
<button type="button" bitLink>
{{ u.name ?? u.email }}
</button>
<span
bitBadge
class="tw-text-xs"
variant="secondary"
*ngIf="u.status === userStatusType.Invited"
>
{{ "invited" | i18n }}
</span>
<span
bitBadge
class="tw-text-xs"
variant="warning"
*ngIf="u.status === userStatusType.Accepted"
>
{{ "needsConfirmation" | i18n }}
</span>
<span
bitBadge
class="tw-text-xs"
variant="secondary"
*ngIf="u.status === userStatusType.Revoked"
>
{{ "revoked" | i18n }}
</span>
</div>
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
{{ u.email }}
</div>
</div>
</div>
</div>
</td>
</td>
</ng-container>
<ng-template #readOnlyUserInfo>
<td bitCell>
<div class="tw-flex tw-items-center">
<bit-avatar
size="small"
[text]="u | userName"
[id]="u.userId"
[color]="u.avatarColor"
class="tw-mr-3"
></bit-avatar>
<div class="tw-flex tw-flex-col">
<div class="tw-flex tw-flex-row tw-gap-2">
<span>{{ u.name ?? u.email }}</span>
<span
bitBadge
class="tw-text-xs"
variant="secondary"
*ngIf="u.status === userStatusType.Invited"
>
{{ "invited" | i18n }}
</span>
<span
bitBadge
class="tw-text-xs"
variant="warning"
*ngIf="u.status === userStatusType.Accepted"
>
{{ "needsConfirmation" | i18n }}
</span>
<span
bitBadge
class="tw-text-xs"
variant="secondary"
*ngIf="u.status === userStatusType.Revoked"
>
{{ "revoked" | i18n }}
</span>
</div>
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
{{ u.email }}
</div>
</div>
</div>
</td>
</ng-template>
<td
bitCell
(click)="edit(u, organization.useGroups ? memberTab.Groups : memberTab.Collections)"
class="tw-cursor-pointer"
>
<bit-badge-list
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
[maxItems]="3"
variant="secondary"
></bit-badge-list>
</td>
<ng-container *ngIf="showUserManagementControls$ | async; else readOnlyGroupsCell">
<td
bitCell
(click)="edit(u, organization.useGroups ? memberTab.Groups : memberTab.Collections)"
class="tw-cursor-pointer"
>
<bit-badge-list
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
[maxItems]="3"
variant="secondary"
></bit-badge-list>
</td>
</ng-container>
<ng-template #readOnlyGroupsCell>
<td bitCell>
<bit-badge-list
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
[maxItems]="3"
variant="secondary"
></bit-badge-list>
</td>
</ng-template>
<td
bitCell
(click)="edit(u, memberTab.Role)"
class="tw-cursor-pointer tw-text-sm tw-text-muted"
>
{{ u.type | userType }}
</td>
<ng-container *ngIf="showUserManagementControls$ | async; else readOnlyRoleCell">
<td
bitCell
(click)="edit(u, memberTab.Role)"
class="tw-cursor-pointer tw-text-sm tw-text-muted"
>
{{ u.type | userType }}
</td>
</ng-container>
<ng-template #readOnlyRoleCell>
<td bitCell class="tw-text-sm tw-text-muted">
{{ u.type | userType }}
</td>
</ng-template>
<td bitCell class="tw-text-muted">
<ng-container *ngIf="u.twoFactorEnabled">
@@ -271,53 +349,58 @@
></button>
<bit-menu #rowMenu>
<button
type="button"
bitMenuItem
(click)="reinvite(u)"
*ngIf="u.status === userStatusType.Invited"
>
<i aria-hidden="true" class="bwi bwi-envelope"></i>
{{ "resendInvitation" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="confirm(u)"
*ngIf="u.status === userStatusType.Accepted"
>
<span class="tw-text-success">
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
</span>
</button>
<bit-menu-divider
*ngIf="
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
"
></bit-menu-divider>
<button type="button" bitMenuItem (click)="edit(u, memberTab.Role)">
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="edit(u, memberTab.Groups)"
*ngIf="organization.useGroups"
>
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(u, memberTab.Collections)">
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
</button>
<bit-menu-divider></bit-menu-divider>
<button
type="button"
bitMenuItem
(click)="openEventsDialog(u)"
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
>
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
</button>
<ng-container *ngIf="showUserManagementControls$ | async">
<button
type="button"
bitMenuItem
(click)="reinvite(u)"
*ngIf="u.status === userStatusType.Invited"
>
<i aria-hidden="true" class="bwi bwi-envelope"></i>
{{ "resendInvitation" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="confirm(u)"
*ngIf="u.status === userStatusType.Accepted"
>
<span class="tw-text-success">
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
</span>
</button>
<bit-menu-divider
*ngIf="
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
"
></bit-menu-divider>
<button type="button" bitMenuItem (click)="edit(u, memberTab.Role)">
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="edit(u, memberTab.Groups)"
*ngIf="organization.useGroups"
>
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
</button>
<button type="button" bitMenuItem (click)="edit(u, memberTab.Collections)">
<i aria-hidden="true" class="bwi bwi-collection-shared"></i>
{{ "collections" | i18n }}
</button>
<bit-menu-divider></bit-menu-divider>
<button
type="button"
bitMenuItem
(click)="openEventsDialog(u)"
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
>
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
</button>
</ng-container>
<!-- Account recovery is available to all users with appropriate permissions -->
<button
type="button"
bitMenuItem
@@ -326,45 +409,48 @@
>
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="restore(u)"
*ngIf="u.status === userStatusType.Revoked"
>
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
{{ "restoreAccess" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="revoke(u)"
*ngIf="u.status !== userStatusType.Revoked"
>
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
{{ "revokeAccess" | i18n }}
</button>
<button
*ngIf="!accountDeprovisioningEnabled || !u.managedByOrganization"
type="button"
bitMenuItem
(click)="remove(u)"
>
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
</span>
</button>
<button
*ngIf="accountDeprovisioningEnabled && u.managedByOrganization"
type="button"
bitMenuItem
(click)="deleteUser(u)"
>
<span class="tw-text-danger">
<i class="bwi bwi-trash" aria-hidden="true"></i>
{{ "delete" | i18n }}
</span>
</button>
<ng-container *ngIf="showUserManagementControls$ | async">
<button
type="button"
bitMenuItem
(click)="restore(u)"
*ngIf="u.status === userStatusType.Revoked"
>
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
{{ "restoreAccess" | i18n }}
</button>
<button
type="button"
bitMenuItem
(click)="revoke(u)"
*ngIf="u.status !== userStatusType.Revoked"
>
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
{{ "revokeAccess" | i18n }}
</button>
<button
*ngIf="!u.managedByOrganization"
type="button"
bitMenuItem
(click)="remove(u)"
>
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
</span>
</button>
<button
*ngIf="u.managedByOrganization"
type="button"
bitMenuItem
(click)="deleteUser(u)"
>
<span class="tw-text-danger">
<i class="bwi bwi-trash" aria-hidden="true"></i>
{{ "delete" | i18n }}
</span>
</button>
</ng-container>
</bit-menu>
</td>
</tr>
@@ -373,4 +459,3 @@
</cdk-virtual-scroll-viewport>
</ng-container>
</ng-container>
<ng-template #resetPasswordTemplate></ng-template>

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import {
@@ -90,11 +90,9 @@ class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView>
@Component({
templateUrl: "members.component.html",
standalone: false,
})
export class MembersComponent extends BaseMembersComponent<OrganizationUserView> implements OnInit {
@ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true })
resetPasswordModalRef: ViewContainerRef;
export class MembersComponent extends BaseMembersComponent<OrganizationUserView> {
userType = OrganizationUserType;
userStatusType = OrganizationUserStatusType;
memberTab = MemberDialogTab;
@@ -104,16 +102,18 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
status: OrganizationUserStatusType = null;
orgResetPasswordPolicyEnabled = false;
orgIsOnSecretsManagerStandalone = false;
accountDeprovisioningEnabled = false;
protected canUseSecretsManager$: Observable<boolean>;
protected showUserManagementControls$: Observable<boolean>;
// Fixed sizes used for cdkVirtualScroll
protected rowHeight = 69;
protected rowHeightClass = `tw-h-[69px]`;
private organizationUsersCount = 0;
get occupiedSeatCount(): number {
return this.dataSource.activeUserCount;
return this.organizationUsersCount;
}
constructor(
@@ -139,8 +139,8 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
private groupService: GroupApiService,
private collectionService: CollectionService,
private billingApiService: BillingApiServiceAbstraction,
private configService: ConfigService,
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
private configService: ConfigService,
) {
super(
apiService,
@@ -220,6 +220,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
);
this.orgIsOnSecretsManagerStandalone = billingMetadata.isOnSecretsManagerStandalone;
this.organizationUsersCount = billingMetadata.organizationOccupiedSeats;
await this.load();
@@ -235,11 +236,16 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
takeUntilDestroyed(),
)
.subscribe();
}
async ngOnInit() {
this.accountDeprovisioningEnabled = await this.configService.getFeatureFlag(
FeatureFlag.AccountDeprovisioning,
// Setup feature flag-dependent observables
const separateCustomRolePermissionsEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.SeparateCustomRolePermissions,
);
this.showUserManagementControls$ = separateCustomRolePermissionsEnabled$.pipe(
map(
(separateCustomRolePermissionsEnabled) =>
!separateCustomRolePermissionsEnabled || this.organization.canManageUsers,
),
);
}
@@ -591,20 +597,18 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
}
async bulkDelete() {
if (this.accountDeprovisioningEnabled) {
const warningAcknowledged = await firstValueFrom(
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
);
const warningAcknowledged = await firstValueFrom(
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
);
if (
!warningAcknowledged &&
this.organization.canManageUsers &&
this.organization.productTierType === ProductTierType.Enterprise
) {
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
if (!acknowledged) {
return;
}
if (
!warningAcknowledged &&
this.organization.canManageUsers &&
this.organization.productTierType === ProductTierType.Enterprise
) {
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
if (!acknowledged) {
return;
}
}
@@ -794,20 +798,18 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
}
async deleteUser(user: OrganizationUserView) {
if (this.accountDeprovisioningEnabled) {
const warningAcknowledged = await firstValueFrom(
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
);
const warningAcknowledged = await firstValueFrom(
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
);
if (
!warningAcknowledged &&
this.organization.canManageUsers &&
this.organization.productTierType === ProductTierType.Enterprise
) {
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
if (!acknowledged) {
return false;
}
if (
!warningAcknowledged &&
this.organization.canManageUsers &&
this.organization.productTierType === ProductTierType.Enterprise
) {
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
if (!acknowledged) {
return false;
}
}
@@ -829,9 +831,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return false;
}
if (this.accountDeprovisioningEnabled) {
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
}
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
this.organization.id,
@@ -864,56 +864,23 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
});
}
get showBulkConfirmUsers(): boolean {
if (!this.accountDeprovisioningEnabled) {
return super.showBulkConfirmUsers;
}
return this.dataSource
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Accepted);
}
get showBulkReinviteUsers(): boolean {
if (!this.accountDeprovisioningEnabled) {
return super.showBulkReinviteUsers;
}
return this.dataSource
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Invited);
}
get showBulkRestoreUsers(): boolean {
return (
!this.accountDeprovisioningEnabled ||
this.dataSource
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Revoked)
);
return this.dataSource
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Revoked);
}
get showBulkRevokeUsers(): boolean {
return (
!this.accountDeprovisioningEnabled ||
this.dataSource
.getCheckedUsers()
.every((member) => member.status != this.userStatusType.Revoked)
);
return this.dataSource
.getCheckedUsers()
.every((member) => member.status != this.userStatusType.Revoked);
}
get showBulkRemoveUsers(): boolean {
return (
!this.accountDeprovisioningEnabled ||
this.dataSource.getCheckedUsers().every((member) => !member.managedByOrganization)
);
return this.dataSource.getCheckedUsers().every((member) => !member.managedByOrganization);
}
get showBulkDeleteUsers(): boolean {
if (!this.accountDeprovisioningEnabled) {
return false;
}
const validStatuses = [
this.userStatusType.Accepted,
this.userStatusType.Confirmed,

View File

@@ -112,7 +112,7 @@ export class OrganizationUserResetPasswordService
if (orgSymKey == null) {
throw new Error("No org key found");
}
const decPrivateKey = await this.encryptService.decryptToBytes(
const decPrivateKey = await this.encryptService.unwrapDecapsulationKey(
new EncString(response.encryptedPrivateKey),
orgSymKey,
);

View File

@@ -14,7 +14,7 @@ import {
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { deepLinkGuard } from "../../auth/guards/deep-link.guard";
import { deepLinkGuard } from "../../auth/guards/deep-link/deep-link.guard";
import { VaultModule } from "./collections/vault.module";
import { isEnterpriseOrgGuard } from "./guards/is-enterprise-org.guard";

View File

@@ -14,5 +14,6 @@ export class DisableSendPolicy extends BasePolicy {
@Component({
selector: "policy-disable-send",
templateUrl: "disable-send.component.html",
standalone: false,
})
export class DisableSendPolicyComponent extends BasePolicyComponent {}

View File

@@ -28,6 +28,7 @@ export class MasterPasswordPolicy extends BasePolicy {
@Component({
selector: "policy-master-password",
templateUrl: "master-password.component.html",
standalone: false,
})
export class MasterPasswordPolicyComponent extends BasePolicyComponent implements OnInit {
MinPasswordLength = Utils.minimumPasswordLength;

View File

@@ -7,7 +7,7 @@ import { BehaviorSubject, map } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Generators } from "@bitwarden/generator-core";
import { BuiltIn, Profile } from "@bitwarden/generator-core";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
@@ -21,18 +21,27 @@ export class PasswordGeneratorPolicy extends BasePolicy {
@Component({
selector: "policy-password-generator",
templateUrl: "password-generator.component.html",
standalone: false,
})
export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
// these properties forward the application default settings to the UI
// for HTML attribute bindings
protected readonly minLengthMin = Generators.password.settings.constraints.length.min;
protected readonly minLengthMax = Generators.password.settings.constraints.length.max;
protected readonly minNumbersMin = Generators.password.settings.constraints.minNumber.min;
protected readonly minNumbersMax = Generators.password.settings.constraints.minNumber.max;
protected readonly minSpecialMin = Generators.password.settings.constraints.minSpecial.min;
protected readonly minSpecialMax = Generators.password.settings.constraints.minSpecial.max;
protected readonly minNumberWordsMin = Generators.passphrase.settings.constraints.numWords.min;
protected readonly minNumberWordsMax = Generators.passphrase.settings.constraints.numWords.max;
protected readonly minLengthMin =
BuiltIn.password.profiles[Profile.account].constraints.default.length.min;
protected readonly minLengthMax =
BuiltIn.password.profiles[Profile.account].constraints.default.length.max;
protected readonly minNumbersMin =
BuiltIn.password.profiles[Profile.account].constraints.default.minNumber.min;
protected readonly minNumbersMax =
BuiltIn.password.profiles[Profile.account].constraints.default.minNumber.max;
protected readonly minSpecialMin =
BuiltIn.password.profiles[Profile.account].constraints.default.minSpecial.min;
protected readonly minSpecialMax =
BuiltIn.password.profiles[Profile.account].constraints.default.minSpecial.max;
protected readonly minNumberWordsMin =
BuiltIn.passphrase.profiles[Profile.account].constraints.default.numWords.min;
protected readonly minNumberWordsMax =
BuiltIn.passphrase.profiles[Profile.account].constraints.default.numWords.max;
data = this.formBuilder.group({
overridePasswordType: [null],

View File

@@ -14,5 +14,6 @@ export class PersonalOwnershipPolicy extends BasePolicy {
@Component({
selector: "policy-personal-ownership",
templateUrl: "personal-ownership.component.html",
standalone: false,
})
export class PersonalOwnershipPolicyComponent extends BasePolicyComponent {}

View File

@@ -35,5 +35,4 @@
</tr>
</ng-template>
</bit-table>
<ng-template #editTemplate></ng-template>
</bit-container>

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs";
import { first } from "rxjs/operators";
@@ -31,11 +31,9 @@ import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.compo
@Component({
selector: "app-org-policies",
templateUrl: "policies.component.html",
standalone: false,
})
export class PoliciesComponent implements OnInit {
@ViewChild("editTemplate", { read: ViewContainerRef, static: true })
editModalRef: ViewContainerRef;
loading = true;
organizationId: string;
policies: BasePolicy[];

View File

@@ -41,6 +41,8 @@ export type PolicyEditDialogData = {
organizationId: string;
};
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum PolicyEditDialogResult {
Saved = "saved",
UpgradePlan = "upgrade-plan",
@@ -48,6 +50,7 @@ export enum PolicyEditDialogResult {
@Component({
selector: "app-policy-edit",
templateUrl: "policy-edit.component.html",
standalone: false,
})
export class PolicyEditComponent implements AfterViewInit {
@ViewChild("policyForm", { read: ViewContainerRef, static: true })

View File

@@ -14,5 +14,6 @@ export class RemoveUnlockWithPinPolicy extends BasePolicy {
@Component({
selector: "remove-unlock-with-pin",
templateUrl: "remove-unlock-with-pin.component.html",
standalone: false,
})
export class RemoveUnlockWithPinPolicyComponent extends BasePolicyComponent {}

View File

@@ -19,5 +19,6 @@ export class RequireSsoPolicy extends BasePolicy {
@Component({
selector: "policy-require-sso",
templateUrl: "require-sso.component.html",
standalone: false,
})
export class RequireSsoPolicyComponent extends BasePolicyComponent {}

View File

@@ -27,6 +27,7 @@ export class ResetPasswordPolicy extends BasePolicy {
@Component({
selector: "policy-reset-password",
templateUrl: "reset-password.component.html",
standalone: false,
})
export class ResetPasswordPolicyComponent extends BasePolicyComponent implements OnInit {
data = this.formBuilder.group({

View File

@@ -15,6 +15,7 @@ export class SendOptionsPolicy extends BasePolicy {
@Component({
selector: "policy-send-options",
templateUrl: "send-options.component.html",
standalone: false,
})
export class SendOptionsPolicyComponent extends BasePolicyComponent {
data = this.formBuilder.group({

View File

@@ -1,11 +1,6 @@
<bit-callout *ngIf="accountDeprovisioningEnabled$ | async; else disabledBlock" type="warning">
<bit-callout type="warning">
{{ "singleOrgPolicyMemberWarning" | i18n }}
</bit-callout>
<ng-template #disabledBlock>
<bit-callout type="warning">
{{ "singleOrgPolicyWarning" | i18n }}
</bit-callout>
</ng-template>
<bit-form-control>
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />

View File

@@ -1,15 +1,12 @@
import { Component, OnInit } from "@angular/core";
import { firstValueFrom, Observable } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
export class SingleOrgPolicy extends BasePolicy {
name = "singleOrg";
description = "singleOrgDesc";
description = "singleOrgPolicyDesc";
type = PolicyType.SingleOrg;
component = SingleOrgPolicyComponent;
}
@@ -17,24 +14,12 @@ export class SingleOrgPolicy extends BasePolicy {
@Component({
selector: "policy-single-org",
templateUrl: "single-org.component.html",
standalone: false,
})
export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnInit {
constructor(private configService: ConfigService) {
super();
}
protected accountDeprovisioningEnabled$: Observable<boolean> = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
async ngOnInit() {
super.ngOnInit();
const isAccountDeprovisioningEnabled = await firstValueFrom(this.accountDeprovisioningEnabled$);
this.policy.description = isAccountDeprovisioningEnabled
? "singleOrgPolicyDesc"
: "singleOrgDesc";
if (!this.policyResponse.canToggleState) {
this.enabled.disable();
}

View File

@@ -14,5 +14,6 @@ export class TwoFactorAuthenticationPolicy extends BasePolicy {
@Component({
selector: "policy-two-factor-authentication",
templateUrl: "two-factor-authentication.component.html",
standalone: false,
})
export class TwoFactorAuthenticationPolicyComponent extends BasePolicyComponent {}

View File

@@ -9,12 +9,16 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { ExposedPasswordsReportComponent } from "../../../tools/reports/pages/organizations/exposed-passwords-report.component";
import { InactiveTwoFactorReportComponent } from "../../../tools/reports/pages/organizations/inactive-two-factor-report.component";
import { ReusedPasswordsReportComponent } from "../../../tools/reports/pages/organizations/reused-passwords-report.component";
import { UnsecuredWebsitesReportComponent } from "../../../tools/reports/pages/organizations/unsecured-websites-report.component";
import { WeakPasswordsReportComponent } from "../../../tools/reports/pages/organizations/weak-passwords-report.component";
/* eslint no-restricted-imports: "error" */
// eslint-disable-next-line no-restricted-imports
import { ExposedPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/exposed-passwords-report.component";
// eslint-disable-next-line no-restricted-imports
import { InactiveTwoFactorReportComponent } from "../../../dirt/reports/pages/organizations/inactive-two-factor-report.component";
// eslint-disable-next-line no-restricted-imports
import { ReusedPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/reused-passwords-report.component";
// eslint-disable-next-line no-restricted-imports
import { UnsecuredWebsitesReportComponent } from "../../../dirt/reports/pages/organizations/unsecured-websites-report.component";
// eslint-disable-next-line no-restricted-imports
import { WeakPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/weak-passwords-report.component";
import { isPaidOrgGuard } from "../guards/is-paid-org.guard";
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
import { organizationRedirectGuard } from "../guards/org-redirect.guard";

View File

@@ -1,8 +1,8 @@
import { NgModule } from "@angular/core";
import { ReportsSharedModule } from "../../../dirt/reports";
import { LooseComponentsModule } from "../../../shared";
import { SharedModule } from "../../../shared/shared.module";
import { ReportsSharedModule } from "../../../tools/reports";
import { OrganizationReportingRoutingModule } from "./organization-reporting-routing.module";
import { ReportsHomeComponent } from "./reports-home.component";

View File

@@ -12,11 +12,12 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { ReportVariant, reports, ReportType, ReportEntry } from "../../../tools/reports";
import { ReportVariant, reports, ReportType, ReportEntry } from "../../../dirt/reports";
@Component({
selector: "app-org-reports-home",
templateUrl: "reports-home.component.html",
standalone: false,
})
export class ReportsHomeComponent implements OnInit {
reports$: Observable<ReportEntry[]>;

View File

@@ -93,7 +93,4 @@
{{ "purgeVault" | i18n }}
</button>
</app-danger-zone>
<ng-template #apiKeyTemplate></ng-template>
<ng-template #rotateApiKeyTemplate></ng-template>
</bit-container>

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import {
@@ -41,13 +41,9 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./
@Component({
selector: "app-org-account",
templateUrl: "account.component.html",
standalone: false,
})
export class AccountComponent implements OnInit, OnDestroy {
@ViewChild("apiKeyTemplate", { read: ViewContainerRef, static: true })
apiKeyModalRef: ViewContainerRef;
@ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true })
rotateApiKeyModalRef: ViewContainerRef;
selfHosted = false;
canEditSubscription = true;
loading = true;

View File

@@ -71,6 +71,8 @@ export interface DeleteOrganizationDialogParams {
requestType: "InvalidFamiliesForEnterprise" | "RegularDelete";
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum DeleteOrganizationDialogResult {
Deleted = "deleted",
Canceled = "canceled",

View File

@@ -29,6 +29,7 @@ import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor/two-
@Component({
selector: "app-two-factor-setup",
templateUrl: "../../../auth/settings/two-factor/two-factor-setup.component.html",
standalone: false,
})
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent implements OnInit {
tabbedHeader = false;
@@ -118,7 +119,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
return this.apiService.getTwoFactorOrganizationProviders(this.organizationId);
}
protected filterProvider(type: TwoFactorProviderType) {
protected filterProvider(type: TwoFactorProviderType): boolean {
return type !== TwoFactorProviderType.OrganizationDuo;
}
}

View File

@@ -26,6 +26,8 @@ import {
Permission,
} from "./access-selector.models";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum PermissionMode {
/**
* No permission controls or column present. No permission values are emitted.
@@ -53,6 +55,7 @@ export enum PermissionMode {
multi: true,
},
],
standalone: false,
})
export class AccessSelectorComponent implements ControlValueAccessor, OnInit, OnDestroy {
private destroy$ = new Subject<void>();
@@ -310,7 +313,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
protected itemIcon(item: AccessItemView) {
switch (item.type) {
case AccessItemType.Collection:
return "bwi-collection";
return "bwi-collection-shared";
case AccessItemType.Group:
return "bwi-users";
case AccessItemType.Member:

View File

@@ -15,6 +15,8 @@ import { GroupView } from "../../../core";
/**
* Permission options that replace/correspond with manage, readOnly, and hidePassword server fields.
*/
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum CollectionPermission {
View = "view",
ViewExceptPass = "viewExceptPass",
@@ -23,6 +25,8 @@ export enum CollectionPermission {
Manage = "manage",
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum AccessItemType {
Collection,
Group,

View File

@@ -5,6 +5,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
@Pipe({
name: "userType",
standalone: false,
})
export class UserTypePipe implements PipeTransform {
constructor(private i18nService: I18nService) {}

View File

@@ -35,7 +35,7 @@
</bit-select>
</bit-form-field>
<bit-form-field *ngIf="isExternalIdVisible$ | async">
<bit-form-field *ngIf="isExternalIdVisible">
<bit-label>{{ "externalId" | i18n }}</bit-label>
<input bitInput formControlName="externalId" />
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
@@ -48,14 +48,14 @@
<bit-option
*ngIf="deletedParentName"
disabled
icon="bwi-collection"
icon="bwi-collection-shared"
[value]="deletedParentName"
label="{{ deletedParentName }} ({{ 'deleted' | i18n }})"
>
</bit-option>
<bit-option
*ngFor="let collection of nestOptions"
icon="bwi-collection"
icon="bwi-collection-shared"
[value]="collection.name"
[label]="collection.name"
>

View File

@@ -38,7 +38,6 @@ 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
DIALOG_DATA,
@@ -65,6 +64,8 @@ import {
} from "../access-selector/access-selector.models";
import { AccessSelectorModule } from "../access-selector/access-selector.module";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum CollectionDialogTabType {
Info = 0,
Access = 1,
@@ -76,6 +77,8 @@ export enum CollectionDialogTabType {
* @readonly
* @enum {string}
*/
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum ButtonType {
/** Displayed when the user has reached the maximum number of collections allowed for the organization. */
Upgrade = "upgrade",
@@ -103,6 +106,8 @@ export interface CollectionDialogResult {
collection: CollectionResponse | CollectionView;
}
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum CollectionDialogAction {
Saved = "saved",
Canceled = "canceled",
@@ -129,7 +134,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
protected showOrgSelector = false;
protected formGroup = this.formBuilder.group({
name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]],
externalId: "",
externalId: { value: "", disabled: true },
parent: undefined as string | undefined,
access: [[] as AccessItemValue[]],
selectedOrg: "",
@@ -139,16 +144,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
protected showAddAccessWarning = false;
protected collections: Collection[];
protected buttonDisplayName: ButtonType = ButtonType.Save;
protected isExternalIdVisible$ = this.configService
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
.pipe(
map((isEnabled) => {
return (
!isEnabled ||
(!!this.params.isAdminConsoleActive && !!this.formGroup.get("externalId")?.value)
);
}),
);
private orgExceedingCollectionLimit!: Organization;
constructor(
@@ -159,7 +154,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
private groupService: GroupApiService,
private collectionAdminService: CollectionAdminService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private organizationUserApiService: OrganizationUserApiService,
private dialogService: DialogService,
private changeDetectorRef: ChangeDetectorRef,
@@ -348,6 +342,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
return this.formGroup.controls.selectedOrg;
}
protected get isExternalIdVisible(): boolean {
return this.params.isAdminConsoleActive && !!this.formGroup.get("externalId")?.value;
}
protected get collectionId() {
return this.params.collectionId;
}
@@ -484,23 +482,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
private handleFormGroupReadonly(readonly: boolean) {
if (readonly) {
this.formGroup.controls.name.disable();
this.formGroup.controls.externalId.disable();
this.formGroup.controls.parent.disable();
this.formGroup.controls.access.disable();
} else {
this.formGroup.controls.name.enable();
this.configService
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
.pipe(takeUntil(this.destroy$))
.subscribe((isEnabled) => {
if (isEnabled) {
this.formGroup.controls.externalId.disable();
} else {
this.formGroup.controls.externalId.enable();
}
});
this.formGroup.controls.parent.enable();
this.formGroup.controls.access.enable();
}

View File

@@ -19,6 +19,7 @@ import { BaseAcceptComponent } from "../../../common/base.accept.component";
@Component({
selector: "app-accept-family-sponsorship",
templateUrl: "accept-family-sponsorship.component.html",
standalone: false,
})
export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent {
protected logo = BitwardenLogo;

View File

@@ -18,8 +18,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { AccountRecoveryTrustComponent } from "@bitwarden/key-management-ui";
import { OrganizationTrustComponent } from "../manage/organization-trust.component";
import { OrganizationUserResetPasswordService } from "../members/services/organization-user-reset-password/organization-user-reset-password.service";
interface EnrollMasterPasswordResetData {
@@ -62,7 +62,7 @@ export class EnrollMasterPasswordReset {
await userVerificationService.buildRequest<OrganizationUserResetPasswordEnrollmentRequest>(
secret,
);
const dialogRef = OrganizationTrustComponent.open(dialogService, {
const dialogRef = AccountRecoveryTrustComponent.open(dialogService, {
name: data.organization.name,
orgId: data.organization.id,
publicKey,

View File

@@ -3,25 +3,20 @@
import { DOCUMENT } from "@angular/common";
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { NavigationEnd, Router } from "@angular/router";
import * as jq from "jquery";
import { Router } from "@angular/router";
import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -29,11 +24,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { NotificationsService } from "@bitwarden/common/platform/notifications";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
import { PolicyListService } from "./admin-console/core/policy-list.service";
@@ -56,6 +49,7 @@ const IdleTimeout = 60000 * 10; // 10 minutes
@Component({
selector: "app-root",
templateUrl: "app.component.html",
standalone: false,
})
export class AppComponent implements OnDestroy, OnInit {
private lastActivity: Date = null;
@@ -69,8 +63,6 @@ export class AppComponent implements OnDestroy, OnInit {
@Inject(DOCUMENT) private document: Document,
private broadcasterService: BroadcasterService,
private folderService: InternalFolderService,
private syncService: SyncService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private cipherService: CipherService,
private authService: AuthService,
private router: Router,
@@ -85,17 +77,13 @@ export class AppComponent implements OnDestroy, OnInit {
private notificationsService: NotificationsService,
private stateService: StateService,
private eventUploadService: EventUploadService,
private policyService: InternalPolicyService,
protected policyListService: PolicyListService,
private keyConnectorService: KeyConnectorService,
protected configService: ConfigService,
private dialogService: DialogService,
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
private organizationService: InternalOrganizationServiceAbstraction,
private accountService: AccountService,
private apiService: ApiService,
private appIdService: AppIdService,
private processReloadService: ProcessReloadServiceAbstraction,
private deviceTrustToastService: DeviceTrustToastService,
) {
@@ -247,15 +235,6 @@ export class AppComponent implements OnDestroy, OnInit {
});
});
this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => {
if (event instanceof NavigationEnd) {
const modals = Array.from(document.querySelectorAll(".modal"));
for (const modal of modals) {
(jq(modal) as any).modal("hide");
}
}
});
this.policyListService.addPolicies([
new TwoFactorAuthenticationPolicy(),
new MasterPasswordPolicy(),
@@ -303,7 +282,7 @@ export class AppComponent implements OnDestroy, OnInit {
);
await Promise.all([
this.keyService.clearKeys(),
this.keyService.clearKeys(userId),
this.cipherService.clear(userId),
this.folderService.clear(userId),
this.collectionService.clear(userId),

View File

@@ -1,3 +1,5 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum WebauthnLoginCredentialPrfStatus {
Enabled = 0,
Supported = 1,

View File

@@ -0,0 +1 @@
export * from "./web-change-password.service";

View File

@@ -0,0 +1,63 @@
import { mock, MockProxy } from "jest-mock-extended";
import { ChangePasswordService } from "@bitwarden/auth/angular";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { KeyService } from "@bitwarden/key-management";
import { UserKeyRotationService } from "@bitwarden/web-vault/app/key-management/key-rotation/user-key-rotation.service";
import { WebChangePasswordService } from "./web-change-password.service";
describe("WebChangePasswordService", () => {
let keyService: MockProxy<KeyService>;
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
let userKeyRotationService: MockProxy<UserKeyRotationService>;
let sut: ChangePasswordService;
const userId = "userId" as UserId;
const user: Account = {
id: userId,
email: "email",
emailVerified: false,
name: "name",
};
const currentPassword = "currentPassword";
const newPassword = "newPassword";
const newPasswordHint = "newPasswordHint";
beforeEach(() => {
keyService = mock<KeyService>();
masterPasswordApiService = mock<MasterPasswordApiService>();
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
userKeyRotationService = mock<UserKeyRotationService>();
sut = new WebChangePasswordService(
keyService,
masterPasswordApiService,
masterPasswordService,
userKeyRotationService,
);
});
describe("rotateUserKeyMasterPasswordAndEncryptedData()", () => {
it("should call the method with the same name on the UserKeyRotationService with the correct arguments", async () => {
// Arrange & Act
await sut.rotateUserKeyMasterPasswordAndEncryptedData(
currentPassword,
newPassword,
user,
newPasswordHint,
);
// Assert
expect(
userKeyRotationService.rotateUserKeyMasterPasswordAndEncryptedData,
).toHaveBeenCalledWith(currentPassword, newPassword, user, newPasswordHint);
});
});
});

View File

@@ -0,0 +1,34 @@
import { ChangePasswordService, DefaultChangePasswordService } from "@bitwarden/auth/angular";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { KeyService } from "@bitwarden/key-management";
import { UserKeyRotationService } from "@bitwarden/web-vault/app/key-management/key-rotation/user-key-rotation.service";
export class WebChangePasswordService
extends DefaultChangePasswordService
implements ChangePasswordService
{
constructor(
protected keyService: KeyService,
protected masterPasswordApiService: MasterPasswordApiService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
private userKeyRotationService: UserKeyRotationService,
) {
super(keyService, masterPasswordApiService, masterPasswordService);
}
override async rotateUserKeyMasterPasswordAndEncryptedData(
currentPassword: string,
newPassword: string,
user: Account,
newPasswordHint: string,
): Promise<void> {
await this.userKeyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
currentPassword,
newPassword,
user,
newPasswordHint,
);
}
}

View File

@@ -1,3 +1,4 @@
export * from "./change-password";
export * from "./login";
export * from "./login-decryption-options";
export * from "./webauthn-login";

View File

@@ -98,7 +98,7 @@ export class WebLoginComponentService
const enforcedPasswordPolicyOptions = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)),
),
);

View File

@@ -172,7 +172,6 @@ describe("WebRegistrationFinishService", () => {
let userKey: UserKey;
let userKeyEncString: EncString;
let userKeyPair: [string, EncString];
let capchaBypassToken: string;
let orgInvite: OrganizationInvite;
let orgSponsoredFreeFamilyPlanToken: string;
@@ -186,11 +185,11 @@ describe("WebRegistrationFinishService", () => {
emailVerificationToken = "emailVerificationToken";
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
passwordInputResult = {
masterKey: masterKey,
serverMasterKeyHash: "serverMasterKeyHash",
localMasterKeyHash: "localMasterKeyHash",
newMasterKey: masterKey,
newServerMasterKeyHash: "newServerMasterKeyHash",
newLocalMasterKeyHash: "newLocalMasterKeyHash",
kdfConfig: DEFAULT_KDF_CONFIG,
hint: "hint",
newPasswordHint: "newPasswordHint",
newPassword: "newPassword",
};
@@ -198,7 +197,6 @@ describe("WebRegistrationFinishService", () => {
userKeyEncString = new EncString("userKeyEncrypted");
userKeyPair = ["publicKey", new EncString("privateKey")];
capchaBypassToken = "capchaBypassToken";
orgInvite = new OrganizationInvite();
orgInvite.organizationUserId = "organizationUserId";
@@ -219,19 +217,13 @@ describe("WebRegistrationFinishService", () => {
);
});
it("registers the user and returns a captcha bypass token when given valid email verification input", async () => {
it("registers the user when given valid email verification input", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue(capchaBypassToken);
accountApiService.registerFinish.mockResolvedValue();
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
const result = await service.finishRegistration(
email,
passwordInputResult,
emailVerificationToken,
);
expect(result).toEqual(capchaBypassToken);
await service.finishRegistration(email, passwordInputResult, emailVerificationToken);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
@@ -239,8 +231,8 @@ describe("WebRegistrationFinishService", () => {
expect.objectContaining({
email,
emailVerificationToken: emailVerificationToken,
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
masterPasswordHint: passwordInputResult.hint,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
@@ -261,15 +253,13 @@ describe("WebRegistrationFinishService", () => {
);
});
it("it registers the user and returns a captcha bypass token when given an org invite", async () => {
it("it registers the user when given an org invite", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue(capchaBypassToken);
accountApiService.registerFinish.mockResolvedValue();
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
const result = await service.finishRegistration(email, passwordInputResult);
expect(result).toEqual(capchaBypassToken);
await service.finishRegistration(email, passwordInputResult);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
@@ -277,8 +267,8 @@ describe("WebRegistrationFinishService", () => {
expect.objectContaining({
email,
emailVerificationToken: undefined,
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
masterPasswordHint: passwordInputResult.hint,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
@@ -299,29 +289,27 @@ describe("WebRegistrationFinishService", () => {
);
});
it("registers the user and returns a captcha bypass token when given an org sponsored free family plan token", async () => {
it("registers the user when given an org sponsored free family plan token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue(capchaBypassToken);
accountApiService.registerFinish.mockResolvedValue();
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
const result = await service.finishRegistration(
await service.finishRegistration(
email,
passwordInputResult,
undefined,
orgSponsoredFreeFamilyPlanToken,
);
expect(result).toEqual(capchaBypassToken);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
emailVerificationToken: undefined,
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
masterPasswordHint: passwordInputResult.hint,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
@@ -342,13 +330,13 @@ describe("WebRegistrationFinishService", () => {
);
});
it("registers the user and returns a captcha bypass token when given an emergency access invite token", async () => {
it("registers the user when given an emergency access invite token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue(capchaBypassToken);
accountApiService.registerFinish.mockResolvedValue();
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
const result = await service.finishRegistration(
await service.finishRegistration(
email,
passwordInputResult,
undefined,
@@ -357,16 +345,14 @@ describe("WebRegistrationFinishService", () => {
emergencyAccessId,
);
expect(result).toEqual(capchaBypassToken);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
emailVerificationToken: undefined,
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
masterPasswordHint: passwordInputResult.hint,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],
@@ -387,13 +373,13 @@ describe("WebRegistrationFinishService", () => {
);
});
it("registers the user and returns a captcha bypass token when given a provider invite token", async () => {
it("registers the user when given a provider invite token", async () => {
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
accountApiService.registerFinish.mockResolvedValue(capchaBypassToken);
accountApiService.registerFinish.mockResolvedValue();
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
const result = await service.finishRegistration(
await service.finishRegistration(
email,
passwordInputResult,
undefined,
@@ -404,16 +390,14 @@ describe("WebRegistrationFinishService", () => {
providerUserId,
);
expect(result).toEqual(capchaBypassToken);
expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey);
expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey);
expect(accountApiService.registerFinish).toHaveBeenCalledWith(
expect.objectContaining({
email,
emailVerificationToken: undefined,
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
masterPasswordHint: passwordInputResult.hint,
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
masterPasswordHint: passwordInputResult.newPasswordHint,
userSymmetricKey: userKeyEncString.encryptedString,
userAsymmetricKeys: {
publicKey: userKeyPair[0],

View File

@@ -58,7 +58,7 @@ export class RotateableKeySetService {
throw new Error("failed to rotate key set: newUserKey is required");
}
const publicKey = await this.encryptService.decryptToBytes(
const publicKey = await this.encryptService.unwrapEncapsulationKey(
keySet.encryptedPublicKey,
oldUserKey,
);

View File

@@ -1,3 +1,5 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum EmergencyAccessStatusType {
Invited = 0,
Accepted = 1,

View File

@@ -1,3 +1,5 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum EmergencyAccessType {
View = 0,
Takeover = 1,

View File

@@ -1,58 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { RouterService } from "../../core/router.service";
/**
* Guard to persist and apply deep links to handle users who are not unlocked.
* @returns returns true. If user is not Unlocked will store URL to state for redirect once
* user is unlocked/Authenticated.
*/
export function deepLinkGuard(): CanActivateFn {
return async (route, routerState) => {
// Inject Services
const authService = inject(AuthService);
const router = inject(Router);
const routerService = inject(RouterService);
// Fetch State
const currentUrl = routerState.url;
const transientPreviousUrl = routerService.getPreviousUrl();
const authStatus = await authService.getAuthStatus();
// Evaluate State
/** before anything else, check if the user is already unlocked. */
if (authStatus === AuthenticationStatus.Unlocked) {
const persistedPreLoginUrl = await routerService.getAndClearLoginRedirectUrl();
if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) {
return router.navigateByUrl(persistedPreLoginUrl);
}
return true;
}
/**
* At this point the user is either `locked` or `loggedOut`, it doesn't matter.
* We opt to persist the currentUrl over the transient previousUrl. This supports
* the case where a user is locked out of their vault and they deep link from
* the "lock" page.
*
* When the user is locked out of their vault the currentUrl contains "lock" so it will
* not be persisted, the previousUrl will be persisted instead.
*/
if (isValidUrl(currentUrl)) {
await routerService.persistLoginRedirectUrl(currentUrl);
} else if (isValidUrl(transientPreviousUrl)) {
await routerService.persistLoginRedirectUrl(transientPreviousUrl);
}
return true;
};
function isValidUrl(url: string | null | undefined): boolean {
return !Utils.isNullOrEmpty(url) && !url?.toLocaleLowerCase().includes("/lock");
}
}

View File

@@ -7,22 +7,25 @@ import { MockProxy, mock } from "jest-mock-extended";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { RouterService } from "../../core/router.service";
import { RouterService } from "../../../core/router.service";
import { deepLinkGuard } from "./deep-link.guard";
@Component({
template: "",
standalone: false,
})
export class GuardedRouteTestComponent {}
@Component({
template: "",
standalone: false,
})
export class LockTestComponent {}
@Component({
template: "",
standalone: false,
})
export class RedirectTestComponent {}
@@ -96,6 +99,18 @@ describe("Deep Link Guard", () => {
expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled();
});
it('should not persist routerService.previousUrl when routerService.previousUrl contains "login-initiated"', async () => {
// Arrange
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked);
routerService.getPreviousUrl.mockReturnValue("/login-initiated");
// Act
await routerHarness.navigateByUrl("/lock-route");
// Assert
expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled();
});
// Story: User's vault times out and previousUrl is undefined
it("should not persist routerService.previousUrl when routerService.previousUrl is undefined", async () => {
// Arrange

View File

@@ -0,0 +1,99 @@
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { RouterService } from "../../../core/router.service";
/**
* Guard to persist and apply deep links to handle users who are not unlocked.
* @returns returns true. If user is not Unlocked will store URL to state for redirect once
* user is unlocked/Authenticated.
*/
export function deepLinkGuard(): CanActivateFn {
return async (route, routerState) => {
// Inject Services
const authService = inject(AuthService);
const router = inject(Router);
const routerService = inject(RouterService);
// Fetch State
const currentUrl = routerState.url;
const transientPreviousUrl = routerService.getPreviousUrl();
const authStatus = await authService.getAuthStatus();
// Evaluate State
/** before anything else, check if the user is already unlocked. */
if (authStatus === AuthenticationStatus.Unlocked) {
const persistedPreLoginUrl: string | undefined =
await routerService.getAndClearLoginRedirectUrl();
if (persistedPreLoginUrl === undefined) {
// Url us undefined, so there is nothing to navigate to.
return true;
}
// Check if the url is empty or null
if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) {
// const urlTree: string | UrlTree = persistedPreLoginUrl;
return router.navigateByUrl(persistedPreLoginUrl);
}
return true;
}
/**
* At this point the user is either `locked` or `loggedOut`, it doesn't matter.
* We opt to persist the currentUrl over the transient previousUrl. This supports
* the case where a user is locked out of their vault and they deep link from
* the "lock" page.
*
* When the user is locked out of their vault the currentUrl contains "lock" so it will
* not be persisted, the previousUrl will be persisted instead.
*/
if (isValidUrl(currentUrl)) {
await routerService.persistLoginRedirectUrl(currentUrl);
} else if (isValidUrl(transientPreviousUrl) && transientPreviousUrl !== undefined) {
await routerService.persistLoginRedirectUrl(transientPreviousUrl);
}
return true;
};
/**
* Check if the URL is valid for deep linking. A valid url is described as not including
* "lock" or "login-initiated". Valid urls are only urls that are not part of login or
* decryption flows.
* We ignore the "lock" url because standard SSO flows will send users to the lock component.
* We ignore "login-initiated" because TDE users decrypting with master passwords are
* sent to the lock component.
* @param url The URL to check.
* @returns True if the URL is valid, false otherwise.
*/
function isValidUrl(url: string | null | undefined): boolean {
if (url === undefined || url === null) {
return false;
}
if (Utils.isNullOrEmpty(url)) {
return false;
}
const lowerCaseUrl: string = url.toLocaleLowerCase();
/**
* "Login-initiated" ignored because it is used for TDE users decrypting from a new device. A TDE user
* can opt to decrypt using their password. Decrypting with a password will send the user to the lock component,
* which is protected by the deep link guard. We don't persist the `login-initiated` url because it is not a
* valid deep-link. We don't want users to be sent to the login-initiated url when they are unlocked.
* If we did navigate to the login-initiated url, the user would get caught by the TDE Guard and be sent
* to the vault and not the intended deep link.
*
* "Lock" is ignored because users cannot deep-link to the lock component if they are already unlocked.
* Users logging in with SSO will be sent to the lock component after they are authenticated with their IdP.
* SSO users would be navigated to the "lock" component loop if we persisted the "lock" url.
*/
if (lowerCaseUrl.includes("/login-initiated") || lowerCaseUrl.includes("/lock")) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,23 @@
# Deep-link Guard
The `deep-link.guard.ts` supports users who are trying to access a protected route from an unauthenticated or locked state.
This guard will persist the protected URL to session state when a user is either unauthenticated or in an encrypted/locked state. This allows users to have multiple tabs of the application running simultaneously without interfering with 'previousUrl` functionality.
Writing to session state allows users who are authenticating through SSO to be routed to their identity provider and back without losing the protected route they were trying to access in the first place.
The deep link guard will not persist Urls that are in the middle of authentication or decryption. SSO users will sometimes have to decrypt their vault after a successful authentication. This is why we do not persist the `/lock` route.
## General operation
The `deep-link.guard.ts` will always return true. The `deep-link.guard.ts` will only persist a URL if the user is in an unauthenticated or locked state. The URL cannot contain `/lock` or `/login-initiated`. The persisted URL is cleared from state when it is read.
## Routes to protect
The deep link guards should be used on routes where a user will be navigated to a protected route but may not be authenticated, decrypted, or have an account.
A use cases is the `emergency-access` route which is a link that is sent to the user's email address, and in order for them to accept the request, they must first authenticate and decrypt.
## TDE Users decrypting/unlocking with password
For TDE users opting to decrypt with a password they will be routed from the `login-initiated` to the `lock` route. We ignore the `login-initiated` route for this reason allowing TDE users who decrypt/unlock with a password to still be navigated to the initial request.

View File

@@ -7,6 +7,7 @@ import { CreatePasskeyIcon } from "@bitwarden/angular/auth/icons/create-passkey.
@Component({
selector: "app-login-via-webauthn",
templateUrl: "login-via-webauthn.component.html",
standalone: false,
})
export class LoginViaWebAuthnComponent extends BaseLoginViaWebAuthnComponent {
protected readonly Icons = { CreatePasskeyIcon, CreatePasskeyFailedIcon };

View File

@@ -14,6 +14,7 @@ import { OrganizationInvite } from "./organization-invite";
@Component({
templateUrl: "accept-organization.component.html",
standalone: false,
})
export class AcceptOrganizationComponent extends BaseAcceptComponent {
orgName$ = this.acceptOrganizationInviteService.orgName$;

View File

@@ -95,7 +95,7 @@ describe("AcceptOrganizationInviteService", () => {
encryptService.wrapDecapsulationKey.mockResolvedValue({
encryptedString: "string",
} as EncString);
encryptService.encrypt.mockResolvedValue({ encryptedString: "string" } as EncString);
encryptService.encryptString.mockResolvedValue({ encryptedString: "string" } as EncString);
const invite = createOrgInvite({ initOrganization: true });
const result = await sut.validateAndAcceptInvite(invite);

View File

@@ -145,7 +145,7 @@ export class AcceptOrganizationInviteService {
const [encryptedOrgKey, orgKey] = await this.keyService.makeOrgKey<OrgKey>();
const [orgPublicKey, encryptedOrgPrivateKey] = await this.keyService.makeKeyPair(orgKey);
const collection = await this.encryptService.encrypt(
const collection = await this.encryptService.encryptString(
this.i18nService.t("defaultCollection"),
orgKey,
);

View File

@@ -13,6 +13,7 @@ import { ToastService } from "@bitwarden/components";
@Component({
selector: "app-recover-delete",
templateUrl: "recover-delete.component.html",
standalone: false,
})
export class RecoverDeleteComponent {
protected recoverDeleteForm = new FormGroup({

View File

@@ -16,7 +16,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { I18nPipe } from "@bitwarden/ui-common";
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
import { RecoverTwoFactorComponent } from "./recover-two-factor.component";
@@ -35,7 +34,6 @@ describe("RecoverTwoFactorComponent", () => {
let mockConfigService: MockProxy<ConfigService>;
let mockLoginSuccessHandlerService: MockProxy<LoginSuccessHandlerService>;
let mockLogService: MockProxy<LogService>;
let mockNewDeviceVerificationNoticeService: MockProxy<NewDeviceVerificationNoticeService>;
beforeEach(() => {
mockRouter = mock<Router>();
@@ -48,7 +46,6 @@ describe("RecoverTwoFactorComponent", () => {
mockConfigService = mock<ConfigService>();
mockLoginSuccessHandlerService = mock<LoginSuccessHandlerService>();
mockLogService = mock<LogService>();
mockNewDeviceVerificationNoticeService = mock<NewDeviceVerificationNoticeService>();
TestBed.configureTestingModule({
declarations: [RecoverTwoFactorComponent],
@@ -63,10 +60,6 @@ describe("RecoverTwoFactorComponent", () => {
{ provide: ConfigService, useValue: mockConfigService },
{ provide: LoginSuccessHandlerService, useValue: mockLoginSuccessHandlerService },
{ provide: LogService, useValue: mockLogService },
{
provide: NewDeviceVerificationNoticeService,
useValue: mockNewDeviceVerificationNoticeService,
},
],
imports: [I18nPipe],
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
@@ -102,9 +95,6 @@ describe("RecoverTwoFactorComponent", () => {
title: "",
message: mockI18nService.t("youHaveBeenLoggedIn"),
});
expect(
mockNewDeviceVerificationNoticeService.updateNewDeviceVerificationSkipNoticeState,
).toHaveBeenCalledWith(authResult.userId, true);
expect(mockRouter.navigate).toHaveBeenCalledWith(["/settings/security/two-factor"]);
});

View File

@@ -12,11 +12,11 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ToastService } from "@bitwarden/components";
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
@Component({
selector: "app-recover-two-factor",
templateUrl: "recover-two-factor.component.html",
standalone: false,
})
export class RecoverTwoFactorComponent implements OnInit {
protected formGroup = new FormGroup({
@@ -37,7 +37,6 @@ export class RecoverTwoFactorComponent implements OnInit {
private toastService: ToastService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
private logService: LogService,
private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService,
) {}
async ngOnInit() {
@@ -82,12 +81,7 @@ export class RecoverTwoFactorComponent implements OnInit {
remember: false,
};
const credentials = new PasswordLoginCredentials(
email,
this.masterPassword,
"",
twoFactorRequest,
);
const credentials = new PasswordLoginCredentials(email, this.masterPassword, twoFactorRequest);
try {
const authResult = await this.loginStrategyService.logIn(credentials);
@@ -104,13 +98,6 @@ export class RecoverTwoFactorComponent implements OnInit {
await this.loginSuccessHandlerService.run(authResult.userId);
// Before routing, set the state to skip the new device notification. This is a temporary
// fix and will be cleaned up in PM-18485.
await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationSkipNoticeState(
authResult.userId,
true,
);
await this.router.navigate(["/settings/security/two-factor"]);
} catch (error) {
// If login errors, redirect to login page per product. Don't show error

View File

@@ -11,6 +11,7 @@ import { AcceptOrganizationInviteService } from "./organization-invite/accept-or
@Component({
selector: "app-set-password",
templateUrl: "set-password.component.html",
standalone: false,
})
export class SetPasswordComponent extends BaseSetPasswordComponent {
routerService = inject(RouterService);

View File

@@ -51,7 +51,4 @@
{{ "deleteAccount" | i18n }}
</button>
</app-danger-zone>
<ng-template #viewUserApiKeyTemplate></ng-template>
<ng-template #rotateUserApiKeyTemplate></ng-template>
</bit-container>

View File

@@ -1,32 +1,34 @@
import { Component, OnInit, OnDestroy } from "@angular/core";
import {
combineLatest,
firstValueFrom,
from,
lastValueFrom,
map,
Observable,
Subject,
takeUntil,
} from "rxjs";
import { firstValueFrom, from, lastValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
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 { DialogService } from "@bitwarden/components";
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared";
import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.component";
import { ChangeEmailComponent } from "./change-email.component";
import { DangerZoneComponent } from "./danger-zone.component";
import { DeauthorizeSessionsComponent } from "./deauthorize-sessions.component";
import { DeleteAccountDialogComponent } from "./delete-account-dialog.component";
import { ProfileComponent } from "./profile.component";
import { SetAccountVerifyDevicesDialogComponent } from "./set-account-verify-devices-dialog.component";
@Component({
selector: "app-account",
templateUrl: "account.component.html",
standalone: true,
imports: [
SharedModule,
HeaderModule,
ProfileComponent,
ChangeEmailComponent,
DangerZoneComponent,
],
})
export class AccountComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
@@ -47,10 +49,6 @@ export class AccountComponent implements OnInit, OnDestroy {
async ngOnInit() {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
const userIsManagedByOrganization$ = this.organizationService
.organizations$(userId)
.pipe(
@@ -61,25 +59,14 @@ export class AccountComponent implements OnInit, OnDestroy {
this.showChangeEmail$ = hasMasterPassword$;
this.showPurgeVault$ = combineLatest([
isAccountDeprovisioningEnabled$,
userIsManagedByOrganization$,
]).pipe(
map(
([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) =>
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
this.showPurgeVault$ = userIsManagedByOrganization$.pipe(
map((userIsManagedByOrganization) => !userIsManagedByOrganization),
);
this.showDeleteAccount$ = combineLatest([
isAccountDeprovisioningEnabled$,
userIsManagedByOrganization$,
]).pipe(
map(
([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) =>
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
this.showDeleteAccount$ = userIsManagedByOrganization$.pipe(
map((userIsManagedByOrganization) => !userIsManagedByOrganization),
);
this.accountService.accountVerifyNewDeviceLogin$
.pipe(takeUntil(this.destroy$))
.subscribe((verifyDevices) => {

View File

@@ -24,6 +24,10 @@ import {
ToastService,
} from "@bitwarden/components";
import { SharedModule } from "../../../shared";
import { SelectableAvatarComponent } from "./selectable-avatar.component";
type ChangeAvatarDialogData = {
profile: ProfileResponse;
};
@@ -31,6 +35,8 @@ type ChangeAvatarDialogData = {
@Component({
templateUrl: "change-avatar-dialog.component.html",
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [SharedModule, SelectableAvatarComponent],
})
export class ChangeAvatarDialogComponent implements OnInit, OnDestroy {
profile: ProfileResponse;

View File

@@ -33,8 +33,7 @@ describe("ChangeEmailComponent", () => {
accountService = mockAccountServiceWith("UserId" as UserId);
await TestBed.configureTestingModule({
declarations: [ChangeEmailComponent],
imports: [ReactiveFormsModule, SharedModule],
imports: [ReactiveFormsModule, SharedModule, ChangeEmailComponent],
providers: [
{ provide: AccountService, useValue: accountService },
{ provide: ApiService, useValue: apiService },

View File

@@ -14,9 +14,13 @@ import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
import { SharedModule } from "../../../shared";
@Component({
selector: "app-change-email",
templateUrl: "change-email.component.html",
standalone: true,
imports: [SharedModule],
})
export class ChangeEmailComponent implements OnInit {
tokenSent = false;

View File

@@ -1,13 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { TypographyModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
/**
* Component for the Danger Zone section of the Account/Organization Settings page.
@@ -16,15 +13,6 @@ import { TypographyModule } from "@bitwarden/components";
selector: "app-danger-zone",
templateUrl: "danger-zone.component.html",
standalone: true,
imports: [TypographyModule, JslibModule, CommonModule],
imports: [CommonModule, TypographyModule, I18nPipe],
})
export class DangerZoneComponent implements OnInit {
constructor(private configService: ConfigService) {}
accountDeprovisioningEnabled$: Observable<boolean>;
ngOnInit(): void {
this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
}
}
export class DangerZoneComponent {}

View File

@@ -1,6 +1,7 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { Verification } from "@bitwarden/common/auth/types/verification";
@@ -9,9 +10,12 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { SharedModule } from "../../../shared";
@Component({
selector: "app-deauthorize-sessions",
templateUrl: "deauthorize-sessions.component.html",
standalone: true,
imports: [SharedModule, UserVerificationFormInputComponent],
})
export class DeauthorizeSessionsComponent {
deauthForm = this.formBuilder.group({

View File

@@ -3,14 +3,19 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular";
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { Verification } from "@bitwarden/common/auth/types/verification";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
import { SharedModule } from "../../../shared";
@Component({
templateUrl: "delete-account-dialog.component.html",
standalone: true,
imports: [SharedModule, UserVerificationFormInputComponent],
})
export class DeleteAccountDialogComponent {
deleteForm = this.formBuilder.group({

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { firstValueFrom, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs";
import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -10,17 +10,21 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { DynamicAvatarComponent } from "../../../components/dynamic-avatar.component";
import { SharedModule } from "../../../shared";
import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component";
import { ChangeAvatarDialogComponent } from "./change-avatar-dialog.component";
@Component({
selector: "app-profile",
templateUrl: "profile.component.html",
standalone: true,
imports: [SharedModule, DynamicAvatarComponent, AccountFingerprintComponent],
})
export class ProfileComponent implements OnInit, OnDestroy {
loading = true;
@@ -40,7 +44,6 @@ export class ProfileComponent implements OnInit, OnDestroy {
private accountService: AccountService,
private dialogService: DialogService,
private toastService: ToastService,
private configService: ConfigService,
private organizationService: OrganizationService,
) {}
@@ -53,21 +56,12 @@ export class ProfileComponent implements OnInit, OnDestroy {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.managingOrganization$ = this.configService
.getFeatureFlag$(FeatureFlag.AccountDeprovisioning)
this.managingOrganization$ = this.organizationService
.organizations$(userId)
.pipe(
switchMap((isAccountDeprovisioningEnabled) =>
isAccountDeprovisioningEnabled
? this.organizationService
.organizations$(userId)
.pipe(
map((organizations) =>
organizations.find((o) => o.userIsManagedByOrganization === true),
),
)
: of(null),
),
map((organizations) => organizations.find((o) => o.userIsManagedByOrganization === true)),
);
this.formGroup.get("name").setValue(this.profile.name);
this.formGroup.get("email").setValue(this.profile.email);

Some files were not shown because too many files have changed in this diff Show More