1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

[PS-1092] Organization Service Observables (#3462)

* Update imports

* Implement observables in a few places

* Add tests

* Get all clients working

* Use _destroy

* Address PR feedback

* Address PR feedback

* Address feedback
This commit is contained in:
Justin Baur
2022-09-27 16:25:19 -04:00
committed by GitHub
parent 2c68518f87
commit c6dccc354c
100 changed files with 1225 additions and 813 deletions

View File

@@ -16,7 +16,7 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde
import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
import { OrganizationService as OrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService as OrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
@@ -27,6 +27,7 @@ import { SendService as SendServiceAbstraction } from "@bitwarden/common/abstrac
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { SyncNotifierService as SyncNotifierServiceAbstraction } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction";
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abstractions/system.service"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abstractions/system.service";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/abstractions/token.service"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/abstractions/token.service";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
@@ -58,7 +59,7 @@ import { FolderApiService } from "@bitwarden/common/services/folder/folder-api.s
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service"; import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service"; import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { NotificationsService } from "@bitwarden/common/services/notifications.service";
import { OrganizationService } from "@bitwarden/common/services/organization.service"; import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service"; import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service";
import { PolicyApiService } from "@bitwarden/common/services/policy/policy-api.service"; import { PolicyApiService } from "@bitwarden/common/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/services/policy/policy.service"; import { PolicyService } from "@bitwarden/common/services/policy/policy.service";
@@ -68,6 +69,7 @@ import { SendService } from "@bitwarden/common/services/send.service";
import { SettingsService } from "@bitwarden/common/services/settings.service"; import { SettingsService } from "@bitwarden/common/services/settings.service";
import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service";
import { SyncService } from "@bitwarden/common/services/sync/sync.service"; import { SyncService } from "@bitwarden/common/services/sync/sync.service";
import { SyncNotifierService } from "@bitwarden/common/services/sync/syncNotifier.service";
import { SystemService } from "@bitwarden/common/services/system.service"; import { SystemService } from "@bitwarden/common/services/system.service";
import { TokenService } from "@bitwarden/common/services/token.service"; import { TokenService } from "@bitwarden/common/services/token.service";
import { TotpService } from "@bitwarden/common/services/totp.service"; import { TotpService } from "@bitwarden/common/services/totp.service";
@@ -158,6 +160,7 @@ export default class MainBackground {
folderApiService: FolderApiServiceAbstraction; folderApiService: FolderApiServiceAbstraction;
policyApiService: PolicyApiServiceAbstraction; policyApiService: PolicyApiServiceAbstraction;
userVerificationApiService: UserVerificationApiServiceAbstraction; userVerificationApiService: UserVerificationApiServiceAbstraction;
syncNotifierService: SyncNotifierServiceAbstraction;
// Passed to the popup for Safari to workaround issues with theming, downloading, etc. // Passed to the popup for Safari to workaround issues with theming, downloading, etc.
backgroundWindow = window; backgroundWindow = window;
@@ -298,7 +301,8 @@ export default class MainBackground {
this.cryptoFunctionService, this.cryptoFunctionService,
this.stateService this.stateService
); );
this.organizationService = new OrganizationService(this.stateService); this.syncNotifierService = new SyncNotifierService();
this.organizationService = new OrganizationService(this.stateService, this.syncNotifierService);
this.policyService = new PolicyService(this.stateService, this.organizationService); this.policyService = new PolicyService(this.stateService, this.organizationService);
this.policyApiService = new PolicyApiService( this.policyApiService = new PolicyApiService(
this.policyService, this.policyService,
@@ -388,9 +392,9 @@ export default class MainBackground {
this.logService, this.logService,
this.keyConnectorService, this.keyConnectorService,
this.stateService, this.stateService,
this.organizationService,
this.providerService, this.providerService,
this.folderApiService, this.folderApiService,
this.syncNotifierService,
logoutCallback logoutCallback
); );
this.eventService = new EventService( this.eventService = new EventService(

View File

@@ -1,12 +1,17 @@
import { OrganizationService as AbstractOrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService as AbstractOrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { OrganizationService } from "@bitwarden/common/services/organization.service"; import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { FactoryOptions, CachedServices, factory } from "./factory-options"; import { FactoryOptions, CachedServices, factory } from "./factory-options";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
import {
syncNotifierServiceFactory,
SyncNotifierServiceInitOptions,
} from "./sync-notifier-service.factory";
type OrganizationServiceFactoryOptions = FactoryOptions; type OrganizationServiceFactoryOptions = FactoryOptions;
export type OrganizationServiceInitOptions = OrganizationServiceFactoryOptions & export type OrganizationServiceInitOptions = OrganizationServiceFactoryOptions &
SyncNotifierServiceInitOptions &
StateServiceInitOptions; StateServiceInitOptions;
export function organizationServiceFactory( export function organizationServiceFactory(
@@ -17,6 +22,10 @@ export function organizationServiceFactory(
cache, cache,
"organizationService", "organizationService",
opts, opts,
async () => new OrganizationService(await stateServiceFactory(cache, opts)) async () =>
new OrganizationService(
await stateServiceFactory(cache, opts),
await syncNotifierServiceFactory(cache, opts)
)
); );
} }

View File

@@ -0,0 +1,17 @@
import { SyncNotifierService as AbstractSyncNotifierService } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction";
import { SyncNotifierService } from "@bitwarden/common/services/sync/syncNotifier.service";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
type SyncNotifierServiceFactoryOptions = FactoryOptions;
export type SyncNotifierServiceInitOptions = SyncNotifierServiceFactoryOptions;
export function syncNotifierServiceFactory(
cache: { syncNotifierService?: AbstractSyncNotifierService } & CachedServices,
opts: SyncNotifierServiceInitOptions
): Promise<AbstractSyncNotifierService> {
return factory(cache, "syncNotifierService", opts, () =>
Promise.resolve(new SyncNotifierService())
);
}

View File

@@ -26,7 +26,7 @@ import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector
import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";

View File

@@ -12,7 +12,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";

View File

@@ -10,7 +10,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { CipherType } from "@bitwarden/common/enums/cipherType"; import { CipherType } from "@bitwarden/common/enums/cipherType";
@@ -78,7 +78,7 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On
async ngOnInit() { async ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari(); this.searchTypeSearch = !this.platformUtilsService.isSafari();
this.showOrganizations = await this.organizationService.hasOrganizations(); this.showOrganizations = this.organizationService.hasOrganizations();
this.vaultFilter = this.vaultFilterService.getVaultFilter(); this.vaultFilter = this.vaultFilterService.getVaultFilter();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => { this.route.queryParams.pipe(first()).subscribe(async (params) => {

View File

@@ -4,7 +4,7 @@ import { Router } from "@angular/router";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
@@ -219,7 +219,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
const otherTypes: CipherType[] = []; const otherTypes: CipherType[] = [];
const dontShowCards = await this.stateService.getDontShowCardsCurrentTab(); const dontShowCards = await this.stateService.getDontShowCardsCurrentTab();
const dontShowIdentities = await this.stateService.getDontShowIdentitiesCurrentTab(); const dontShowIdentities = await this.stateService.getDontShowIdentitiesCurrentTab();
this.showOrganizations = await this.organizationService.hasOrganizations(); this.showOrganizations = this.organizationService.hasOrganizations();
if (!dontShowCards) { if (!dontShowCards) {
otherTypes.push(CipherType.Card); otherTypes.push(CipherType.Card);
} }

View File

@@ -1,70 +1,76 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header> <ng-container *ngIf="organizations$ | async as organizations">
<div class="left"> <header>
<button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button> <div class="left">
</div> <button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
<h1 class="center">
<span class="title">{{ "moveToOrganization" | i18n }}</span>
</h1>
<div class="right">
<button
type="submit"
[disabled]="form.loading || !canSave"
*ngIf="organizations && organizations.length"
>
<span [hidden]="form.loading">{{ "move" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content" *ngIf="!organizations || !organizations.length">
<div class="box-content-row padded no-hover">
{{ "noOrganizationsList" | i18n }}
</div>
</div> </div>
<div class="box-content" *ngIf="organizations && organizations.length"> <h1 class="center">
<div class="box-content-row" appBoxRow> <span class="title">{{ "moveToOrganization" | i18n }}</span>
<label for="organization">{{ "organization" | i18n }}</label> </h1>
<select <div class="right">
id="organization" <button
name="OrganizationId" type="submit"
[(ngModel)]="organizationId" [disabled]="form.loading || !canSave"
(change)="filterCollections()" *ngIf="organizations && organizations.length"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
</div>
<div class="box-footer">
{{ "moveToOrgDesc" | i18n }}
</div>
</div>
<div class="box" *ngIf="organizations && organizations.length">
<h2 class="box-header">
{{ "collections" | i18n }}
</h2>
<div class="box-content" *ngIf="!collections || !collections.length">
<div class="box-content-row padded no-hover">
{{ "noCollectionsInList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
> >
<label for="collection_{{ i }}">{{ c.name }}</label> <span [hidden]="form.loading">{{ "move" | i18n }}</span>
<input <i
id="collection_{{ i }}" class="bwi bwi-spinner bwi-lg bwi-spin"
type="checkbox" [hidden]="!form.loading"
[(ngModel)]="c.checked" aria-hidden="true"
name="Collection[{{ i }}].Checked" ></i>
/> </button>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content" *ngIf="!organizations || !organizations.length">
<div class="box-content-row padded no-hover">
{{ "noOrganizationsList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="organizations && organizations.length">
<div class="box-content-row" appBoxRow>
<label for="organization">{{ "organization" | i18n }}</label>
<select
id="organization"
name="OrganizationId"
[(ngModel)]="organizationId"
(change)="filterCollections()"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
</div>
<div class="box-footer">
{{ "moveToOrgDesc" | i18n }}
</div> </div>
</div> </div>
</div> <div class="box" *ngIf="organizations && organizations.length">
</main> <h2 class="box-header">
{{ "collections" | i18n }}
</h2>
<div class="box-content" *ngIf="!collections || !collections.length">
<div class="box-content-row padded no-hover">
{{ "noCollectionsInList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
>
<label for="collection_{{ i }}">{{ c.name }}</label>
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
/>
</div>
</div>
</div>
</main>
</ng-container>
</form> </form>

View File

@@ -7,7 +7,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@Component({ @Component({

View File

@@ -1,71 +1,70 @@
<div class="content org-filter-content" *ngIf="loaded && show"> <ng-container *ngIf="loaded && organizations$ | async as organizations">
<button <div class="content org-filter-content" *ngIf="loaded && shouldShow(organizations)">
#toggleVaults <ng-container *ngIf="selectedVault$ | async as vaultFilterDisplay">
class="org-filter"
(click)="openOverlay()"
aria-haspopup="menu"
aria-controls="cdk-overlay-container"
[attr.aria-expanded]="isOpen"
[attr.aria-label]="vaultFilterDisplay"
>
{{ vaultFilterDisplay | ellipsis: 45 }}&nbsp;
<i
class="bwi bwi-sm"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-chevron-up': isOpen }"
></i>
</button>
<ng-template class="vault-select-container" #vaultSelectorTemplate>
<div
class="vault-select"
[@transformPanel]="'open'"
cdkTrapFocus
cdkTrapFocusAutoCapture
role="dialog"
aria-modal="true"
>
<button <button
appStopClick #toggleVaults
(click)="selectAllVaults()" class="org-filter"
[ngClass]="{ active: !myVaultOnly && !selectOrganizationId }" (click)="openOverlay()"
appA11yTitle="{{ 'vault' | i18n }}: {{ 'allVaults' | i18n }}" aria-haspopup="menu"
> aria-controls="cdk-overlay-container"
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i> [attr.aria-expanded]="isOpen"
&nbsp;{{ "allVaults" | i18n }} [attr.aria-label]="vaultFilterDisplay"
</button>
<button
*ngIf="!enforcePersonalOwnwership"
appStopClick
(click)="selectMyVault()"
appA11yTitle="{{ 'vault' | i18n }}: {{ 'myVault' | i18n }}"
>
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
&nbsp;{{ "myVault" | i18n }}
</button>
<button
*ngFor="let organization of organizations"
appStopClick
(click)="selectOrganization(organization)"
appA11yTitle="{{ 'vault' | i18n }}: {{ organization.name }}"
> >
{{ vaultFilterDisplay | ellipsis: 45 }}&nbsp;
<i <i
*ngIf="organization.planProductType !== 1" class="bwi bwi-sm"
class="bwi bwi-fw bwi-business"
aria-hidden="true" aria-hidden="true"
></i> [ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-chevron-up': isOpen }"
<i
*ngIf="organization.planProductType === 1"
class="bwi bwi-fw bwi-family"
aria-hidden="true"
></i>
<span>&nbsp;{{ organization.name | ellipsis: (organization.enabled ? 21 : 18):true }}</span>
<i
*ngIf="!organization.enabled"
class="bwi bwi-fw bwi-exclamation-triangle text-danger"
aria-label="{{ 'organizationIsDisabled' | i18n }}"
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
></i> ></i>
</button> </button>
</div> </ng-container>
</ng-template> <ng-template class="vault-select-container" #vaultSelectorTemplate>
</div> <div
class="vault-select"
[@transformPanel]="'open'"
cdkTrapFocus
cdkTrapFocusAutoCapture
role="dialog"
aria-modal="true"
>
<button
appStopClick
(click)="selectAllVaults()"
[ngClass]="{ active: !myVaultOnly && !selectOrganizationId }"
>
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i>
&nbsp;{{ "allVaults" | i18n }}
</button>
<button *ngIf="!enforcePersonalOwnwership" appStopClick (click)="selectMyVault()">
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
&nbsp;{{ "myVault" | i18n }}
</button>
<button
*ngFor="let organization of organizations"
appStopClick
(click)="selectOrganization(organization)"
>
<i
*ngIf="organization.planProductType !== 1"
class="bwi bwi-fw bwi-business"
aria-hidden="true"
></i>
<i
*ngIf="organization.planProductType === 1"
class="bwi bwi-fw bwi-family"
aria-hidden="true"
></i>
<span
>&nbsp;{{ organization.name | ellipsis: (organization.enabled ? 21 : 18):true }}</span
>
<i
*ngIf="!organization.enabled"
class="bwi bwi-fw bwi-exclamation-triangle text-danger"
aria-label="{{ 'organizationIsDisabled' | i18n }}"
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
></i>
</button>
</div>
</ng-template>
</div>
</ng-container>

View File

@@ -5,19 +5,19 @@ import {
Component, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
NgZone,
OnInit, OnInit,
Output, Output,
TemplateRef, TemplateRef,
ViewChild, ViewChild,
ViewContainerRef, ViewContainerRef,
HostListener, HostListener,
OnDestroy,
} from "@angular/core"; } from "@angular/core";
import { merge } from "rxjs"; import { BehaviorSubject, concatMap, map, merge, Observable, Subject, takeUntil } from "rxjs";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
@@ -47,20 +47,22 @@ import { VaultFilterService } from "../../services/vaultFilter.service";
]), ]),
], ],
}) })
// eslint-disable-next-line rxjs-angular/prefer-takeuntil export class VaultSelectComponent implements OnInit, OnDestroy {
export class VaultSelectComponent implements OnInit {
@Output() onVaultSelectionChanged = new EventEmitter(); @Output() onVaultSelectionChanged = new EventEmitter();
@ViewChild("toggleVaults", { read: ElementRef }) @ViewChild("toggleVaults", { read: ElementRef })
buttonRef: ElementRef<HTMLButtonElement>; buttonRef: ElementRef<HTMLButtonElement>;
@ViewChild("vaultSelectorTemplate", { read: TemplateRef }) templateRef: TemplateRef<HTMLElement>; @ViewChild("vaultSelectorTemplate", { read: TemplateRef }) templateRef: TemplateRef<HTMLElement>;
private _selectedVault = new BehaviorSubject<string>(null);
isOpen = false; isOpen = false;
loaded = false; loaded = false;
organizations: Organization[]; organizations$: Observable<Organization[]>;
selectedVault$: Observable<string> = this._selectedVault.asObservable();
vaultFilter: VaultFilter = new VaultFilter(); vaultFilter: VaultFilter = new VaultFilter();
vaultFilterDisplay = ""; enforcePersonalOwnership = false;
enforcePersonalOwnwership = false;
overlayPostition: ConnectedPosition[] = [ overlayPostition: ConnectedPosition[] = [
{ {
originX: "start", originX: "start",
@@ -71,22 +73,22 @@ export class VaultSelectComponent implements OnInit {
]; ];
private overlayRef: OverlayRef; private overlayRef: OverlayRef;
private _destroy = new Subject<void>();
get show() { shouldShow(organizations: Organization[]): boolean {
return ( return (
(this.organizations.length > 0 && !this.enforcePersonalOwnwership) || (organizations.length > 0 && !this.enforcePersonalOwnership) ||
(this.organizations.length > 1 && this.enforcePersonalOwnwership) (organizations.length > 1 && this.enforcePersonalOwnership)
); );
} }
constructor( constructor(
private vaultFilterService: VaultFilterService, private vaultFilterService: VaultFilterService,
private i18nService: I18nService, private i18nService: I18nService,
private ngZone: NgZone,
private broadcasterService: BroadcasterService,
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private platformUtilsService: PlatformUtilsService private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService
) {} ) {}
@HostListener("document:keydown.escape", ["$event"]) @HostListener("document:keydown.escape", ["$event"])
@@ -98,46 +100,45 @@ export class VaultSelectComponent implements OnInit {
} }
async ngOnInit() { async ngOnInit() {
await this.load(); this.organizations$ = this.organizationService.organizations$
this.broadcasterService.subscribe(this.constructor.name, (message: any) => { .pipe(takeUntil(this._destroy))
this.ngZone.run(async () => { .pipe(map((orgs) => orgs.sort((a, b) => a.name.localeCompare(b.name))));
switch (message.command) {
case "syncCompleted": this.organizations$
await this.load(); .pipe(
break; concatMap(async (organizations) => {
default: this.enforcePersonalOwnership =
break; await this.vaultFilterService.checkForPersonalOwnershipPolicy();
}
}); if (this.shouldShow(organizations)) {
}); if (this.enforcePersonalOwnership && !this.vaultFilter.myVaultOnly) {
const firstOrganization = organizations[0];
this._selectedVault.next(firstOrganization.name);
this.vaultFilterService.setVaultFilter(firstOrganization.id);
this.vaultFilter.selectedOrganizationId = firstOrganization.id;
} else if (this.vaultFilter.myVaultOnly) {
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault));
} else if (this.vaultFilter.selectedOrganizationId != null) {
const selectedOrganization = organizations.find(
(o) => o.id === this.vaultFilter.selectedOrganizationId
);
this._selectedVault.next(selectedOrganization.name);
} else {
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults));
}
}
})
)
.pipe(takeUntil(this._destroy))
.subscribe();
this.loaded = true;
} }
async load() { ngOnDestroy(): void {
this.vaultFilter = this.vaultFilterService.getVaultFilter(); this._destroy.next();
this.organizations = (await this.vaultFilterService.buildOrganizations()).sort((a, b) => this._destroy.complete();
a.name.localeCompare(b.name) this._selectedVault.complete();
);
this.enforcePersonalOwnwership =
await this.vaultFilterService.checkForPersonalOwnershipPolicy();
if (this.show) {
if (this.enforcePersonalOwnwership && !this.vaultFilter.myVaultOnly) {
this.vaultFilterService.setVaultFilter(this.organizations[0].id);
this.vaultFilter.selectedOrganizationId = this.organizations[0].id;
this.vaultFilterDisplay = this.organizations.find(
(o) => o.id === this.vaultFilter.selectedOrganizationId
).name;
} else if (this.vaultFilter.myVaultOnly) {
this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.myVault);
} else if (this.vaultFilter.selectedOrganizationId != null) {
this.vaultFilterDisplay = this.organizations.find(
(o) => o.id === this.vaultFilter.selectedOrganizationId
).name;
} else {
this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.allVaults);
}
}
this.loaded = true;
} }
openOverlay() { openOverlay() {
@@ -191,20 +192,20 @@ export class VaultSelectComponent implements OnInit {
this.i18nService.t("disabledOrganizationFilterError") this.i18nService.t("disabledOrganizationFilterError")
); );
} else { } else {
this.vaultFilterDisplay = organization.name; this._selectedVault.next(organization.name);
this.vaultFilterService.setVaultFilter(organization.id); this.vaultFilterService.setVaultFilter(organization.id);
this.onVaultSelectionChanged.emit(); this.onVaultSelectionChanged.emit();
this.close(); this.close();
} }
} }
selectAllVaults() { selectAllVaults() {
this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.allVaults); this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults));
this.vaultFilterService.setVaultFilter(this.vaultFilterService.allVaults); this.vaultFilterService.setVaultFilter(this.vaultFilterService.allVaults);
this.onVaultSelectionChanged.emit(); this.onVaultSelectionChanged.emit();
this.close(); this.close();
} }
selectMyVault() { selectMyVault() {
this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.myVault); this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault));
this.vaultFilterService.setVaultFilter(this.vaultFilterService.myVault); this.vaultFilterService.setVaultFilter(this.vaultFilterService.myVault);
this.onVaultSelectionChanged.emit(); this.onVaultSelectionChanged.emit();
this.close(); this.close();

View File

@@ -3,7 +3,7 @@ import { VaultFilterService as BaseVaultFilterService } from "@bitwarden/angular
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { CipherView } from "@bitwarden/common/models/view/cipherView"; import { CipherView } from "@bitwarden/common/models/view/cipherView";

View File

@@ -30,8 +30,8 @@ import { ImportService } from "@bitwarden/common/services/import.service";
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service"; import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service"; import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { NoopMessagingService } from "@bitwarden/common/services/noopMessaging.service"; import { NoopMessagingService } from "@bitwarden/common/services/noopMessaging.service";
import { OrganizationService } from "@bitwarden/common/services/organization.service";
import { OrganizationApiService } from "@bitwarden/common/services/organization/organization-api.service"; import { OrganizationApiService } from "@bitwarden/common/services/organization/organization-api.service";
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service"; import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service";
import { PolicyService } from "@bitwarden/common/services/policy/policy.service"; import { PolicyService } from "@bitwarden/common/services/policy/policy.service";
import { ProviderService } from "@bitwarden/common/services/provider.service"; import { ProviderService } from "@bitwarden/common/services/provider.service";
@@ -41,6 +41,7 @@ import { SettingsService } from "@bitwarden/common/services/settings.service";
import { StateService } from "@bitwarden/common/services/state.service"; import { StateService } from "@bitwarden/common/services/state.service";
import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service";
import { SyncService } from "@bitwarden/common/services/sync/sync.service"; import { SyncService } from "@bitwarden/common/services/sync/sync.service";
import { SyncNotifierService } from "@bitwarden/common/services/sync/syncNotifier.service";
import { TokenService } from "@bitwarden/common/services/token.service"; import { TokenService } from "@bitwarden/common/services/token.service";
import { TotpService } from "@bitwarden/common/services/totp.service"; import { TotpService } from "@bitwarden/common/services/totp.service";
import { TwoFactorService } from "@bitwarden/common/services/twoFactor.service"; import { TwoFactorService } from "@bitwarden/common/services/twoFactor.service";
@@ -113,6 +114,7 @@ export class Main {
folderApiService: FolderApiService; folderApiService: FolderApiService;
userVerificationApiService: UserVerificationApiService; userVerificationApiService: UserVerificationApiService;
organizationApiService: OrganizationApiServiceAbstraction; organizationApiService: OrganizationApiServiceAbstraction;
syncNotifierService: SyncNotifierService;
constructor() { constructor() {
let p = null; let p = null;
@@ -191,7 +193,9 @@ export class Main {
customUserAgent customUserAgent
); );
this.organizationApiService = new OrganizationApiService(this.apiService); this.syncNotifierService = new SyncNotifierService();
this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService);
this.containerService = new ContainerService(this.cryptoService, this.encryptService); this.containerService = new ContainerService(this.cryptoService, this.encryptService);
@@ -231,7 +235,7 @@ export class Main {
this.providerService = new ProviderService(this.stateService); this.providerService = new ProviderService(this.stateService);
this.organizationService = new OrganizationService(this.stateService); this.organizationService = new OrganizationService(this.stateService, this.syncNotifierService);
this.policyService = new PolicyService(this.stateService, this.organizationService); this.policyService = new PolicyService(this.stateService, this.organizationService);
@@ -311,9 +315,9 @@ export class Main {
this.logService, this.logService,
this.keyConnectorService, this.keyConnectorService,
this.stateService, this.stateService,
this.organizationService,
this.providerService, this.providerService,
this.folderApiService, this.folderApiService,
this.syncNotifierService,
async (expired: boolean) => await this.logout() async (expired: boolean) => await this.logout()
); );

View File

@@ -74,7 +74,6 @@ export class ConvertToKeyConnectorCommand {
} else if (answer.convert === "leave") { } else if (answer.convert === "leave") {
await this.organizationApiService.leave(organization.id); await this.organizationApiService.leave(organization.id);
await this.keyConnectorService.removeConvertAccountRequired(); await this.keyConnectorService.removeConvertAccountRequired();
await this.syncService.fullSync(true);
return Response.success(); return Response.success();
} else { } else {
await this.logout(); await this.logout();

View File

@@ -4,7 +4,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { TotpService } from "@bitwarden/common/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/abstractions/totp.service";

View File

@@ -2,7 +2,7 @@ import * as program from "commander";
import * as inquirer from "inquirer"; import * as inquirer from "inquirer";
import { ImportService } from "@bitwarden/common/abstractions/import.service"; import { ImportService } from "@bitwarden/common/abstractions/import.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { ImportType } from "@bitwarden/common/enums/importOptions"; import { ImportType } from "@bitwarden/common/enums/importOptions";
import { Importer } from "@bitwarden/common/importers/importer"; import { Importer } from "@bitwarden/common/importers/importer";
import { Response } from "@bitwarden/node/cli/models/response"; import { Response } from "@bitwarden/node/cli/models/response";

View File

@@ -2,7 +2,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { Utils } from "@bitwarden/common/misc/utils"; import { Utils } from "@bitwarden/common/misc/utils";
import { CollectionData } from "@bitwarden/common/models/data/collectionData"; import { CollectionData } from "@bitwarden/common/models/data/collectionData";

View File

@@ -11,7 +11,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";

View File

@@ -1,78 +1,80 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="moveToOrgTitle"> <div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="moveToOrgTitle">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-body"> <ng-container *ngIf="organizations$ | async as organizations">
<div class="box"> <div class="modal-body">
<h1 class="box-header" id="moveToOrgTitle"> <div class="box">
{{ "moveToOrganization" | i18n }} <h1 class="box-header" id="moveToOrgTitle">
</h1> {{ "moveToOrganization" | i18n }}
<div class="box-content" *ngIf="!organizations || !organizations.length"> </h1>
<div class="box-content-row"> <div class="box-content" *ngIf="!organizations || !organizations.length">
{{ "noOrganizationsList" | i18n }} <div class="box-content-row">
{{ "noOrganizationsList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="organizations && organizations.length">
<div class="box-content-row" appBoxRow>
<label for="organization">{{ "organization" | i18n }}</label>
<select
id="organization"
name="OrganizationId"
[(ngModel)]="organizationId"
(change)="filterCollections()"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
</div>
<div class="box-footer">
{{ "moveToOrgDesc" | i18n }}
</div> </div>
</div> </div>
<div class="box-content" *ngIf="organizations && organizations.length"> <div class="box" *ngIf="organizations && organizations.length">
<div class="box-content-row" appBoxRow> <h2 class="box-header">
<label for="organization">{{ "organization" | i18n }}</label> {{ "collections" | i18n }}
<select </h2>
id="organization" <div class="box-content" *ngIf="!collections || !collections.length">
name="OrganizationId" {{ "noCollectionsInList" | i18n }}
[(ngModel)]="organizationId" </div>
(change)="filterCollections()" <div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
> >
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option> <label for="collection_{{ i }}">{{ c.name }}</label>
</select> <input
</div> id="collection_{{ i }}"
</div> type="checkbox"
<div class="box-footer"> [(ngModel)]="c.checked"
{{ "moveToOrgDesc" | i18n }} name="Collection[{{ i }}].Checked"
</div> />
</div> </div>
<div class="box" *ngIf="organizations && organizations.length">
<h2 class="box-header">
{{ "collections" | i18n }}
</h2>
<div class="box-content" *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
>
<label for="collection_{{ i }}">{{ c.name }}</label>
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
/>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="modal-footer">
<div class="modal-footer"> <button
<button type="submit"
type="submit" class="primary"
class="primary" appA11yTitle="{{ 'save' | i18n }}"
appA11yTitle="{{ 'save' | i18n }}" [disabled]="form.loading || !canSave"
[disabled]="form.loading || !canSave" *ngIf="organizations && organizations.length"
*ngIf="organizations && organizations.length" >
> <i
<i class="bwi bwi-save-changes bwi-lg bwi-fw"
class="bwi bwi-save-changes bwi-lg bwi-fw" [hidden]="form.loading"
[hidden]="form.loading" aria-hidden="true"
aria-hidden="true" ></i>
></i> <i
<i class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw" [hidden]="!form.loading"
[hidden]="!form.loading" aria-hidden="true"
aria-hidden="true" ></i>
></i> </button>
</button> <button type="button" data-dismiss="modal">{{ "cancel" | i18n }}</button>
<button type="button" data-dismiss="modal">{{ "cancel" | i18n }}</button> </div>
</div> </ng-container>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@Component({ @Component({

View File

@@ -7,8 +7,8 @@ import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";

View File

@@ -126,7 +126,12 @@ export abstract class BasePeopleComponent<
} }
this.allUsers = response.data != null && response.data.length > 0 ? response.data : []; this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
this.allUsers.sort(Utils.getSortFunction(this.i18nService, "email")); this.allUsers.sort(
Utils.getSortFunction<ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse>(
this.i18nService,
"email"
)
);
this.allUsers.forEach((u) => { this.allUsers.forEach((u) => {
if (!this.statusMap.has(u.status)) { if (!this.statusMap.has(u.status)) {
this.statusMap.set(u.status, [u]); this.statusMap.set(u.status, [u]);

View File

@@ -45,7 +45,11 @@
<bit-menu #orgPickerMenu> <bit-menu #orgPickerMenu>
<ul aria-labelledby="pickerButton" class="tw-m-0 tw-p-0"> <ul aria-labelledby="pickerButton" class="tw-m-0 tw-p-0">
<li *ngFor="let org of organizations" class="tw-flex tw-list-none tw-flex-col" role="none"> <li
*ngFor="let org of organizations$ | async"
class="tw-flex tw-list-none tw-flex-col"
role="none"
>
<a bitMenuItem [routerLink]="['/organizations', org.id]"> <a bitMenuItem [routerLink]="['/organizations', org.id]">
<i <i
class="bwi bwi-check mr-2" class="bwi bwi-check mr-2"

View File

@@ -1,12 +1,13 @@
import { Component, Input, OnInit } from "@angular/core"; import { Component, Input, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import {
import { Utils } from "@bitwarden/common/misc/utils"; canAccessAdmin,
OrganizationService,
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
import { canAccessOrgAdmin } from "../organizations/navigation-permissions";
@Component({ @Component({
selector: "app-organization-switcher", selector: "app-organization-switcher",
templateUrl: "organization-switcher.component.html", templateUrl: "organization-switcher.component.html",
@@ -15,19 +16,14 @@ export class OrganizationSwitcherComponent implements OnInit {
constructor(private organizationService: OrganizationService, private i18nService: I18nService) {} constructor(private organizationService: OrganizationService, private i18nService: I18nService) {}
@Input() activeOrganization: Organization = null; @Input() activeOrganization: Organization = null;
organizations: Organization[] = []; organizations$: Observable<Organization[]>;
loaded = false; loaded = false;
async ngOnInit() { async ngOnInit() {
await this.load(); this.organizations$ = this.organizationService.organizations$.pipe(
} canAccessAdmin(this.i18nService)
);
async load() {
const orgs = await this.organizationService.getAll();
this.organizations = orgs
.filter(canAccessOrgAdmin)
.sort(Utils.getSortFunction(this.i18nService, "name"));
this.loaded = true; this.loaded = true;
} }

View File

@@ -17,8 +17,12 @@
<li class="nav-item" routerLinkActive="active"> <li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="/reports">{{ "reports" | i18n }}</a> <a class="nav-link" routerLink="/reports">{{ "reports" | i18n }}</a>
</li> </li>
<li *ngIf="organizations.length >= 1" class="nav-item" routerLinkActive="active"> <li
<a class="nav-link" [routerLink]="['/organizations', organizations[0].id]">{{ *ngIf="(organizations$ | async)?.length >= 1"
class="nav-item"
routerLinkActive="active"
>
<a class="nav-link" [routerLink]="['/organizations', (organizations$ | async)[0].id]">{{
"organizations" | i18n "organizations" | i18n
}}</a> }}</a>
</li> </li>

View File

@@ -1,19 +1,19 @@
import { Component, NgZone, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { Observable } from "rxjs";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import {
canAccessAdmin,
OrganizationService,
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ProviderService } from "@bitwarden/common/abstractions/provider.service"; import { ProviderService } from "@bitwarden/common/abstractions/provider.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { TokenService } from "@bitwarden/common/abstractions/token.service"; import { TokenService } from "@bitwarden/common/abstractions/token.service";
import { Utils } from "@bitwarden/common/misc/utils";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
import { Provider } from "@bitwarden/common/models/domain/provider"; import { Provider } from "@bitwarden/common/models/domain/provider";
import { canAccessOrgAdmin } from "../organizations/navigation-permissions";
@Component({ @Component({
selector: "app-navbar", selector: "app-navbar",
templateUrl: "navbar.component.html", templateUrl: "navbar.component.html",
@@ -24,7 +24,7 @@ export class NavbarComponent implements OnInit {
name: string; name: string;
email: string; email: string;
providers: Provider[] = []; providers: Provider[] = [];
organizations: Organization[] = []; organizations$: Observable<Organization[]>;
constructor( constructor(
private messagingService: MessagingService, private messagingService: MessagingService,
@@ -33,9 +33,7 @@ export class NavbarComponent implements OnInit {
private providerService: ProviderService, private providerService: ProviderService,
private syncService: SyncService, private syncService: SyncService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private i18nService: I18nService, private i18nService: I18nService
private broadcasterService: BroadcasterService,
private ngZone: NgZone
) { ) {
this.selfHosted = this.platformUtilsService.isSelfHost(); this.selfHosted = this.platformUtilsService.isSelfHost();
} }
@@ -53,24 +51,9 @@ export class NavbarComponent implements OnInit {
} }
this.providers = await this.providerService.getAll(); this.providers = await this.providerService.getAll();
this.organizations = await this.buildOrganizations(); this.organizations$ = this.organizationService.organizations$.pipe(
canAccessAdmin(this.i18nService)
this.broadcasterService.subscribe(this.constructor.name, async (message: any) => { );
this.ngZone.run(async () => {
switch (message.command) {
case "organizationCreated":
if (this.organizations.length < 1) {
this.organizations = await this.buildOrganizations();
}
break;
}
});
});
}
async buildOrganizations() {
const allOrgs = await this.organizationService.getAll();
return allOrgs.filter(canAccessOrgAdmin).sort(Utils.getSortFunction(this.i18nService, "name"));
} }
lock() { lock() {

View File

@@ -7,7 +7,7 @@ import {
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
@@ -57,7 +57,7 @@ describe("Organization Permissions Guard", () => {
}); });
it("blocks navigation if organization does not exist", async () => { it("blocks navigation if organization does not exist", async () => {
organizationService.get.mockResolvedValue(null); organizationService.get.mockReturnValue(null);
const actual = await organizationPermissionsGuard.canActivate(route, state); const actual = await organizationPermissionsGuard.canActivate(route, state);
@@ -66,7 +66,7 @@ describe("Organization Permissions Guard", () => {
it("permits navigation if no permissions are specified", async () => { it("permits navigation if no permissions are specified", async () => {
const org = orgFactory(); const org = orgFactory();
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockReturnValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state); const actual = await organizationPermissionsGuard.canActivate(route, state);
@@ -81,7 +81,7 @@ describe("Organization Permissions Guard", () => {
}; };
const org = orgFactory(); const org = orgFactory();
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockReturnValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state); const actual = await organizationPermissionsGuard.canActivate(route, state);
@@ -104,7 +104,7 @@ describe("Organization Permissions Guard", () => {
}); });
const org = orgFactory(); const org = orgFactory();
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockReturnValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state); const actual = await organizationPermissionsGuard.canActivate(route, state);
@@ -124,7 +124,7 @@ describe("Organization Permissions Guard", () => {
}), }),
}); });
const org = orgFactory(); const org = orgFactory();
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockReturnValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state); const actual = await organizationPermissionsGuard.canActivate(route, state);
@@ -141,7 +141,7 @@ describe("Organization Permissions Guard", () => {
type: OrganizationUserType.Admin, type: OrganizationUserType.Admin,
enabled: false, enabled: false,
}); });
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockReturnValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state); const actual = await organizationPermissionsGuard.canActivate(route, state);
@@ -153,7 +153,7 @@ describe("Organization Permissions Guard", () => {
type: OrganizationUserType.Owner, type: OrganizationUserType.Owner,
enabled: false, enabled: false,
}); });
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockReturnValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state); const actual = await organizationPermissionsGuard.canActivate(route, state);

View File

@@ -2,13 +2,14 @@ import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import {
canAccessOrgAdmin,
OrganizationService,
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
import { canAccessOrgAdmin } from "../navigation-permissions";
@Injectable({ @Injectable({
providedIn: "root", providedIn: "root",
}) })
@@ -27,7 +28,7 @@ export class OrganizationPermissionsGuard implements CanActivate {
await this.syncService.fullSync(false); await this.syncService.fullSync(false);
} }
const org = await this.organizationService.get(route.params.organizationId); const org = this.organizationService.get(route.params.organizationId);
if (org == null) { if (org == null) {
return this.router.createUrlTree(["/"]); return this.router.createUrlTree(["/"]);
} }

View File

@@ -1,39 +1,49 @@
<app-navbar></app-navbar> <app-navbar></app-navbar>
<div class="org-nav" *ngIf="organization"> <ng-container *ngIf="organization$ | async as organization">
<div class="container d-flex"> <div class="org-nav" *ngIf="organization">
<div class="d-flex flex-column"> <div class="container d-flex">
<app-organization-switcher <div class="d-flex flex-column">
class="my-auto pl-1" <app-organization-switcher
[activeOrganization]="organization" class="my-auto pl-1"
></app-organization-switcher> [activeOrganization]="organization"
<ul class="nav nav-tabs"> ></app-organization-switcher>
<li class="nav-item"> <ul class="nav nav-tabs">
<a class="nav-link" routerLink="vault" routerLinkActive="active"> <li class="nav-item">
<i class="bwi bwi-lock" aria-hidden="true"></i> <a class="nav-link" routerLink="vault" routerLinkActive="active">
{{ "vault" | i18n }} <i class="bwi bwi-lock" aria-hidden="true"></i>
</a> {{ "vault" | i18n }}
</li> </a>
<li class="nav-item" *ngIf="showManageTab"> </li>
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active"> <li class="nav-item" *ngIf="canShowManageTab(organization)">
<i class="bwi bwi-sliders" aria-hidden="true"></i> <a
{{ "manage" | i18n }} class="nav-link"
</a> [routerLink]="getManageRoute(organization)"
</li> routerLinkActive="active"
<li class="nav-item" *ngIf="showToolsTab"> >
<a class="nav-link" [routerLink]="toolsRoute" routerLinkActive="active"> <i class="bwi bwi-sliders" aria-hidden="true"></i>
<i class="bwi bwi-wrench" aria-hidden="true"></i> {{ "manage" | i18n }}
{{ "tools" | i18n }} </a>
</a> </li>
</li> <li class="nav-item" *ngIf="canShowToolsTab(organization)">
<li class="nav-item" *ngIf="showSettingsTab"> <a
<a class="nav-link" routerLink="settings" routerLinkActive="active"> class="nav-link"
<i class="bwi bwi-cogs" aria-hidden="true"></i> [routerLink]="getToolsRoute(organization)"
{{ "settings" | i18n }} routerLinkActive="active"
</a> >
</li> <i class="bwi bwi-wrench" aria-hidden="true"></i>
</ul> {{ "tools" | i18n }}
</a>
</li>
<li class="nav-item" *ngIf="canShowSettingsTab(organization)">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="bwi bwi-cogs" aria-hidden="true"></i>
{{ "settings" | i18n }}
</a>
</li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </ng-container>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<app-footer></app-footer> <app-footer></app-footer>

View File

@@ -1,100 +1,86 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { map, mergeMap, Observable, Subject, takeUntil } from "rxjs";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { import {
OrganizationService,
getOrganizationById,
canAccessManageTab, canAccessManageTab,
canAccessSettingsTab, canAccessSettingsTab,
canAccessToolsTab, canAccessToolsTab,
} from "../navigation-permissions"; } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/models/domain/organization";
const BroadcasterSubscriptionId = "OrganizationLayoutComponent";
@Component({ @Component({
selector: "app-organization-layout", selector: "app-organization-layout",
templateUrl: "organization-layout.component.html", templateUrl: "organization-layout.component.html",
}) })
export class OrganizationLayoutComponent implements OnInit, OnDestroy { export class OrganizationLayoutComponent implements OnInit, OnDestroy {
organization: Organization; organization$: Observable<Organization>;
businessTokenPromise: Promise<any>; businessTokenPromise: Promise<void>;
private organizationId: string;
constructor( private _destroy = new Subject<void>();
private route: ActivatedRoute,
private organizationService: OrganizationService, constructor(private route: ActivatedRoute, private organizationService: OrganizationService) {}
private broadcasterService: BroadcasterService,
private ngZone: NgZone
) {}
ngOnInit() { ngOnInit() {
document.body.classList.remove("layout_frontend"); document.body.classList.remove("layout_frontend");
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.params.subscribe(async (params: any) => { this.organization$ = this.route.params
this.organizationId = params.organizationId; .pipe(takeUntil(this._destroy))
await this.load(); .pipe<string>(map((p) => p.organizationId))
}); .pipe(
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { mergeMap((id) => {
this.ngZone.run(async () => { return this.organizationService.organizations$
switch (message.command) { .pipe(takeUntil(this._destroy))
case "updatedOrgLicense": .pipe(getOrganizationById(id));
await this.load(); })
break; );
}
});
});
} }
ngOnDestroy() { ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); this._destroy.next();
this._destroy.complete();
} }
async load() { canShowManageTab(organization: Organization): boolean {
this.organization = await this.organizationService.get(this.organizationId); return canAccessManageTab(organization);
} }
get showManageTab(): boolean { canShowToolsTab(organization: Organization): boolean {
return canAccessManageTab(this.organization); return canAccessToolsTab(organization);
} }
get showToolsTab(): boolean { canShowSettingsTab(organization: Organization): boolean {
return canAccessToolsTab(this.organization); return canAccessSettingsTab(organization);
} }
get showSettingsTab(): boolean { getToolsRoute(organization: Organization): string {
return canAccessSettingsTab(this.organization); return organization.canAccessImportExport ? "tools/import" : "tools/exposed-passwords-report";
} }
get toolsRoute(): string { getManageRoute(organization: Organization): string {
return this.organization.canAccessImportExport
? "tools/import"
: "tools/exposed-passwords-report";
}
get manageRoute(): string {
let route: string; let route: string;
switch (true) { switch (true) {
case this.organization.canManageUsers: case organization.canManageUsers:
route = "manage/people"; route = "manage/people";
break; break;
case this.organization.canViewAssignedCollections || this.organization.canViewAllCollections: case organization.canViewAssignedCollections || organization.canViewAllCollections:
route = "manage/collections"; route = "manage/collections";
break; break;
case this.organization.canManageGroups: case organization.canManageGroups:
route = "manage/groups"; route = "manage/groups";
break; break;
case this.organization.canManagePolicies: case organization.canManagePolicies:
route = "manage/policies"; route = "manage/policies";
break; break;
case this.organization.canManageSso: case organization.canManageSso:
route = "manage/sso"; route = "manage/sso";
break; break;
case this.organization.canManageScim: case organization.canManageScim:
route = "manage/scim"; route = "manage/scim";
break; break;
case this.organization.canAccessEventLogs: case organization.canAccessEventLogs:
route = "manage/events"; route = "manage/events";
break; break;
} }

View File

@@ -4,7 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { Utils } from "@bitwarden/common/misc/utils"; import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/encString"; import { EncString } from "@bitwarden/common/models/domain/encString";

View File

@@ -7,7 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { CollectionData } from "@bitwarden/common/models/data/collectionData"; import { CollectionData } from "@bitwarden/common/models/data/collectionData";

View File

@@ -7,7 +7,7 @@ import { ExportService } from "@bitwarden/common/abstractions/export.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ProviderService } from "@bitwarden/common/abstractions/provider.service"; import { ProviderService } from "@bitwarden/common/abstractions/provider.service";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";

View File

@@ -1,7 +1,7 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
@Component({ @Component({

View File

@@ -10,8 +10,8 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";

View File

@@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators"; import { first } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
import { PolicyType } from "@bitwarden/common/enums/policyType"; import { PolicyType } from "@bitwarden/common/enums/policyType";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";

View File

@@ -1,29 +0,0 @@
import { Organization } from "@bitwarden/common/models/domain/organization";
export function canAccessToolsTab(org: Organization): boolean {
return org.canAccessImportExport || org.canAccessReports;
}
export function canAccessSettingsTab(org: Organization): boolean {
return org.isOwner;
}
export function canAccessManageTab(org: Organization): boolean {
return (
org.canCreateNewCollections ||
org.canEditAnyCollection ||
org.canDeleteAnyCollection ||
org.canEditAssignedCollections ||
org.canDeleteAssignedCollections ||
org.canAccessEventLogs ||
org.canManageGroups ||
org.canManageUsers ||
org.canManagePolicies ||
org.canManageSso ||
org.canManageScim
);
}
export function canAccessOrgAdmin(org: Organization): boolean {
return canAccessToolsTab(org) || canAccessSettingsTab(org) || canAccessManageTab(org);
}

View File

@@ -2,6 +2,12 @@ import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard"; import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
import {
canAccessOrgAdmin,
canAccessManageTab,
canAccessSettingsTab,
canAccessToolsTab,
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
import { OrganizationPermissionsGuard } from "./guards/org-permissions.guard"; import { OrganizationPermissionsGuard } from "./guards/org-permissions.guard";
@@ -12,12 +18,6 @@ import { GroupsComponent } from "./manage/groups.component";
import { ManageComponent } from "./manage/manage.component"; import { ManageComponent } from "./manage/manage.component";
import { PeopleComponent } from "./manage/people.component"; import { PeopleComponent } from "./manage/people.component";
import { PoliciesComponent } from "./manage/policies.component"; import { PoliciesComponent } from "./manage/policies.component";
import {
canAccessOrgAdmin,
canAccessManageTab,
canAccessSettingsTab,
canAccessToolsTab,
} from "./navigation-permissions";
import { AccountComponent } from "./settings/account.component"; import { AccountComponent } from "./settings/account.component";
import { OrganizationBillingComponent } from "./settings/organization-billing.component"; import { OrganizationBillingComponent } from "./settings/organization-billing.component";
import { OrganizationSubscriptionComponent } from "./settings/organization-subscription.component"; import { OrganizationSubscriptionComponent } from "./settings/organization-subscription.component";

View File

@@ -2,7 +2,7 @@ import { Component } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms"; import { UntypedFormBuilder } from "@angular/forms";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyType } from "@bitwarden/common/enums/policyType"; import { PolicyType } from "@bitwarden/common/enums/policyType";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component"; import { BasePolicy, BasePolicyComponent } from "./base-policy.component";

View File

@@ -1,7 +1,7 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms"; import { UntypedFormBuilder } from "@angular/forms";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyType } from "@bitwarden/common/enums/policyType"; import { PolicyType } from "@bitwarden/common/enums/policyType";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";

View File

@@ -2,14 +2,12 @@ import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { OrganizationKeysRequest } from "@bitwarden/common/models/request/organizationKeysRequest"; import { OrganizationKeysRequest } from "@bitwarden/common/models/request/organizationKeysRequest";
import { OrganizationUpdateRequest } from "@bitwarden/common/models/request/organizationUpdateRequest"; import { OrganizationUpdateRequest } from "@bitwarden/common/models/request/organizationUpdateRequest";
import { OrganizationResponse } from "@bitwarden/common/models/response/organizationResponse"; import { OrganizationResponse } from "@bitwarden/common/models/response/organizationResponse";
@@ -41,17 +39,15 @@ export class AccountComponent {
loading = true; loading = true;
canUseApi = false; canUseApi = false;
org: OrganizationResponse; org: OrganizationResponse;
formPromise: Promise<boolean>; formPromise: Promise<OrganizationResponse>;
taxFormPromise: Promise<unknown>; taxFormPromise: Promise<unknown>;
private organizationId: string; private organizationId: string;
constructor( constructor(
private modalService: ModalService, private modalService: ModalService,
private apiService: ApiService,
private i18nService: I18nService, private i18nService: I18nService,
private route: ActivatedRoute, private route: ActivatedRoute,
private syncService: SyncService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private logService: LogService, private logService: LogService,
@@ -66,9 +62,7 @@ export class AccountComponent {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
this.canManageBilling = ( this.canManageBilling = this.organizationService.get(this.organizationId).canManageBilling;
await this.organizationService.get(this.organizationId)
).canManageBilling;
try { try {
this.org = await this.organizationApiService.get(this.organizationId); this.org = await this.organizationApiService.get(this.organizationId);
this.canUseApi = this.org.useApi; this.canUseApi = this.org.useApi;
@@ -94,9 +88,7 @@ export class AccountComponent {
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
} }
this.formPromise = this.organizationApiService.save(this.organizationId, request).then(() => { this.formPromise = this.organizationApiService.save(this.organizationId, request);
return this.syncService.fullSync(true);
});
await this.formPromise; await this.formPromise;
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"success", "success",

View File

@@ -3,8 +3,8 @@ import { Component, EventEmitter, OnInit, Output } from "@angular/core";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
import { CipherType } from "@bitwarden/common/enums/cipherType"; import { CipherType } from "@bitwarden/common/enums/cipherType";

View File

@@ -7,8 +7,8 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationApiKeyType } from "@bitwarden/common/enums/organizationApiKeyType"; import { OrganizationApiKeyType } from "@bitwarden/common/enums/organizationApiKeyType";
import { OrganizationConnectionType } from "@bitwarden/common/enums/organizationConnectionType"; import { OrganizationConnectionType } from "@bitwarden/common/enums/organizationConnectionType";
@@ -87,7 +87,7 @@ export class OrganizationSubscriptionComponent implements OnInit {
} }
this.loading = true; this.loading = true;
this.userOrg = await this.organizationService.get(this.organizationId); this.userOrg = this.organizationService.get(this.organizationId);
if (this.userOrg.canManageBilling) { if (this.userOrg.canManageBilling) {
this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.sub = await this.organizationApiService.getSubscription(this.organizationId);
} }

View File

@@ -1,7 +1,7 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@Component({ @Component({

View File

@@ -34,7 +34,9 @@
> >
<option value="" disabled>-- {{ "select" | i18n }} --</option> <option value="" disabled>-- {{ "select" | i18n }} --</option>
<option value="createNew">{{ "newFamiliesOrganization" | i18n }}</option> <option value="createNew">{{ "newFamiliesOrganization" | i18n }}</option>
<option *ngFor="let o of existingFamilyOrganizations" [ngValue]="o.id">{{ o.name }}</option> <option *ngFor="let o of existingFamilyOrganizations$ | async" [ngValue]="o.id">
{{ o.name }}
</option>
</select> </select>
</div> </div>
<div *ngIf="showNewOrganization" class="col-12"> <div *ngIf="showNewOrganization" class="col-12">

View File

@@ -1,12 +1,13 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators"; import { Observable, Subject } from "rxjs";
import { first, map, takeUntil } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ValidationService } from "@bitwarden/angular/services/validation.service"; import { ValidationService } from "@bitwarden/angular/services/validation.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { PlanSponsorshipType } from "@bitwarden/common/enums/planSponsorshipType"; import { PlanSponsorshipType } from "@bitwarden/common/enums/planSponsorshipType";
@@ -22,8 +23,7 @@ import { DeleteOrganizationComponent } from "../settings/delete-organization.com
selector: "families-for-enterprise-setup", selector: "families-for-enterprise-setup",
templateUrl: "families-for-enterprise-setup.component.html", templateUrl: "families-for-enterprise-setup.component.html",
}) })
// eslint-disable-next-line rxjs-angular/prefer-takeuntil export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
export class FamiliesForEnterpriseSetupComponent implements OnInit {
@ViewChild(OrganizationPlansComponent, { static: false }) @ViewChild(OrganizationPlansComponent, { static: false })
set organizationPlansComponent(value: OrganizationPlansComponent) { set organizationPlansComponent(value: OrganizationPlansComponent) {
if (!value) { if (!value) {
@@ -46,11 +46,14 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit {
token: string; token: string;
existingFamilyOrganizations: Organization[]; existingFamilyOrganizations: Organization[];
existingFamilyOrganizations$: Observable<Organization[]>;
showNewOrganization = false; showNewOrganization = false;
_organizationPlansComponent: OrganizationPlansComponent; _organizationPlansComponent: OrganizationPlansComponent;
_selectedFamilyOrganizationId = ""; _selectedFamilyOrganizationId = "";
private _destroy = new Subject<void>();
constructor( constructor(
private router: Router, private router: Router,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
@@ -84,17 +87,24 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit {
await this.syncService.fullSync(true); await this.syncService.fullSync(true);
this.badToken = !(await this.apiService.postPreValidateSponsorshipToken(this.token)); this.badToken = !(await this.apiService.postPreValidateSponsorshipToken(this.token));
this.loading = false; this.loading = false;
});
this.existingFamilyOrganizations = (await this.organizationService.getAll()).filter( this.existingFamilyOrganizations$ = this.organizationService.organizations$.pipe(
(o) => o.planProductType === ProductType.Families map((orgs) => orgs.filter((o) => o.planProductType === ProductType.Families))
); );
if (this.existingFamilyOrganizations.length === 0) { this.existingFamilyOrganizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => {
if (orgs.length === 0) {
this.selectedFamilyOrganizationId = "createNew"; this.selectedFamilyOrganizationId = "createNew";
} }
}); });
} }
ngOnDestroy(): void {
this._destroy.next();
this._destroy.complete();
}
async submit() { async submit() {
this.formPromise = this.doSubmit(this._selectedFamilyOrganizationId); this.formPromise = this.doSubmit(this._selectedFamilyOrganizationId);
await this.formPromise; await this.formPromise;

View File

@@ -5,7 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { Cipher } from "@bitwarden/common/models/domain/cipher"; import { Cipher } from "@bitwarden/common/models/domain/cipher";

View File

@@ -5,7 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { ImportService } from "@bitwarden/common/abstractions/import.service"; import { ImportService } from "@bitwarden/common/abstractions/import.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";

View File

@@ -5,7 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { CipherView } from "@bitwarden/common/models/view/cipherView"; import { CipherView } from "@bitwarden/common/models/view/cipherView";

View File

@@ -4,7 +4,7 @@ import { ActivatedRoute } from "@angular/router";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { Cipher } from "@bitwarden/common/models/domain/cipher"; import { Cipher } from "@bitwarden/common/models/domain/cipher";

View File

@@ -2,7 +2,7 @@ import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
@Component({ @Component({

View File

@@ -4,7 +4,7 @@ import { ActivatedRoute } from "@angular/router";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { CipherView } from "@bitwarden/common/models/view/cipherView"; import { CipherView } from "@bitwarden/common/models/view/cipherView";

View File

@@ -4,7 +4,7 @@ import { ActivatedRoute } from "@angular/router";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";

View File

@@ -9,7 +9,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";

View File

@@ -5,7 +5,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { EventService } from "@bitwarden/common/abstractions/event.service"; import { EventService } from "@bitwarden/common/abstractions/event.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";

View File

@@ -16,7 +16,7 @@ import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.s
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";

View File

@@ -11,7 +11,7 @@ import {
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PayPalConfig } from "@bitwarden/common/abstractions/environment.service"; import { PayPalConfig } from "@bitwarden/common/abstractions/environment.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType"; import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";

View File

@@ -10,8 +10,8 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service"; import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";

View File

@@ -7,7 +7,7 @@ import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { EmergencyAccessStatusType } from "@bitwarden/common/enums/emergencyAccessStatusType"; import { EmergencyAccessStatusType } from "@bitwarden/common/enums/emergencyAccessStatusType";

View File

@@ -8,7 +8,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";

View File

@@ -7,8 +7,8 @@ import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";

View File

@@ -2,7 +2,7 @@ import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { TokenService } from "@bitwarden/common/abstractions/token.service"; import { TokenService } from "@bitwarden/common/abstractions/token.service";

View File

@@ -22,7 +22,7 @@
[appApiAction]="formPromise" [appApiAction]="formPromise"
[formGroup]="sponsorshipForm" [formGroup]="sponsorshipForm"
ngNativeValidate ngNativeValidate
*ngIf="anyOrgsAvailable" *ngIf="anyOrgsAvailable$ | async"
> >
<div class="form-group col-7"> <div class="form-group col-7">
<label for="availableSponsorshipOrg">{{ "familiesSponsoringOrgSelect" | i18n }}</label> <label for="availableSponsorshipOrg">{{ "familiesSponsoringOrgSelect" | i18n }}</label>
@@ -34,7 +34,9 @@
required required
> >
<option disabled="true" value="">-- {{ "select" | i18n }} --</option> <option disabled="true" value="">-- {{ "select" | i18n }} --</option>
<option *ngFor="let o of availableSponsorshipOrgs" [ngValue]="o.id">{{ o.name }}</option> <option *ngFor="let o of availableSponsorshipOrgs$ | async" [ngValue]="o.id">
{{ o.name }}
</option>
</select> </select>
</div> </div>
<div class="form-group col-7"> <div class="form-group col-7">
@@ -74,7 +76,7 @@
</button> </button>
</div> </div>
</form> </form>
<ng-container *ngIf="anyActiveSponsorships"> <ng-container *ngIf="anyActiveSponsorships$ | async">
<div class="border-bottom"> <div class="border-bottom">
<table class="table table-hover table-list"> <table class="table table-hover table-list">
<thead> <thead>
@@ -86,12 +88,12 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<ng-container *ngFor="let o of activeSponsorshipOrgs"> <ng-container *ngFor="let o of activeSponsorshipOrgs$ | async">
<tr <tr
sponsoring-org-row sponsoring-org-row
[sponsoringOrg]="o" [sponsoringOrg]="o"
[isSelfHosted]="isSelfHosted" [isSelfHosted]="isSelfHosted"
(sponsorshipRemoved)="load(true)" (sponsorshipRemoved)="forceReload()"
></tr> ></tr>
</ng-container> </ng-container>
</tbody> </tbody>

View File

@@ -1,30 +1,40 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms"; import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
import { map, Observable, Subject, takeUntil } from "rxjs";
import { notAllowedValueAsync } from "@bitwarden/angular/validators/notAllowedValueAsync.validator"; import { notAllowedValueAsync } from "@bitwarden/angular/validators/notAllowedValueAsync.validator";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { PlanSponsorshipType } from "@bitwarden/common/enums/planSponsorshipType"; import { PlanSponsorshipType } from "@bitwarden/common/enums/planSponsorshipType";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
interface RequestSponsorshipForm {
selectedSponsorshipOrgId: FormControl<string>;
sponsorshipEmail: FormControl<string>;
}
@Component({ @Component({
selector: "app-sponsored-families", selector: "app-sponsored-families",
templateUrl: "sponsored-families.component.html", templateUrl: "sponsored-families.component.html",
}) })
export class SponsoredFamiliesComponent implements OnInit { export class SponsoredFamiliesComponent implements OnInit, OnDestroy {
loading = false; loading = false;
availableSponsorshipOrgs: Organization[] = []; availableSponsorshipOrgs$: Observable<Organization[]>;
activeSponsorshipOrgs: Organization[] = []; activeSponsorshipOrgs$: Observable<Organization[]>;
anyOrgsAvailable$: Observable<boolean>;
anyActiveSponsorships$: Observable<boolean>;
// Conditional display properties // Conditional display properties
formPromise: Promise<any>; formPromise: Promise<void>;
sponsorshipForm: UntypedFormGroup; sponsorshipForm: FormGroup<RequestSponsorshipForm>;
private _destroy = new Subject<void>();
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
@@ -32,31 +42,50 @@ export class SponsoredFamiliesComponent implements OnInit {
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private syncService: SyncService, private syncService: SyncService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private formBuilder: UntypedFormBuilder, private formBuilder: FormBuilder,
private stateService: StateService private stateService: StateService
) { ) {
this.sponsorshipForm = this.formBuilder.group({ this.sponsorshipForm = this.formBuilder.group<RequestSponsorshipForm>({
selectedSponsorshipOrgId: [ selectedSponsorshipOrgId: new FormControl("", {
"", validators: [Validators.required],
{ }),
validators: [Validators.required], sponsorshipEmail: new FormControl("", {
}, validators: [Validators.email],
], asyncValidators: [
sponsorshipEmail: [ notAllowedValueAsync(async () => await this.stateService.getEmail(), true),
"", ],
{ updateOn: "blur",
validators: [Validators.email], }),
asyncValidators: [
notAllowedValueAsync(async () => await this.stateService.getEmail(), true),
],
updateOn: "blur",
},
],
}); });
} }
async ngOnInit() { async ngOnInit() {
await this.load(); this.availableSponsorshipOrgs$ = this.organizationService.organizations$.pipe(
map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable))
);
this.availableSponsorshipOrgs$.pipe(takeUntil(this._destroy)).subscribe((orgs) => {
if (orgs.length === 1) {
this.sponsorshipForm.patchValue({
selectedSponsorshipOrgId: orgs[0].id,
});
}
});
this.anyOrgsAvailable$ = this.availableSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0));
this.activeSponsorshipOrgs$ = this.organizationService.organizations$.pipe(
map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null))
);
this.anyActiveSponsorships$ = this.activeSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0));
this.loading = false;
}
ngOnDestroy(): void {
this._destroy.next();
this._destroy.complete();
} }
async submit() { async submit() {
@@ -73,50 +102,23 @@ export class SponsoredFamiliesComponent implements OnInit {
this.platformUtilsService.showToast("success", null, this.i18nService.t("sponsorshipCreated")); this.platformUtilsService.showToast("success", null, this.i18nService.t("sponsorshipCreated"));
this.formPromise = null; this.formPromise = null;
this.resetForm(); this.resetForm();
await this.load(true); await this.forceReload();
} }
async load(forceReload = false) { async forceReload() {
if (this.loading) {
return;
}
this.loading = true; this.loading = true;
if (forceReload) { await this.syncService.fullSync(true);
await this.syncService.fullSync(true);
}
const allOrgs = await this.organizationService.getAll();
this.availableSponsorshipOrgs = allOrgs.filter((org) => org.familySponsorshipAvailable);
this.activeSponsorshipOrgs = allOrgs.filter(
(org) => org.familySponsorshipFriendlyName !== null
);
if (this.availableSponsorshipOrgs.length === 1) {
this.sponsorshipForm.patchValue({
selectedSponsorshipOrgId: this.availableSponsorshipOrgs[0].id,
});
}
this.loading = false; this.loading = false;
} }
get sponsorshipEmailControl() { get sponsorshipEmailControl() {
return this.sponsorshipForm.controls["sponsorshipEmail"]; return this.sponsorshipForm.controls.sponsorshipEmail;
} }
private async resetForm() { private async resetForm() {
this.sponsorshipForm.reset(); this.sponsorshipForm.reset();
} }
get anyActiveSponsorships(): boolean {
return this.activeSponsorshipOrgs.length > 0;
}
get anyOrgsAvailable(): boolean {
return this.availableSponsorshipOrgs.length > 0;
}
get isSelfHosted(): boolean { get isSelfHosted(): boolean {
return this.platformUtilsService.isSelfHost(); return this.platformUtilsService.isSelfHost();
} }

View File

@@ -9,7 +9,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";

View File

@@ -4,11 +4,12 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
import { CipherView } from "@bitwarden/common/models/view/cipherView"; import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { CollectionView } from "@bitwarden/common/models/view/collectionView"; import { CollectionView } from "@bitwarden/common/models/view/collectionView";
import { Checkable, isChecked } from "@bitwarden/common/types/checkable";
@Component({ @Component({
selector: "app-vault-bulk-share", selector: "app-vault-bulk-share",
@@ -20,10 +21,10 @@ export class BulkShareComponent implements OnInit {
@Output() onShared = new EventEmitter(); @Output() onShared = new EventEmitter();
nonShareableCount = 0; nonShareableCount = 0;
collections: CollectionView[] = []; collections: Checkable<CollectionView>[] = [];
organizations: Organization[] = []; organizations: Organization[] = [];
shareableCiphers: CipherView[] = []; shareableCiphers: CipherView[] = [];
formPromise: Promise<any>; formPromise: Promise<void>;
private writeableCollections: CollectionView[] = []; private writeableCollections: CollectionView[] = [];
@@ -66,9 +67,7 @@ export class BulkShareComponent implements OnInit {
} }
async submit() { async submit() {
const checkedCollectionIds = this.collections const checkedCollectionIds = this.collections.filter(isChecked).map((c) => c.id);
.filter((c) => (c as any).checked)
.map((c) => c.id);
try { try {
this.formPromise = this.cipherService.shareManyWithServer( this.formPromise = this.cipherService.shareManyWithServer(
this.shareableCiphers, this.shareableCiphers,
@@ -90,8 +89,8 @@ export class BulkShareComponent implements OnInit {
} }
} }
check(c: CollectionView, select?: boolean) { check(c: Checkable<CollectionView>, select?: boolean) {
(c as any).checked = select == null ? !(c as any).checked : select; c.checked = select == null ? !c.checked : select;
} }
selectAll(select: boolean) { selectAll(select: boolean) {
@@ -106,7 +105,7 @@ export class BulkShareComponent implements OnInit {
this.collections != null this.collections != null
) { ) {
for (let i = 0; i < this.collections.length; i++) { for (let i = 0; i < this.collections.length; i++) {
if ((this.collections[i] as any).checked) { if (this.collections[i].checked) {
return true; return true;
} }
} }

View File

@@ -5,7 +5,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { EventService } from "@bitwarden/common/abstractions/event.service"; import { EventService } from "@bitwarden/common/abstractions/event.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";

View File

@@ -15,78 +15,87 @@
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body" *ngIf="!organizations || !organizations.length"> <ng-container *ngIf="organizations$ | async as organizations">
{{ "noOrganizationsList" | i18n }} <div class="modal-body" *ngIf="!organizations || !organizations.length">
</div> {{ "noOrganizationsList" | i18n }}
<div class="modal-body" *ngIf="organizations && organizations.length">
<p>{{ "moveToOrgDesc" | i18n }}</p>
<div class="form-group">
<label for="organization">{{ "organization" | i18n }}</label>
<select
id="organization"
name="OrganizationId"
[(ngModel)]="organizationId"
class="form-control"
(change)="filterCollections()"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div> </div>
<div class="d-flex"> <div class="modal-body" *ngIf="organizations && organizations.length">
<h3>{{ "collections" | i18n }}</h3> <p>{{ "moveToOrgDesc" | i18n }}</p>
<div class="ml-auto d-flex" *ngIf="collections && collections.length"> <div class="form-group">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0"> <label for="organization">{{ "organization" | i18n }}</label>
{{ "selectAll" | i18n }} <select
</button> id="organization"
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0"> name="OrganizationId"
{{ "unselectAll" | i18n }} [(ngModel)]="organizationId"
</button> class="form-control"
(change)="filterCollections()"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div> </div>
<div class="d-flex">
<h3>{{ "collections" | i18n }}</h3>
<div class="ml-auto d-flex" *ngIf="collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{ "selectAll" | i18n }}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{ "unselectAll" | i18n }}
</button>
</div>
</div>
<div *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<table
class="table table-hover table-list mb-0"
*ngIf="collections && collections.length"
>
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
appStopProp
/>
</td>
<td>
{{ c.name }}
</td>
</tr>
</tbody>
</table>
</div> </div>
<div *ngIf="!collections || !collections.length"> <div class="modal-footer">
{{ "noCollectionsInList" | i18n }} <button
type="submit"
class="btn btn-primary btn-submit manual"
[disabled]="form.loading || !canSave"
[ngClass]="{ loading: form.loading }"
*ngIf="organizations && organizations.length"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "save" | i18n }}</span>
</button>
<a
href="#"
routerLink="/create-organization"
class="btn btn-primary"
*ngIf="!organizations || !organizations.length"
>
{{ "newOrganization" | i18n }}
</a>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div> </div>
<table class="table table-hover table-list mb-0" *ngIf="collections && collections.length"> </ng-container>
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
appStopProp
/>
</td>
<td>
{{ c.name }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary btn-submit manual"
[disabled]="form.loading || !canSave"
[ngClass]="{ loading: form.loading }"
*ngIf="organizations && organizations.length"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<a
href="#"
routerLink="/create-organization"
class="btn btn-primary"
*ngIf="!organizations || !organizations.length"
>
{{ "newOrganization" | i18n }}
</a>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { CollectionView } from "@bitwarden/common/models/view/collectionView"; import { CollectionView } from "@bitwarden/common/models/view/collectionView";

View File

@@ -102,9 +102,7 @@ export class OrganizationOptionsComponent {
} }
try { try {
this.actionPromise = this.organizationApiService.leave(org.id).then(() => { this.actionPromise = this.organizationApiService.leave(org.id);
return this.syncService.fullSync(true);
});
await this.actionPromise; await this.actionPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization")); this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
await this.load(); await this.load();

View File

@@ -8,7 +8,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { CollectionData } from "@bitwarden/common/models/data/collectionData"; import { CollectionData } from "@bitwarden/common/models/data/collectionData";

View File

@@ -17,7 +17,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";

View File

@@ -6,8 +6,8 @@ import { SelectOptions } from "@bitwarden/angular/interfaces/selectOptions";
import { dirtyRequired } from "@bitwarden/angular/validators/dirty.validator"; import { dirtyRequired } from "@bitwarden/angular/validators/dirty.validator";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { import {
OpenIdConnectRedirectBehavior, OpenIdConnectRedirectBehavior,

View File

@@ -2,12 +2,12 @@ import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard"; import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
import { canAccessManageTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
import { OrganizationPermissionsGuard } from "src/app/organizations/guards/org-permissions.guard"; import { OrganizationPermissionsGuard } from "src/app/organizations/guards/org-permissions.guard";
import { OrganizationLayoutComponent } from "src/app/organizations/layouts/organization-layout.component"; import { OrganizationLayoutComponent } from "src/app/organizations/layouts/organization-layout.component";
import { ManageComponent } from "src/app/organizations/manage/manage.component"; import { ManageComponent } from "src/app/organizations/manage/manage.component";
import { canAccessManageTab } from "src/app/organizations/navigation-permissions";
import { ScimComponent } from "./manage/scim.component"; import { ScimComponent } from "./manage/scim.component";
import { SsoComponent } from "./manage/sso.component"; import { SsoComponent } from "./manage/sso.component";

View File

@@ -7,8 +7,8 @@ import { ValidationService } from "@bitwarden/angular/services/validation.servic
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ProviderService } from "@bitwarden/common/abstractions/provider.service"; import { ProviderService } from "@bitwarden/common/abstractions/provider.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";

View File

@@ -9,7 +9,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";

View File

@@ -1,6 +1,6 @@
import { Component, Input, OnInit } from "@angular/core"; import { Component, Input, OnInit } from "@angular/core";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
@Component({ @Component({
@@ -23,7 +23,7 @@ export class ExportScopeCalloutComponent implements OnInit {
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
if (!(await this.organizationService.hasOrganizations())) { if (!this.organizationService.hasOrganizations()) {
return; return;
} }
this.scopeConfig = this.scopeConfig =
@@ -31,7 +31,7 @@ export class ExportScopeCalloutComponent implements OnInit {
? { ? {
title: "exportingOrganizationVaultTitle", title: "exportingOrganizationVaultTitle",
description: "exportingOrganizationVaultDescription", description: "exportingOrganizationVaultDescription",
scopeIdentifier: (await this.organizationService.get(this.organizationId)).name, scopeIdentifier: this.organizationService.get(this.organizationId).name,
} }
: { : {
title: "exportingPersonalVaultTitle", title: "exportingPersonalVaultTitle",

View File

@@ -1,7 +1,6 @@
import { Directive, OnInit } from "@angular/core"; import { Directive, OnInit } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service"; import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
@@ -23,7 +22,6 @@ export class RemovePasswordComponent implements OnInit {
constructor( constructor(
private router: Router, private router: Router,
private stateService: StateService, private stateService: StateService,
private apiService: ApiService,
private syncService: SyncService, private syncService: SyncService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private i18nService: I18nService,
@@ -70,9 +68,7 @@ export class RemovePasswordComponent implements OnInit {
try { try {
this.leaving = true; this.leaving = true;
this.actionPromise = this.organizationApiService.leave(this.organization.id).then(() => { this.actionPromise = this.organizationApiService.leave(this.organization.id);
return this.syncService.fullSync(true);
});
await this.actionPromise; await this.actionPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization")); this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
await this.keyConnectorService.removeConvertAccountRequired(); await this.keyConnectorService.removeConvertAccountRequired();

View File

@@ -1,29 +1,33 @@
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
import { Utils } from "@bitwarden/common/misc/utils"; import { Utils } from "@bitwarden/common/misc/utils";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
import { CipherView } from "@bitwarden/common/models/view/cipherView"; import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { CollectionView } from "@bitwarden/common/models/view/collectionView"; import { CollectionView } from "@bitwarden/common/models/view/collectionView";
import { Checkable, isChecked } from "@bitwarden/common/types/checkable";
@Directive() @Directive()
export class ShareComponent implements OnInit { export class ShareComponent implements OnInit, OnDestroy {
@Input() cipherId: string; @Input() cipherId: string;
@Input() organizationId: string; @Input() organizationId: string;
@Output() onSharedCipher = new EventEmitter(); @Output() onSharedCipher = new EventEmitter();
formPromise: Promise<any>; formPromise: Promise<void>;
cipher: CipherView; cipher: CipherView;
collections: CollectionView[] = []; collections: Checkable<CollectionView>[] = [];
organizations: Organization[] = []; organizations$: Observable<Organization[]>;
protected writeableCollections: CollectionView[] = []; protected writeableCollections: Checkable<CollectionView>[] = [];
private _destroy = new Subject<void>();
constructor( constructor(
protected collectionService: CollectionService, protected collectionService: CollectionService,
@@ -38,24 +42,37 @@ export class ShareComponent implements OnInit {
await this.load(); await this.load();
} }
ngOnDestroy(): void {
this._destroy.next();
this._destroy.complete();
}
async load() { async load() {
const allCollections = await this.collectionService.getAllDecrypted(); const allCollections = await this.collectionService.getAllDecrypted();
this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly); this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly);
const orgs = await this.organizationService.getAll();
this.organizations = orgs this.organizations$ = this.organizationService.organizations$.pipe(
.sort(Utils.getSortFunction(this.i18nService, "name")) map((orgs) => {
.filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed); return orgs
.filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed)
.sort(Utils.getSortFunction(this.i18nService, "name"));
})
);
this.organizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => {
if (this.organizationId == null && orgs.length > 0) {
this.organizationId = orgs[0].id;
}
});
const cipherDomain = await this.cipherService.get(this.cipherId); const cipherDomain = await this.cipherService.get(this.cipherId);
this.cipher = await cipherDomain.decrypt(); this.cipher = await cipherDomain.decrypt();
if (this.organizationId == null && this.organizations.length > 0) {
this.organizationId = this.organizations[0].id;
}
this.filterCollections(); this.filterCollections();
} }
filterCollections() { filterCollections() {
this.writeableCollections.forEach((c) => ((c as any).checked = false)); this.writeableCollections.forEach((c) => (c.checked = false));
if (this.organizationId == null || this.writeableCollections.length === 0) { if (this.organizationId == null || this.writeableCollections.length === 0) {
this.collections = []; this.collections = [];
} else { } else {
@@ -66,9 +83,7 @@ export class ShareComponent implements OnInit {
} }
async submit(): Promise<boolean> { async submit(): Promise<boolean> {
const selectedCollectionIds = this.collections const selectedCollectionIds = this.collections.filter(isChecked).map((c) => c.id);
.filter((c) => !!(c as any).checked)
.map((c) => c.id);
if (selectedCollectionIds.length === 0) { if (selectedCollectionIds.length === 0) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"error", "error",
@@ -80,9 +95,9 @@ export class ShareComponent implements OnInit {
const cipherDomain = await this.cipherService.get(this.cipherId); const cipherDomain = await this.cipherService.get(this.cipherId);
const cipherView = await cipherDomain.decrypt(); const cipherView = await cipherDomain.decrypt();
const orgs = await firstValueFrom(this.organizations$);
const orgName = const orgName =
this.organizations.find((o) => o.id === this.organizationId)?.name ?? orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization");
this.i18nService.t("organization");
try { try {
this.formPromise = this.cipherService this.formPromise = this.cipherService
@@ -106,7 +121,7 @@ export class ShareComponent implements OnInit {
get canSave() { get canSave() {
if (this.collections != null) { if (this.collections != null) {
for (let i = 0; i < this.collections.length; i++) { for (let i = 0; i < this.collections.length; i++) {
if ((this.collections[i] as any).checked) { if (this.collections[i].checked) {
return true; return true;
} }
} }

View File

@@ -32,8 +32,8 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
import { OrganizationService as OrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService as OrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
@@ -50,6 +50,7 @@ import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstr
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service"; import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { SyncNotifierService as SyncNotifierServiceAbstraction } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/abstractions/token.service"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/abstractions/token.service";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/abstractions/twoFactor.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/abstractions/twoFactor.service";
@@ -84,8 +85,8 @@ import { FolderService } from "@bitwarden/common/services/folder/folder.service"
import { FormValidationErrorsService } from "@bitwarden/common/services/formValidationErrors.service"; import { FormValidationErrorsService } from "@bitwarden/common/services/formValidationErrors.service";
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service"; import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service";
import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { NotificationsService } from "@bitwarden/common/services/notifications.service";
import { OrganizationService } from "@bitwarden/common/services/organization.service";
import { OrganizationApiService } from "@bitwarden/common/services/organization/organization-api.service"; import { OrganizationApiService } from "@bitwarden/common/services/organization/organization-api.service";
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service"; import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service";
import { PolicyApiService } from "@bitwarden/common/services/policy/policy-api.service"; import { PolicyApiService } from "@bitwarden/common/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/services/policy/policy.service"; import { PolicyService } from "@bitwarden/common/services/policy/policy.service";
@@ -96,6 +97,7 @@ import { SettingsService } from "@bitwarden/common/services/settings.service";
import { StateService } from "@bitwarden/common/services/state.service"; import { StateService } from "@bitwarden/common/services/state.service";
import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service";
import { SyncService } from "@bitwarden/common/services/sync/sync.service"; import { SyncService } from "@bitwarden/common/services/sync/sync.service";
import { SyncNotifierService } from "@bitwarden/common/services/sync/syncNotifier.service";
import { TokenService } from "@bitwarden/common/services/token.service"; import { TokenService } from "@bitwarden/common/services/token.service";
import { TotpService } from "@bitwarden/common/services/totp.service"; import { TotpService } from "@bitwarden/common/services/totp.service";
import { TwoFactorService } from "@bitwarden/common/services/twoFactor.service"; import { TwoFactorService } from "@bitwarden/common/services/twoFactor.service";
@@ -338,9 +340,9 @@ import { ValidationService } from "./validation.service";
LogService, LogService,
KeyConnectorServiceAbstraction, KeyConnectorServiceAbstraction,
StateServiceAbstraction, StateServiceAbstraction,
OrganizationServiceAbstraction,
ProviderServiceAbstraction, ProviderServiceAbstraction,
FolderApiServiceAbstraction, FolderApiServiceAbstraction,
SyncNotifierServiceAbstraction,
LOGOUT_CALLBACK, LOGOUT_CALLBACK,
], ],
}, },
@@ -506,7 +508,7 @@ import { ValidationService } from "./validation.service";
{ {
provide: OrganizationServiceAbstraction, provide: OrganizationServiceAbstraction,
useClass: OrganizationService, useClass: OrganizationService,
deps: [StateServiceAbstraction], deps: [StateServiceAbstraction, SyncNotifierServiceAbstraction],
}, },
{ {
provide: ProviderServiceAbstraction, provide: ProviderServiceAbstraction,
@@ -534,7 +536,15 @@ import { ValidationService } from "./validation.service";
{ {
provide: OrganizationApiServiceAbstraction, provide: OrganizationApiServiceAbstraction,
useClass: OrganizationApiService, useClass: OrganizationApiService,
deps: [ApiServiceAbstraction], // This is a slightly odd dependency tree for a specialized api service
// it depends on SyncService so that new data can be retrieved through the sync
// rather than updating the OrganizationService directly. Instead OrganizationService
// subscribes to sync notifications and will update itself based on that.
deps: [ApiServiceAbstraction, SyncServiceAbstraction],
},
{
provide: SyncNotifierServiceAbstraction,
useClass: SyncNotifierService,
}, },
{ {
provide: ConfigServiceAbstraction, provide: ConfigServiceAbstraction,

View File

@@ -4,7 +4,7 @@ import { firstValueFrom, from, mergeMap, Observable } from "rxjs";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PolicyType } from "@bitwarden/common/enums/policyType"; import { PolicyType } from "@bitwarden/common/enums/policyType";
@@ -37,8 +37,8 @@ export class VaultFilterService {
return new Set(await this.stateService.getCollapsedGroupings()); return new Set(await this.stateService.getCollapsedGroupings());
} }
async buildOrganizations(): Promise<Organization[]> { buildOrganizations(): Promise<Organization[]> {
return await this.organizationService.getAll(); return this.organizationService.getAll();
} }
buildNestedFolders(organizationId?: string): Observable<DynamicTreeNode<FolderView>> { buildNestedFolders(organizationId?: string): Observable<DynamicTreeNode<FolderView>> {

View File

@@ -0,0 +1,210 @@
import { MockProxy, mock, any, mockClear, matches } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, Subject } from "rxjs";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncNotifierService } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction";
import { OrganizationData } from "@bitwarden/common/models/data/organizationData";
import { SyncResponse } from "@bitwarden/common/models/response/syncResponse";
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
import { SyncEventArgs } from "@bitwarden/common/types/syncEventArgs";
describe("Organization Service", () => {
let organizationService: OrganizationService;
let stateService: MockProxy<StateService>;
let activeAccount: BehaviorSubject<string>;
let activeAccountUnlocked: BehaviorSubject<boolean>;
let syncNotifierService: MockProxy<SyncNotifierService>;
let sync: Subject<SyncEventArgs>;
const resetStateService = async (
customizeStateService: (stateService: MockProxy<StateService>) => void
) => {
mockClear(stateService);
stateService = mock<StateService>();
stateService.activeAccount$ = activeAccount;
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
customizeStateService(stateService);
organizationService = new OrganizationService(stateService, syncNotifierService);
await new Promise((r) => setTimeout(r, 50));
};
beforeEach(() => {
activeAccount = new BehaviorSubject("123");
activeAccountUnlocked = new BehaviorSubject(true);
stateService = mock<StateService>();
stateService.activeAccount$ = activeAccount;
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
stateService.getOrganizations.calledWith(any()).mockResolvedValue({
"1": organizationData("1", "Test Org"),
});
sync = new Subject<SyncEventArgs>();
syncNotifierService = mock<SyncNotifierService>();
syncNotifierService.sync$ = sync;
organizationService = new OrganizationService(stateService, syncNotifierService);
});
afterEach(() => {
activeAccount.complete();
activeAccountUnlocked.complete();
});
it("getAll", async () => {
const orgs = await organizationService.getAll();
expect(orgs).toHaveLength(1);
const org = orgs[0];
expect(org).toEqual({
id: "1",
name: "Test Org",
identifier: "test",
});
});
describe("canManageSponsorships", () => {
it("can because one is available", async () => {
await resetStateService((stateService) => {
stateService.getOrganizations.mockResolvedValue({
"1": { ...organizationData("1", "Org"), familySponsorshipAvailable: true },
});
});
const result = await organizationService.canManageSponsorships();
expect(result).toBe(true);
});
it("can because one is used", async () => {
await resetStateService((stateService) => {
stateService.getOrganizations.mockResolvedValue({
"1": { ...organizationData("1", "Test Org"), familySponsorshipFriendlyName: "Something" },
});
});
const result = await organizationService.canManageSponsorships();
expect(result).toBe(true);
});
it("can not because one isn't available or taken", async () => {
await resetStateService((stateService) => {
stateService.getOrganizations.mockResolvedValue({
"1": { ...organizationData("1", "Org"), familySponsorshipFriendlyName: null },
});
});
const result = await organizationService.canManageSponsorships();
expect(result).toBe(false);
});
});
describe("get", () => {
it("exists", async () => {
const result = organizationService.get("1");
expect(result).toEqual({
id: "1",
name: "Test Org",
identifier: "test",
});
});
it("does not exist", async () => {
const result = organizationService.get("2");
expect(result).toBe(undefined);
});
});
it("upsert", async () => {
await organizationService.upsert(organizationData("2", "Test 2"));
expect(await firstValueFrom(organizationService.organizations$)).toEqual([
{
id: "1",
name: "Test Org",
identifier: "test",
},
{
id: "2",
name: "Test 2",
identifier: "test",
},
]);
});
describe("getByIdentifier", () => {
it("exists", async () => {
const result = organizationService.getByIdentifier("test");
expect(result).toEqual({
id: "1",
name: "Test Org",
identifier: "test",
});
});
it("does not exist", async () => {
const result = organizationService.getByIdentifier("blah");
expect(result).toBeUndefined();
});
});
describe("delete", () => {
it("exists", async () => {
await organizationService.delete("1");
expect(stateService.getOrganizations).toHaveBeenCalledTimes(2);
expect(stateService.setOrganizations).toHaveBeenCalledTimes(1);
});
it("does not exist", async () => {
organizationService.delete("1");
expect(stateService.getOrganizations).toHaveBeenCalledTimes(2);
});
});
describe("syncEvent works", () => {
it("Complete event updates data", async () => {
sync.next({
status: "Completed",
successfully: true,
data: new SyncResponse({
profile: {
organizations: [
{
id: "1",
name: "Updated Name",
},
],
},
}),
});
await new Promise((r) => setTimeout(r, 500));
expect(stateService.setOrganizations).toHaveBeenCalledTimes(1);
expect(stateService.setOrganizations).toHaveBeenLastCalledWith(
matches((organizationData: { [id: string]: OrganizationData }) => {
const organization = organizationData["1"];
return organization?.name === "Updated Name";
})
);
});
});
function organizationData(id: string, name: string) {
const data = new OrganizationData({} as any);
data.id = id;
data.name = name;
data.identifier = "test";
return data;
}
});

View File

@@ -1,11 +0,0 @@
import { OrganizationData } from "../models/data/organizationData";
import { Organization } from "../models/domain/organization";
export abstract class OrganizationService {
get: (id: string) => Promise<Organization>;
getByIdentifier: (identifier: string) => Promise<Organization>;
getAll: (userId?: string) => Promise<Organization[]>;
save: (orgs: { [id: string]: OrganizationData }) => Promise<any>;
canManageSponsorships: () => Promise<boolean>;
hasOrganizations: (userId?: string) => Promise<boolean>;
}

View File

@@ -0,0 +1,53 @@
import { map, Observable } from "rxjs";
import { Utils } from "../../misc/utils";
import { Organization } from "../../models/domain/organization";
import { I18nService } from "../i18n.service";
export function canAccessToolsTab(org: Organization): boolean {
return org.canAccessImportExport || org.canAccessReports;
}
export function canAccessSettingsTab(org: Organization): boolean {
return org.isOwner;
}
export function canAccessManageTab(org: Organization): boolean {
return (
org.canCreateNewCollections ||
org.canEditAnyCollection ||
org.canDeleteAnyCollection ||
org.canEditAssignedCollections ||
org.canDeleteAssignedCollections ||
org.canAccessEventLogs ||
org.canManageGroups ||
org.canManageUsers ||
org.canManagePolicies ||
org.canManageSso ||
org.canManageScim
);
}
export function canAccessOrgAdmin(org: Organization): boolean {
return canAccessToolsTab(org) || canAccessSettingsTab(org) || canAccessManageTab(org);
}
export function getOrganizationById(id: string) {
return map<Organization[], Organization | undefined>((orgs) => orgs.find((o) => o.id === id));
}
export function canAccessAdmin(i18nService: I18nService) {
return map<Organization[], Organization[]>((orgs) =>
orgs.filter(canAccessOrgAdmin).sort(Utils.getSortFunction(i18nService, "name"))
);
}
export abstract class OrganizationService {
organizations$: Observable<Organization[]>;
get: (id: string) => Organization;
getByIdentifier: (identifier: string) => Organization;
getAll: (userId?: string) => Promise<Organization[]>;
canManageSponsorships: () => Promise<boolean>;
hasOrganizations: () => boolean;
}

View File

@@ -273,7 +273,13 @@ export abstract class StateService<T extends Account = Account> {
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>; setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>; getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>; setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use OrganizationService
*/
getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>; getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>;
/**
* @deprecated Do not call this directly, use OrganizationService
*/
setOrganizations: ( setOrganizations: (
value: { [id: string]: OrganizationData }, value: { [id: string]: OrganizationData },
options?: StorageOptions options?: StorageOptions

View File

@@ -1,17 +1,12 @@
import { Observable } from "rxjs";
import { import {
SyncCipherNotification, SyncCipherNotification,
SyncFolderNotification, SyncFolderNotification,
SyncSendNotification, SyncSendNotification,
} from "../../models/response/notificationResponse"; } from "../../models/response/notificationResponse";
import { SyncEventArgs } from "../../types/syncEventArgs";
export abstract class SyncService { export abstract class SyncService {
syncInProgress: boolean; syncInProgress: boolean;
sync$: Observable<SyncEventArgs>;
getLastSync: () => Promise<Date>; getLastSync: () => Promise<Date>;
setLastSync: (date: Date, userId?: string) => Promise<any>; setLastSync: (date: Date, userId?: string) => Promise<any>;
fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise<boolean>; fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise<boolean>;

View File

@@ -0,0 +1,8 @@
import { Observable } from "rxjs";
import { SyncEventArgs } from "../../types/syncEventArgs";
export abstract class SyncNotifierService {
sync$: Observable<SyncEventArgs>;
next: (event: SyncEventArgs) => void;
}

View File

@@ -310,8 +310,11 @@ export class Utils {
return map; return map;
} }
static getSortFunction(i18nService: I18nService, prop: string) { static getSortFunction<T>(
return (a: any, b: any) => { i18nService: I18nService,
prop: { [K in keyof T]: T[K] extends string ? K : never }[keyof T]
): (a: T, b: T) => number {
return (a, b) => {
if (a[prop] == null && b[prop] != null) { if (a[prop] == null && b[prop] != null) {
return -1; return -1;
} }
@@ -322,9 +325,10 @@ export class Utils {
return 0; return 0;
} }
// The `as unknown as string` here is unfortunate because typescript doesn't property understand that the return of T[prop] will be a string
return i18nService.collator return i18nService.collator
? i18nService.collator.compare(a[prop], b[prop]) ? i18nService.collator.compare(a[prop] as unknown as string, b[prop] as unknown as string)
: a[prop].localeCompare(b[prop]); : (a[prop] as unknown as string).localeCompare(b[prop] as unknown as string);
}; };
} }

View File

@@ -2,7 +2,7 @@ import { ApiService } from "../abstractions/api.service";
import { CipherService } from "../abstractions/cipher.service"; import { CipherService } from "../abstractions/cipher.service";
import { EventService as EventServiceAbstraction } from "../abstractions/event.service"; import { EventService as EventServiceAbstraction } from "../abstractions/event.service";
import { LogService } from "../abstractions/log.service"; import { LogService } from "../abstractions/log.service";
import { OrganizationService } from "../abstractions/organization.service"; import { OrganizationService } from "../abstractions/organization/organization.service.abstraction";
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { EventType } from "../enums/eventType"; import { EventType } from "../enums/eventType";
import { EventData } from "../models/data/eventData"; import { EventData } from "../models/data/eventData";

View File

@@ -3,7 +3,7 @@ import { CryptoService } from "../abstractions/crypto.service";
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service"; import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service";
import { LogService } from "../abstractions/log.service"; import { LogService } from "../abstractions/log.service";
import { OrganizationService } from "../abstractions/organization.service"; import { OrganizationService } from "../abstractions/organization/organization.service.abstraction";
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { TokenService } from "../abstractions/token.service"; import { TokenService } from "../abstractions/token.service";
import { OrganizationUserType } from "../enums/organizationUserType"; import { OrganizationUserType } from "../enums/organizationUserType";

View File

@@ -1,56 +0,0 @@
import { OrganizationService as OrganizationServiceAbstraction } from "../abstractions/organization.service";
import { StateService } from "../abstractions/state.service";
import { OrganizationData } from "../models/data/organizationData";
import { Organization } from "../models/domain/organization";
export class OrganizationService implements OrganizationServiceAbstraction {
constructor(private stateService: StateService) {}
async get(id: string): Promise<Organization> {
const organizations = await this.stateService.getOrganizations();
// eslint-disable-next-line
if (organizations == null || !organizations.hasOwnProperty(id)) {
return null;
}
return new Organization(organizations[id]);
}
async getByIdentifier(identifier: string): Promise<Organization> {
const organizations = await this.getAll();
if (organizations == null || organizations.length === 0) {
return null;
}
return organizations.find((o) => o.identifier === identifier);
}
async getAll(userId?: string): Promise<Organization[]> {
const organizations = await this.stateService.getOrganizations({ userId: userId });
const response: Organization[] = [];
for (const id in organizations) {
// eslint-disable-next-line
if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) {
response.push(new Organization(organizations[id]));
}
}
const sortedResponse = response.sort((a, b) => a.name.localeCompare(b.name));
return sortedResponse;
}
async save(organizations: { [id: string]: OrganizationData }) {
return await this.stateService.setOrganizations(organizations);
}
async canManageSponsorships(): Promise<boolean> {
const orgs = await this.getAll();
return orgs.some(
(o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null
);
}
async hasOrganizations(userId?: string): Promise<boolean> {
const organizations = await this.getAll(userId);
return organizations.length > 0;
}
}

View File

@@ -1,5 +1,6 @@
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
import { SyncService } from "../../abstractions/sync/sync.service.abstraction";
import { OrganizationApiKeyType } from "../../enums/organizationApiKeyType"; import { OrganizationApiKeyType } from "../../enums/organizationApiKeyType";
import { ImportDirectoryRequest } from "../../models/request/importDirectoryRequest"; import { ImportDirectoryRequest } from "../../models/request/importDirectoryRequest";
import { OrganizationSsoRequest } from "../../models/request/organization/organizationSsoRequest"; import { OrganizationSsoRequest } from "../../models/request/organization/organizationSsoRequest";
@@ -28,7 +29,7 @@ import { PaymentResponse } from "../../models/response/paymentResponse";
import { TaxInfoResponse } from "../../models/response/taxInfoResponse"; import { TaxInfoResponse } from "../../models/response/taxInfoResponse";
export class OrganizationApiService implements OrganizationApiServiceAbstraction { export class OrganizationApiService implements OrganizationApiServiceAbstraction {
constructor(private apiService: ApiService) {} constructor(private apiService: ApiService, private syncService: SyncService) {}
async get(id: string): Promise<OrganizationResponse> { async get(id: string): Promise<OrganizationResponse> {
const r = await this.apiService.send("GET", "/organizations/" + id, null, true, true); const r = await this.apiService.send("GET", "/organizations/" + id, null, true, true);
@@ -80,6 +81,8 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
async create(request: OrganizationCreateRequest): Promise<OrganizationResponse> { async create(request: OrganizationCreateRequest): Promise<OrganizationResponse> {
const r = await this.apiService.send("POST", "/organizations", request, true, true); const r = await this.apiService.send("POST", "/organizations", request, true, true);
// Forcing a sync will notify organization service that they need to repull
await this.syncService.fullSync(true);
return new OrganizationResponse(r); return new OrganizationResponse(r);
} }
@@ -90,7 +93,9 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
async save(id: string, request: OrganizationUpdateRequest): Promise<OrganizationResponse> { async save(id: string, request: OrganizationUpdateRequest): Promise<OrganizationResponse> {
const r = await this.apiService.send("PUT", "/organizations/" + id, request, true, true); const r = await this.apiService.send("PUT", "/organizations/" + id, request, true, true);
return new OrganizationResponse(r); const data = new OrganizationResponse(r);
await this.syncService.fullSync(true);
return data;
} }
async updatePayment(id: string, request: PaymentRequest): Promise<void> { async updatePayment(id: string, request: PaymentRequest): Promise<void> {
@@ -144,7 +149,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
} }
async verifyBank(id: string, request: VerifyBankRequest): Promise<void> { async verifyBank(id: string, request: VerifyBankRequest): Promise<void> {
return this.apiService.send( await this.apiService.send(
"POST", "POST",
"/organizations/" + id + "/verify-bank", "/organizations/" + id + "/verify-bank",
request, request,
@@ -162,15 +167,17 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
} }
async leave(id: string): Promise<void> { async leave(id: string): Promise<void> {
return this.apiService.send("POST", "/organizations/" + id + "/leave", null, true, false); await this.apiService.send("POST", "/organizations/" + id + "/leave", null, true, false);
await this.syncService.fullSync(true);
} }
async delete(id: string, request: SecretVerificationRequest): Promise<void> { async delete(id: string, request: SecretVerificationRequest): Promise<void> {
return this.apiService.send("DELETE", "/organizations/" + id, request, true, false); await this.apiService.send("DELETE", "/organizations/" + id, request, true, false);
await this.syncService.fullSync(true);
} }
async updateLicense(id: string, data: FormData): Promise<void> { async updateLicense(id: string, data: FormData): Promise<void> {
return this.apiService.send("POST", "/organizations/" + id + "/license", data, true, false); await this.apiService.send("POST", "/organizations/" + id + "/license", data, true, false);
} }
async importDirectory(organizationId: string, request: ImportDirectoryRequest): Promise<void> { async importDirectory(organizationId: string, request: ImportDirectoryRequest): Promise<void> {
@@ -223,6 +230,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
} }
async updateTaxInfo(id: string, request: OrganizationTaxInfoUpdateRequest): Promise<void> { async updateTaxInfo(id: string, request: OrganizationTaxInfoUpdateRequest): Promise<void> {
// Can't broadcast anything because the response doesn't have content
return this.apiService.send("PUT", "/organizations/" + id + "/tax", request, true, false); return this.apiService.send("PUT", "/organizations/" + id + "/tax", request, true, false);
} }
@@ -242,6 +250,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
true, true,
true true
); );
// Not broadcasting anything because data on this response doesn't correspond to `Organization`
return new OrganizationKeysResponse(r); return new OrganizationKeysResponse(r);
} }
@@ -258,6 +267,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
true, true,
true true
); );
// Not broadcasting anything because data on this response doesn't correspond to `Organization`
return new OrganizationSsoResponse(r); return new OrganizationSsoResponse(r);
} }
} }

View File

@@ -0,0 +1,119 @@
import { BehaviorSubject, concatMap, filter } from "rxjs";
import { OrganizationService as OrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction";
import { StateService } from "../../abstractions/state.service";
import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction";
import { OrganizationData } from "../../models/data/organizationData";
import { Organization } from "../../models/domain/organization";
import { isSuccessfullyCompleted } from "../../types/syncEventArgs";
export class OrganizationService implements OrganizationServiceAbstraction {
private _organizations = new BehaviorSubject<Organization[]>([]);
organizations$ = this._organizations.asObservable();
constructor(
private stateService: StateService,
private syncNotifierService: SyncNotifierService
) {
this.stateService.activeAccountUnlocked$
.pipe(
concatMap(async (unlocked) => {
if (!unlocked) {
this._organizations.next([]);
return;
}
const data = await this.stateService.getOrganizations();
this.updateObservables(data);
})
)
.subscribe();
this.syncNotifierService.sync$
.pipe(
filter(isSuccessfullyCompleted),
concatMap(async ({ data }) => {
const { profile } = data;
const organizations: { [id: string]: OrganizationData } = {};
profile.organizations.forEach((o) => {
organizations[o.id] = new OrganizationData(o);
});
profile.providerOrganizations.forEach((o) => {
if (organizations[o.id] == null) {
organizations[o.id] = new OrganizationData(o);
organizations[o.id].isProviderUser = true;
}
});
await this.updateStateAndObservables(organizations);
})
)
.subscribe();
}
async getAll(userId?: string): Promise<Organization[]> {
const organizationsMap = await this.stateService.getOrganizations({ userId: userId });
return Object.values(organizationsMap || {}).map((o) => new Organization(o));
}
async canManageSponsorships(): Promise<boolean> {
const organizations = this._organizations.getValue();
return organizations.some(
(o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null
);
}
hasOrganizations(): boolean {
const organizations = this._organizations.getValue();
return organizations.length > 0;
}
async upsert(organization: OrganizationData): Promise<void> {
let organizations = await this.stateService.getOrganizations();
if (organizations == null) {
organizations = {};
}
organizations[organization.id] = organization;
await this.updateStateAndObservables(organizations);
}
async delete(id: string): Promise<void> {
const organizations = await this.stateService.getOrganizations();
if (organizations == null) {
return;
}
if (organizations[id] == null) {
return;
}
delete organizations[id];
await this.updateStateAndObservables(organizations);
}
get(id: string): Organization {
const organizations = this._organizations.getValue();
return organizations.find((organization) => organization.id === id);
}
getByIdentifier(identifier: string): Organization {
const organizations = this._organizations.getValue();
return organizations.find((organization) => organization.identifier === identifier);
}
private async updateStateAndObservables(organizationsMap: { [id: string]: OrganizationData }) {
await this.stateService.setOrganizations(organizationsMap);
this.updateObservables(organizationsMap);
}
private updateObservables(organizationsMap: { [id: string]: OrganizationData }) {
const organizations = Object.values(organizationsMap || {}).map((o) => new Organization(o));
this._organizations.next(organizations);
}
}

View File

@@ -1,5 +1,5 @@
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";

View File

@@ -1,4 +1,4 @@
import { OrganizationService } from "../../abstractions/organization.service"; import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
import { InternalPolicyService as InternalPolicyServiceAbstraction } from "../../abstractions/policy/policy.service.abstraction"; import { InternalPolicyService as InternalPolicyServiceAbstraction } from "../../abstractions/policy/policy.service.abstraction";
import { StateService } from "../../abstractions/state.service"; import { StateService } from "../../abstractions/state.service";
import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType"; import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType";

View File

@@ -1927,12 +1927,18 @@ export class StateService<
); );
} }
/**
* @deprecated Do not call this directly, use OrganizationService
*/
async getOrganizations(options?: StorageOptions): Promise<{ [id: string]: OrganizationData }> { async getOrganizations(options?: StorageOptions): Promise<{ [id: string]: OrganizationData }> {
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.data?.organizations; )?.data?.organizations;
} }
/**
* @deprecated Do not call this directly, use OrganizationService
*/
async setOrganizations( async setOrganizations(
value: { [id: string]: OrganizationData }, value: { [id: string]: OrganizationData },
options?: StorageOptions options?: StorageOptions

View File

@@ -1,5 +1,3 @@
import { Subject } from "rxjs";
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { CipherService } from "../../abstractions/cipher.service"; import { CipherService } from "../../abstractions/cipher.service";
import { CollectionService } from "../../abstractions/collection.service"; import { CollectionService } from "../../abstractions/collection.service";
@@ -9,18 +7,17 @@ import { InternalFolderService } from "../../abstractions/folder/folder.service.
import { KeyConnectorService } from "../../abstractions/keyConnector.service"; import { KeyConnectorService } from "../../abstractions/keyConnector.service";
import { LogService } from "../../abstractions/log.service"; import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service"; import { MessagingService } from "../../abstractions/messaging.service";
import { OrganizationService } from "../../abstractions/organization.service";
import { InternalPolicyService } from "../../abstractions/policy/policy.service.abstraction"; import { InternalPolicyService } from "../../abstractions/policy/policy.service.abstraction";
import { ProviderService } from "../../abstractions/provider.service"; import { ProviderService } from "../../abstractions/provider.service";
import { SendService } from "../../abstractions/send.service"; import { SendService } from "../../abstractions/send.service";
import { SettingsService } from "../../abstractions/settings.service"; import { SettingsService } from "../../abstractions/settings.service";
import { StateService } from "../../abstractions/state.service"; import { StateService } from "../../abstractions/state.service";
import { SyncService as SyncServiceAbstraction } from "../../abstractions/sync/sync.service.abstraction"; import { SyncService as SyncServiceAbstraction } from "../../abstractions/sync/sync.service.abstraction";
import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction";
import { sequentialize } from "../../misc/sequentialize"; import { sequentialize } from "../../misc/sequentialize";
import { CipherData } from "../../models/data/cipherData"; import { CipherData } from "../../models/data/cipherData";
import { CollectionData } from "../../models/data/collectionData"; import { CollectionData } from "../../models/data/collectionData";
import { FolderData } from "../../models/data/folderData"; import { FolderData } from "../../models/data/folderData";
import { OrganizationData } from "../../models/data/organizationData";
import { PolicyData } from "../../models/data/policyData"; import { PolicyData } from "../../models/data/policyData";
import { ProviderData } from "../../models/data/providerData"; import { ProviderData } from "../../models/data/providerData";
import { SendData } from "../../models/data/sendData"; import { SendData } from "../../models/data/sendData";
@@ -36,15 +33,10 @@ import {
import { PolicyResponse } from "../../models/response/policyResponse"; import { PolicyResponse } from "../../models/response/policyResponse";
import { ProfileResponse } from "../../models/response/profileResponse"; import { ProfileResponse } from "../../models/response/profileResponse";
import { SendResponse } from "../../models/response/sendResponse"; import { SendResponse } from "../../models/response/sendResponse";
import { SyncEventArgs } from "../../types/syncEventArgs";
export class SyncService implements SyncServiceAbstraction { export class SyncService implements SyncServiceAbstraction {
syncInProgress = false; syncInProgress = false;
private _sync = new Subject<SyncEventArgs>();
sync$ = this._sync.asObservable();
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private settingsService: SettingsService, private settingsService: SettingsService,
@@ -58,9 +50,9 @@ export class SyncService implements SyncServiceAbstraction {
private logService: LogService, private logService: LogService,
private keyConnectorService: KeyConnectorService, private keyConnectorService: KeyConnectorService,
private stateService: StateService, private stateService: StateService,
private organizationService: OrganizationService,
private providerService: ProviderService, private providerService: ProviderService,
private folderApiService: FolderApiServiceAbstraction, private folderApiService: FolderApiServiceAbstraction,
private syncNotifierService: SyncNotifierService,
private logoutCallback: (expired: boolean) => Promise<void> private logoutCallback: (expired: boolean) => Promise<void>
) {} ) {}
@@ -84,8 +76,10 @@ export class SyncService implements SyncServiceAbstraction {
@sequentialize(() => "fullSync") @sequentialize(() => "fullSync")
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> { async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
this.syncStarted(); this.syncStarted();
this.syncNotifierService.next({ status: "Started" });
const isAuthenticated = await this.stateService.getIsAuthenticated(); const isAuthenticated = await this.stateService.getIsAuthenticated();
if (!isAuthenticated) { if (!isAuthenticated) {
this.syncNotifierService.next({ status: "Completed", successfully: false });
return this.syncCompleted(false); return this.syncCompleted(false);
} }
@@ -101,6 +95,7 @@ export class SyncService implements SyncServiceAbstraction {
if (!needsSync) { if (!needsSync) {
await this.setLastSync(now); await this.setLastSync(now);
this.syncNotifierService.next({ status: "Completed", successfully: false });
return this.syncCompleted(false); return this.syncCompleted(false);
} }
@@ -117,11 +112,13 @@ export class SyncService implements SyncServiceAbstraction {
await this.syncPolicies(response.policies); await this.syncPolicies(response.policies);
await this.setLastSync(now); await this.setLastSync(now);
this.syncNotifierService.next({ status: "Completed", successfully: true, data: response });
return this.syncCompleted(true); return this.syncCompleted(true);
} catch (e) { } catch (e) {
if (allowThrowOnError) { if (allowThrowOnError) {
throw e; throw e;
} else { } else {
this.syncNotifierService.next({ status: "Completed", successfully: false });
return this.syncCompleted(false); return this.syncCompleted(false);
} }
} }
@@ -272,13 +269,11 @@ export class SyncService implements SyncServiceAbstraction {
private syncStarted() { private syncStarted() {
this.syncInProgress = true; this.syncInProgress = true;
this.messagingService.send("syncStarted"); this.messagingService.send("syncStarted");
this._sync.next({ status: "Started" });
} }
private syncCompleted(successfully: boolean): boolean { private syncCompleted(successfully: boolean): boolean {
this.syncInProgress = false; this.syncInProgress = false;
this.messagingService.send("syncCompleted", { successfully: successfully }); this.messagingService.send("syncCompleted", { successfully: successfully });
this._sync.next({ status: successfully ? "SuccessfullyCompleted" : "UnsuccessfullyCompleted" });
return successfully; return successfully;
} }
@@ -320,24 +315,11 @@ export class SyncService implements SyncServiceAbstraction {
await this.stateService.setForcePasswordReset(response.forcePasswordReset); await this.stateService.setForcePasswordReset(response.forcePasswordReset);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector); await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
const organizations: { [id: string]: OrganizationData } = {};
response.organizations.forEach((o) => {
organizations[o.id] = new OrganizationData(o);
});
const providers: { [id: string]: ProviderData } = {}; const providers: { [id: string]: ProviderData } = {};
response.providers.forEach((p) => { response.providers.forEach((p) => {
providers[p.id] = new ProviderData(p); providers[p.id] = new ProviderData(p);
}); });
response.providerOrganizations.forEach((o) => {
if (organizations[o.id] == null) {
organizations[o.id] = new OrganizationData(o);
organizations[o.id].isProviderUser = true;
}
});
await this.organizationService.save(organizations);
await this.providerService.save(providers); await this.providerService.save(providers);
if (await this.keyConnectorService.userNeedsMigration()) { if (await this.keyConnectorService.userNeedsMigration()) {

View File

@@ -0,0 +1,18 @@
import { Subject } from "rxjs";
import { SyncNotifierService as SyncNotifierServiceAbstraction } from "../../abstractions/sync/syncNotifier.service.abstraction";
import { SyncEventArgs } from "../../types/syncEventArgs";
/**
* This class should most likely have 0 dependencies because it will hopefully
* be rolled into SyncService once upon a time.
*/
export class SyncNotifierService implements SyncNotifierServiceAbstraction {
private _sync = new Subject<SyncEventArgs>();
sync$ = this._sync.asObservable();
next(event: SyncEventArgs): void {
this._sync.next(event);
}
}

View File

@@ -0,0 +1,9 @@
type CheckableBase = {
checked?: boolean;
};
export type Checkable<T> = T & CheckableBase;
export function isChecked(item: CheckableBase): boolean {
return !!item.checked;
}

View File

@@ -1,15 +1,38 @@
import { filter } from "rxjs"; import { SyncResponse } from "../models/response/syncResponse";
export type SyncStatus = "Started" | "SuccessfullyCompleted" | "UnsuccessfullyCompleted"; type SyncStatus = "Started" | "Completed";
export type SyncEventArgs = { type SyncEventArgsBase<T extends SyncStatus> = {
status: SyncStatus; status: T;
}; };
type SyncCompletedEventArgsBase<T extends boolean> = SyncEventArgsBase<"Completed"> & {
successfully: T;
};
type SyncSuccessfullyCompletedEventArgs = SyncCompletedEventArgsBase<true> & {
data: SyncResponse;
};
export type SyncEventArgs =
| SyncSuccessfullyCompletedEventArgs
| SyncCompletedEventArgsBase<false>
| SyncEventArgsBase<"Started">;
/** /**
* Helper function to filter only on successfully completed syncs * Helper function to filter only on successfully completed syncs
* @returns a function that can be used in a `.pipe()` from an observable * @returns a function that can be used in a `.pipe(filter(...))` from an observable
* @example
* ```
* of<SyncEventArgs>({ status: "Completed", successfully: true, data: new SyncResponse() })
* .pipe(filter(isSuccessfullyCompleted))
* .subscribe(event => {
* console.log(event.data);
* });
* ```
*/ */
export function onlySuccessfullyCompleted() { export function isSuccessfullyCompleted(
return filter<SyncEventArgs>((syncEvent) => syncEvent.status === "SuccessfullyCompleted"); syncEvent: SyncEventArgs
): syncEvent is SyncSuccessfullyCompletedEventArgs {
return syncEvent.status === "Completed" && syncEvent.successfully;
} }