diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.html deleted file mode 100644 index 788b7d4ab98..00000000000 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.html +++ /dev/null @@ -1,9 +0,0 @@ - - - {{ "linkSso" | i18n }} - diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.directive.ts similarity index 86% rename from apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts rename to apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.directive.ts index 026e1f883a9..e7341c5c333 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.directive.ts @@ -1,4 +1,4 @@ -import { AfterContentInit, Component, Input } from "@angular/core"; +import { AfterContentInit, Directive, HostListener, Input } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { SsoComponent } from "@bitwarden/angular/auth/components/sso.component"; @@ -14,14 +14,19 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; -@Component({ - selector: "app-link-sso", - templateUrl: "link-sso.component.html", +@Directive({ + selector: "[app-link-sso]", }) -export class LinkSsoComponent extends SsoComponent implements AfterContentInit { +export class LinkSsoDirective extends SsoComponent implements AfterContentInit { @Input() organization: Organization; returnUri = "/settings/organizations"; + @HostListener("click", ["$event"]) + async onClick($event: MouseEvent) { + $event.preventDefault(); + await this.submit(this.returnUri, true); + } + constructor( platformUtilsService: PlatformUtilsService, i18nService: I18nService, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html index 15f3f80b3f5..f4fb2cc040a 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html @@ -1,54 +1,60 @@ - - - {{ "loading" | i18n }} - -
- - - - - - - - - -
+ + + + + + + + {{ "linkSso" | i18n }} + + + + + + + diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index 35c51643889..f8c60b26232 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -1,7 +1,6 @@ import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; -import { map, Subject, takeUntil } from "rxjs"; +import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; @@ -13,6 +12,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; @@ -25,34 +25,58 @@ import { OrganizationFilter } from "../shared/models/vault-filter.type"; templateUrl: "organization-options.component.html", }) export class OrganizationOptionsComponent implements OnInit, OnDestroy { - actionPromise: Promise; - policies: Policy[]; - loaded = false; + protected actionPromise: Promise; + protected resetPasswordPolicy?: Policy | undefined; + protected loaded = false; + protected hideMenu = false; + protected showLeaveOrgOption = false; + protected organization: OrganizationFilter; private destroy$ = new Subject(); constructor( - @Inject(OptionsInput) protected organization: OrganizationFilter, + @Inject(OptionsInput) protected organization$: Observable, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private apiService: ApiService, private syncService: SyncService, private policyService: PolicyService, - private modalService: ModalService, private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserService: OrganizationUserService, - private dialogService: DialogService + private dialogService: DialogService, + private stateService: StateService ) {} async ngOnInit() { - this.policyService.policies$ - .pipe( - map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)), - takeUntil(this.destroy$) - ) - .subscribe((policies) => { - this.policies = policies; + const resetPasswordPolicies$ = this.policyService.policies$.pipe( + map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)) + ); + + combineLatest([ + this.organization$, + resetPasswordPolicies$, + this.stateService.getAccountDecryptionOptions(), + ]) + .pipe(takeUntil(this.destroy$)) + .subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => { + this.organization = organization; + this.resetPasswordPolicy = resetPasswordPolicies.find( + (p) => p.organizationId === organization.id + ); + + // A user can leave an organization if they are NOT using TDE and Key Connector, or they have a master password. + this.showLeaveOrgOption = + (decryptionOptions.trustedDeviceOption == undefined && + decryptionOptions.keyConnectorOption == undefined) || + decryptionOptions.hasMasterPassword; + + // Hide the 3 dot menu if the user has no available actions + this.hideMenu = + !this.showLeaveOrgOption && + !this.showSsoOptions(this.organization) && + !this.allowEnrollmentChanges(this.organization); + this.loaded = true; }); } @@ -64,21 +88,16 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { allowEnrollmentChanges(org: OrganizationFilter): boolean { if (org.usePolicies && org.useResetPassword && org.hasPublicAndPrivateKeys) { - const policy = this.policies.find((p) => p.organizationId === org.id); - if (policy != null && policy.enabled) { - return org.resetPasswordEnrolled && policy.data.autoEnrollEnabled ? false : true; + if (this.resetPasswordPolicy != undefined && this.resetPasswordPolicy.enabled) { + return !(org.resetPasswordEnrolled && this.resetPasswordPolicy.data.autoEnrollEnabled); } } return false; } - showEnrolledStatus(org: Organization): boolean { - return ( - org.useResetPassword && - org.resetPasswordEnrolled && - this.policies.some((p) => p.organizationId === org.id && p.enabled) - ); + showSsoOptions(org: OrganizationFilter) { + return org.useSso && org.identifier; } async unlinkSso(org: Organization) { @@ -143,7 +162,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { null, this.i18nService.t("withdrawPasswordResetSuccess") ); - this.syncService.fullSync(true); + await this.syncService.fullSync(true); } catch (e) { this.logService.error(e); } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts index 88f004262e5..23ad5e1a4ae 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts @@ -39,7 +39,7 @@ describe("vault filter service", () => { organizations = new ReplaySubject(1); folderViews = new ReplaySubject(1); - organizationService.organizations$ = organizations; + organizationService.memberOrganizations$ = organizations; folderService.folderViews$ = folderViews; vaultFilterService = new VaultFilterService( diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index f7663ebc42c..db0dffbdd71 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -11,10 +11,7 @@ import { switchMap, } from "rxjs"; -import { - isMember, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -48,7 +45,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { ); organizationTree$: Observable> = - this.organizationService.organizations$.pipe( + this.organizationService.memberOrganizations$.pipe( switchMap((orgs) => this.buildOrganizationTree(orgs)) ); @@ -139,7 +136,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { } if (orgs) { const orgNodes: TreeNode[] = []; - orgs.filter(isMember).forEach((org) => { + orgs.forEach((org) => { const orgCopy = org as OrganizationFilter; orgCopy.icon = "bwi-business"; const node = new TreeNode(orgCopy, headNode, orgCopy.name); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html index bcd151193a5..aac08395205 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html @@ -98,16 +98,11 @@ class="org-options bwi bwi-fw bwi-exclamation-triangle text-danger" [attr.aria-label]="'organizationIsDisabled' | i18n" appA11yTitle="{{ 'organizationIsDisabled' | i18n }}" - > - - - + > + + diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts index a15521dc228..21e6ef727da 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts @@ -1,5 +1,6 @@ import { Component, InjectionToken, Injector, Input, OnDestroy, OnInit } from "@angular/core"; -import { Subject, takeUntil } from "rxjs"; +import { Observable, Subject, takeUntil } from "rxjs"; +import { map } from "rxjs/operators"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ITreeNodeObject, TreeNode } from "@bitwarden/common/models/domain/tree-node"; @@ -120,9 +121,15 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy { // here we are creating a new injector for each filter that has options createInjector(data: VaultFilterType) { let inject = this.injectors.get(data.id); + if (!inject) { + // Pass an observable to the component in order to update the component when the data changes + // as data binding does not work with dynamic components in Angular 15 (inputs are supported starting Angular 16) + const data$ = this.section.data$.pipe( + map((sectionNode) => sectionNode?.children?.find((node) => node.node.id === data.id)?.node) + ); inject = Injector.create({ - providers: [{ provide: OptionsInput, useValue: data }], + providers: [{ provide: OptionsInput, useValue: data$ }], parent: this.injector, }); this.injectors.set(data.id, inject); @@ -130,4 +137,4 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy { return inject; } } -export const OptionsInput = new InjectionToken("OptionsInput"); +export const OptionsInput = new InjectionToken>("OptionsInput"); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts b/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts index b6edd3e9838..2acab17e383 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts @@ -2,7 +2,7 @@ import { NgModule } from "@angular/core"; import { VaultFilterSharedModule } from "../../individual-vault/vault-filter/shared/vault-filter-shared.module"; -import { LinkSsoComponent } from "./components/link-sso.component"; +import { LinkSsoDirective } from "./components/link-sso.directive"; import { OrganizationOptionsComponent } from "./components/organization-options.component"; import { VaultFilterComponent } from "./components/vault-filter.component"; import { VaultFilterService as VaultFilterServiceAbstraction } from "./services/abstractions/vault-filter.service"; @@ -10,7 +10,7 @@ import { VaultFilterService } from "./services/vault-filter.service"; @NgModule({ imports: [VaultFilterSharedModule], - declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoComponent], + declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoDirective], exports: [VaultFilterComponent], providers: [ {