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:
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -75,6 +75,7 @@ const groupsFilter = (filter: string) => {
|
||||
|
||||
@Component({
|
||||
templateUrl: "groups.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class GroupsComponent {
|
||||
loading = true;
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -33,6 +33,7 @@ type BulkConfirmDialogParams = {
|
||||
|
||||
@Component({
|
||||
templateUrl: "bulk-confirm-dialog.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||
organizationId: string;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -21,6 +21,7 @@ type BulkRemoveDialogParams = {
|
||||
|
||||
@Component({
|
||||
templateUrl: "bulk-remove-dialog.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class BulkRemoveDialogComponent extends BaseBulkRemoveComponent {
|
||||
organizationId: string;
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -35,5 +35,4 @@
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<ng-template #editTemplate></ng-template>
|
||||
</bit-container>
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
@@ -93,7 +93,4 @@
|
||||
{{ "purgeVault" | i18n }}
|
||||
</button>
|
||||
</app-danger-zone>
|
||||
|
||||
<ng-template #apiKeyTemplate></ng-template>
|
||||
<ng-template #rotateApiKeyTemplate></ng-template>
|
||||
</bit-container>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./web-change-password.service";
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./change-password";
|
||||
export * from "./login";
|
||||
export * from "./login-decryption-options";
|
||||
export * from "./webauthn-login";
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
99
apps/web/src/app/auth/guards/deep-link/deep-link.guard.ts
Normal file
99
apps/web/src/app/auth/guards/deep-link/deep-link.guard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
23
apps/web/src/app/auth/guards/deep-link/readme.md
Normal file
23
apps/web/src/app/auth/guards/deep-link/readme.md
Normal 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.
|
||||
@@ -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 };
|
||||
|
||||
@@ -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$;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -51,7 +51,4 @@
|
||||
{{ "deleteAccount" | i18n }}
|
||||
</button>
|
||||
</app-danger-zone>
|
||||
|
||||
<ng-template #viewUserApiKeyTemplate></ng-template>
|
||||
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
||||
</bit-container>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user