From a94faf06a99236524d662e3f25bbc6e2484870f0 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 21 Jul 2021 11:32:27 +0200 Subject: [PATCH] [Provider] Add support for managing providers (#1014) --- .../src/app/app-routing.module.ts | 15 + bitwarden_license/src/app/app.module.ts | 11 +- bitwarden_license/src/app/main.ts | 2 +- .../clients/add-organization.component.html | 35 ++ .../clients/add-organization.component.ts | 81 +++++ .../providers/clients/clients.component.html | 61 ++++ .../providers/clients/clients.component.ts | 144 ++++++++ .../create-organization.component.html | 5 + .../clients/create-organization.component.ts | 26 ++ .../manage/accept-provider.component.html | 35 ++ .../manage/accept-provider.component.ts | 48 +++ .../manage/bulk/bulk-confirm.component.ts | 38 +++ .../manage/bulk/bulk-remove.component.ts | 21 ++ .../providers/manage/events.component.html | 68 ++++ .../app/providers/manage/events.component.ts | 71 ++++ .../providers/manage/manage.component.html | 22 ++ .../app/providers/manage/manage.component.ts | 27 ++ .../providers/manage/people.component.html | 149 ++++++++ .../app/providers/manage/people.component.ts | 286 ++++++++++++++++ .../manage/user-add-edit.component.html | 71 ++++ .../manage/user-add-edit.component.ts | 104 ++++++ .../providers/providers-layout.component.html | 44 +++ .../providers/providers-layout.component.ts | 47 +++ .../app/providers/providers-routing.module.ts | 117 +++++++ .../src/app/providers/providers.module.ts | 62 ++++ .../services/provider-guard.service.ts | 32 ++ .../services/provider-type-guard.service.ts | 31 ++ .../providers/services/provider.service.ts | 32 ++ .../providers/settings/account.component.html | 35 ++ .../providers/settings/account.component.ts | 62 ++++ .../settings/settings.component.html | 17 + .../providers/settings/settings.component.ts | 20 ++ .../setup/setup-provider.component.html | 31 ++ .../setup/setup-provider.component.ts | 22 ++ .../app/providers/setup/setup.component.html | 38 +++ .../app/providers/setup/setup.component.ts | 96 ++++++ bitwarden_license/webpack.config.js | 7 + package.json | 4 +- src/app/app.module.ts | 4 +- src/app/common/base.events.component.ts | 157 +++++++++ src/app/common/base.people.component.ts | 314 +++++++++++++++++ .../organization-layout.component.html | 6 + .../manage/bulk/bulk-confirm.component.ts | 45 ++- .../manage/bulk/bulk-remove.component.html | 8 +- .../manage/bulk/bulk-remove.component.ts | 14 +- .../manage/bulk/bulk-status.component.ts | 11 +- .../manage/entity-events.component.ts | 6 +- .../organizations/manage/events.component.ts | 172 +++------- .../manage/people.component.html | 28 +- .../organizations/manage/people.component.ts | 322 +++--------------- .../manage/user-confirm.component.ts | 17 +- .../organization-subscription.component.html | 8 + .../organization-subscription.component.ts | 10 +- ...outing.module.ts => oss-routing.module.ts} | 2 +- src/app/oss.module.ts | 18 + src/app/providers/providers.component.html | 20 ++ src/app/providers/providers.component.ts | 36 ++ src/app/services/event.service.ts | 26 +- src/app/services/services.module.ts | 2 + .../settings/organization-plans.component.ts | 22 +- src/app/vault/vault.component.html | 12 + src/app/vault/vault.component.ts | 3 + src/locales/en/messages.json | 118 +++++++ tsconfig.json | 3 + 64 files changed, 2910 insertions(+), 491 deletions(-) create mode 100644 bitwarden_license/src/app/app-routing.module.ts create mode 100644 bitwarden_license/src/app/providers/clients/add-organization.component.html create mode 100644 bitwarden_license/src/app/providers/clients/add-organization.component.ts create mode 100644 bitwarden_license/src/app/providers/clients/clients.component.html create mode 100644 bitwarden_license/src/app/providers/clients/clients.component.ts create mode 100644 bitwarden_license/src/app/providers/clients/create-organization.component.html create mode 100644 bitwarden_license/src/app/providers/clients/create-organization.component.ts create mode 100644 bitwarden_license/src/app/providers/manage/accept-provider.component.html create mode 100644 bitwarden_license/src/app/providers/manage/accept-provider.component.ts create mode 100644 bitwarden_license/src/app/providers/manage/bulk/bulk-confirm.component.ts create mode 100644 bitwarden_license/src/app/providers/manage/bulk/bulk-remove.component.ts create mode 100644 bitwarden_license/src/app/providers/manage/events.component.html create mode 100644 bitwarden_license/src/app/providers/manage/events.component.ts create mode 100644 bitwarden_license/src/app/providers/manage/manage.component.html create mode 100644 bitwarden_license/src/app/providers/manage/manage.component.ts create mode 100644 bitwarden_license/src/app/providers/manage/people.component.html create mode 100644 bitwarden_license/src/app/providers/manage/people.component.ts create mode 100644 bitwarden_license/src/app/providers/manage/user-add-edit.component.html create mode 100644 bitwarden_license/src/app/providers/manage/user-add-edit.component.ts create mode 100644 bitwarden_license/src/app/providers/providers-layout.component.html create mode 100644 bitwarden_license/src/app/providers/providers-layout.component.ts create mode 100644 bitwarden_license/src/app/providers/providers-routing.module.ts create mode 100644 bitwarden_license/src/app/providers/providers.module.ts create mode 100644 bitwarden_license/src/app/providers/services/provider-guard.service.ts create mode 100644 bitwarden_license/src/app/providers/services/provider-type-guard.service.ts create mode 100644 bitwarden_license/src/app/providers/services/provider.service.ts create mode 100644 bitwarden_license/src/app/providers/settings/account.component.html create mode 100644 bitwarden_license/src/app/providers/settings/account.component.ts create mode 100644 bitwarden_license/src/app/providers/settings/settings.component.html create mode 100644 bitwarden_license/src/app/providers/settings/settings.component.ts create mode 100644 bitwarden_license/src/app/providers/setup/setup-provider.component.html create mode 100644 bitwarden_license/src/app/providers/setup/setup-provider.component.ts create mode 100644 bitwarden_license/src/app/providers/setup/setup.component.html create mode 100644 bitwarden_license/src/app/providers/setup/setup.component.ts create mode 100644 src/app/common/base.events.component.ts create mode 100644 src/app/common/base.people.component.ts rename src/app/{app-routing.module.ts => oss-routing.module.ts} (99%) create mode 100644 src/app/providers/providers.component.html create mode 100644 src/app/providers/providers.component.ts diff --git a/bitwarden_license/src/app/app-routing.module.ts b/bitwarden_license/src/app/app-routing.module.ts new file mode 100644 index 0000000000..3d03f7e348 --- /dev/null +++ b/bitwarden_license/src/app/app-routing.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: 'providers', + loadChildren: async () => (await import('./providers/providers.module')).ProvidersModule, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AppRoutingModule { } diff --git a/bitwarden_license/src/app/app.module.ts b/bitwarden_license/src/app/app.module.ts index 5719b35cc7..cdf705c1c3 100644 --- a/bitwarden_license/src/app/app.module.ts +++ b/bitwarden_license/src/app/app.module.ts @@ -6,10 +6,12 @@ import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { AppRoutingModule } from '../../../src/app/app-routing.module'; -import { AppComponent } from '../../../src/app/app.component'; -import { OssModule } from '../../../src/app/oss.module'; -import { ServicesModule } from '../../../src/app/services/services.module'; +import { AppRoutingModule } from './app-routing.module'; + +import { AppComponent } from 'src/app/app.component'; +import { OssRoutingModule } from 'src/app/oss-routing.module'; +import { OssModule } from 'src/app/oss.module'; +import { ServicesModule } from 'src/app/services/services.module'; @NgModule({ imports: [ @@ -21,6 +23,7 @@ import { ServicesModule } from '../../../src/app/services/services.module'; InfiniteScrollModule, DragDropModule, AppRoutingModule, + OssRoutingModule, ], bootstrap: [AppComponent], }) diff --git a/bitwarden_license/src/app/main.ts b/bitwarden_license/src/app/main.ts index 0bf25df7d4..9fe086495e 100644 --- a/bitwarden_license/src/app/main.ts +++ b/bitwarden_license/src/app/main.ts @@ -6,7 +6,7 @@ import 'jquery'; import 'popper.js'; // tslint:disable-next-line -require('../../../src/scss/styles.scss'); +require('src/scss/styles.scss'); import { AppModule } from './app.module'; diff --git a/bitwarden_license/src/app/providers/clients/add-organization.component.html b/bitwarden_license/src/app/providers/clients/add-organization.component.html new file mode 100644 index 0000000000..0ddda52d6e --- /dev/null +++ b/bitwarden_license/src/app/providers/clients/add-organization.component.html @@ -0,0 +1,35 @@ + diff --git a/bitwarden_license/src/app/providers/clients/add-organization.component.ts b/bitwarden_license/src/app/providers/clients/add-organization.component.ts new file mode 100644 index 0000000000..1186f505c3 --- /dev/null +++ b/bitwarden_license/src/app/providers/clients/add-organization.component.ts @@ -0,0 +1,81 @@ +import { + Component, + EventEmitter, + Input, + OnInit, + Output +} from '@angular/core'; +import { ToasterService } from 'angular2-toaster'; + +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { UserService } from 'jslib-common/abstractions/user.service'; + +import { ValidationService } from 'jslib-angular/services/validation.service'; + +import { ProviderService } from '../services/provider.service'; + +import { Organization } from 'jslib-common/models/domain/organization'; +import { Provider } from 'jslib-common/models/domain/provider'; + + +@Component({ + selector: 'provider-add-organization', + templateUrl: 'add-organization.component.html', +}) +export class AddOrganizationComponent implements OnInit { + + @Input() providerId: string; + @Output() onAddedOrganization = new EventEmitter(); + + provider: Provider; + formPromise: Promise; + loading = true; + organizations: Organization[]; + + constructor(private userService: UserService, private providerService: ProviderService, + private toasterService: ToasterService, private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, private validationService: ValidationService) { } + + async ngOnInit() { + await this.load(); + } + + async load() { + if (this.providerId == null) { + return; + } + + this.provider = await this.userService.getProvider(this.providerId); + + this.organizations = (await this.userService.getAllOrganizations()).filter(p => p.providerId == null); + this.loading = false; + } + + async add(organization: Organization) { + if (this.formPromise) { + return; + } + + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('addOrganizationConfirmation', organization.name, this.provider.name), organization.name, + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + + if (!confirmed) { + return false; + } + + try { + this.formPromise = this.providerService.addOrganizationToProvider(this.providerId, organization.id); + await this.formPromise; + } catch (e) { + this.validationService.showError(e); + return; + } finally { + this.formPromise = null; + } + + this.toasterService.popAsync('success', null, this.i18nService.t('organizationJoinedProvider')); + this.onAddedOrganization.emit(); + } +} diff --git a/bitwarden_license/src/app/providers/clients/clients.component.html b/bitwarden_license/src/app/providers/clients/clients.component.html new file mode 100644 index 0000000000..234d3e90ef --- /dev/null +++ b/bitwarden_license/src/app/providers/clients/clients.component.html @@ -0,0 +1,61 @@ + + + + + {{'loading' | i18n}} + + + +

{{'noClientsInList' | i18n}}

+ + + + + + + + + +
+ + + {{o.organizationName}} + + +
+
+
+ + diff --git a/bitwarden_license/src/app/providers/clients/clients.component.ts b/bitwarden_license/src/app/providers/clients/clients.component.ts new file mode 100644 index 0000000000..4ed3293672 --- /dev/null +++ b/bitwarden_license/src/app/providers/clients/clients.component.ts @@ -0,0 +1,144 @@ +import { + Component, + ComponentFactoryResolver, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ToasterService } from 'angular2-toaster'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { SearchService } from 'jslib-common/abstractions/search.service'; +import { UserService } from 'jslib-common/abstractions/user.service'; + +import { ValidationService } from 'jslib-angular/services/validation.service'; + +import { + ProviderOrganizationOrganizationDetailsResponse +} from 'jslib-common/models/response/provider/providerOrganizationResponse'; + +import { LogService } from 'jslib-common/abstractions'; +import { ModalComponent } from 'src/app/modal.component'; +import { ProviderService } from '../services/provider.service'; +import { AddOrganizationComponent } from './add-organization.component'; + +@Component({ + templateUrl: 'clients.component.html', +}) +export class ClientsComponent implements OnInit { + + @ViewChild('add', { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef; + + providerId: any; + searchText: string; + loading = true; + showAddExisting = false; + + clients: ProviderOrganizationOrganizationDetailsResponse[]; + pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; + modal: ModalComponent; + + protected didScroll = false; + protected pageSize = 100; + protected actionPromise: Promise; + private pagedClientsCount = 0; + + constructor(private route: ActivatedRoute, private userService: UserService, + private apiService: ApiService, private searchService: SearchService, + private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private toasterService: ToasterService, private validationService: ValidationService, + private providerService: ProviderService, private componentFactoryResolver: ComponentFactoryResolver, + private logService: LogService) { } + + async ngOnInit() { + this.route.parent.params.subscribe(async params => { + this.providerId = params.providerId; + + await this.load(); + }); + } + + async load() { + const response = await this.apiService.getProviderClients(this.providerId); + this.clients = response.data != null && response.data.length > 0 ? response.data : []; + this.showAddExisting = (await this.userService.getAllOrganizations()).some(org => org.providerId == null); + this.loading = false; + } + + isPaging() { + const searching = this.isSearching(); + if (searching && this.didScroll) { + this.resetPaging(); + } + return !searching && this.clients && this.clients.length > this.pageSize; + } + + isSearching() { + return this.searchService.isSearchable(this.searchText); + } + + async resetPaging() { + this.pagedClients = []; + this.loadMore(); + } + + + loadMore() { + if (!this.clients || this.clients.length <= this.pageSize) { + return; + } + const pagedLength = this.pagedClients.length; + let pagedSize = this.pageSize; + if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) { + pagedSize = this.pagedClientsCount; + } + if (this.clients.length > pagedLength) { + this.pagedClients = this.pagedClients.concat(this.clients.slice(pagedLength, pagedLength + pagedSize)); + } + this.pagedClientsCount = this.pagedClients.length; + this.didScroll = this.pagedClients.length > this.pageSize; + } + + addExistingOrganization() { + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.addModalRef.createComponent(factory).instance; + const childComponent = this.modal.show(AddOrganizationComponent, this.addModalRef); + + childComponent.providerId = this.providerId; + childComponent.onAddedOrganization.subscribe(async () => { + try { + await this.load(); + this.modal.close(); + } catch (e) { + this.logService.error(`Handled exception: ${e}`); + } + }); + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + } + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('detachOrganizationConfirmation'), organization.organizationName, + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + + if (!confirmed) { + return false; + } + + this.actionPromise = this.providerService.detachOrganizastion(this.providerId, organization.id); + try { + await this.actionPromise; + this.toasterService.popAsync('success', null, this.i18nService.t('detachedOrganization', organization.organizationName)); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + this.actionPromise = null; + } +} diff --git a/bitwarden_license/src/app/providers/clients/create-organization.component.html b/bitwarden_license/src/app/providers/clients/create-organization.component.html new file mode 100644 index 0000000000..6abe22d983 --- /dev/null +++ b/bitwarden_license/src/app/providers/clients/create-organization.component.html @@ -0,0 +1,5 @@ + +

{{'newClientOrganizationDesc' | i18n}}

+ diff --git a/bitwarden_license/src/app/providers/clients/create-organization.component.ts b/bitwarden_license/src/app/providers/clients/create-organization.component.ts new file mode 100644 index 0000000000..be29d544b6 --- /dev/null +++ b/bitwarden_license/src/app/providers/clients/create-organization.component.ts @@ -0,0 +1,26 @@ +import { + Component, + OnInit, + ViewChild, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { OrganizationPlansComponent } from 'src/app/settings/organization-plans.component'; + +@Component({ + selector: 'app-create-organization', + templateUrl: 'create-organization.component.html', +}) +export class CreateOrganizationComponent implements OnInit { + @ViewChild(OrganizationPlansComponent, { static: true }) orgPlansComponent: OrganizationPlansComponent; + + providerId: string; + + constructor(private route: ActivatedRoute) { } + + ngOnInit() { + this.route.parent.params.subscribe(async params => { + this.providerId = params.providerId; + }); + } +} diff --git a/bitwarden_license/src/app/providers/manage/accept-provider.component.html b/bitwarden_license/src/app/providers/manage/accept-provider.component.html new file mode 100644 index 0000000000..e9a6014eaa --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/accept-provider.component.html @@ -0,0 +1,35 @@ +
+
+ +

+ + {{'loading' | i18n}} +

+
+
+
+
+
+

{{'joinProvider' | i18n}}

+
+
+

+ {{providerName}} + {{email}} +

+

{{'joinProviderDesc' | i18n}}

+
+ +
+
+
+
+
diff --git a/bitwarden_license/src/app/providers/manage/accept-provider.component.ts b/bitwarden_license/src/app/providers/manage/accept-provider.component.ts new file mode 100644 index 0000000000..1788f67cd5 --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/accept-provider.component.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Toast, ToasterService } from 'angular2-toaster'; + +import { BaseAcceptComponent } from 'src/app/common/base.accept.component'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; + +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { StateService } from 'jslib-common/abstractions/state.service'; +import { UserService } from 'jslib-common/abstractions/user.service'; +import { ProviderUserAcceptRequest } from 'jslib-common/models/request/provider/providerUserAcceptRequest'; + +@Component({ + selector: 'app-accept-provider', + templateUrl: 'accept-provider.component.html', +}) +export class AcceptProviderComponent extends BaseAcceptComponent { + providerName: string; + + failedMessage = 'providerInviteAcceptFailed'; + + requiredParameters = ['providerId', 'providerUserId', 'token']; + + constructor(router: Router, toasterService: ToasterService, i18nService: I18nService, route: ActivatedRoute, + userService: UserService, stateService: StateService, private apiService: ApiService) { + super(router, toasterService, i18nService, route, userService, stateService); + } + + async authedHandler(qParams: any) { + const request = new ProviderUserAcceptRequest(); + request.token = qParams.token; + + await this.apiService.postProviderUserAccept(qParams.providerId, qParams.providerUserId, request); + const toast: Toast = { + type: 'success', + title: this.i18nService.t('inviteAccepted'), + body: this.i18nService.t('providerInviteAcceptedDesc'), + timeout: 10000, + }; + this.toasterService.popAsync(toast); + this.router.navigate(['/vault']); + } + + async unauthedHandler(qParams: any) { + this.providerName = qParams.providerName; + } +} diff --git a/bitwarden_license/src/app/providers/manage/bulk/bulk-confirm.component.ts b/bitwarden_license/src/app/providers/manage/bulk/bulk-confirm.component.ts new file mode 100644 index 0000000000..ddfdd06ee7 --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/bulk/bulk-confirm.component.ts @@ -0,0 +1,38 @@ +import { + Component, + Input, +} from '@angular/core'; + +import { ProviderUserBulkConfirmRequest } from 'jslib-common/models/request/provider/providerUserBulkConfirmRequest'; +import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest'; + +import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType'; + +import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from 'src/app/organizations/manage/bulk/bulk-confirm.component'; +import { BulkUserDetails } from 'src/app/organizations/manage/bulk/bulk-status.component'; + +@Component({ + templateUrl: '/src/app/organizations/manage/bulk/bulk-confirm.component.html', +}) +export class BulkConfirmComponent extends OrganizationBulkConfirmComponent { + + @Input() providerId: string; + + protected isAccepted(user: BulkUserDetails) { + return user.status === ProviderUserStatusType.Accepted; + } + + protected async getPublicKeys() { + const request = new ProviderUserBulkRequest(this.filteredUsers.map(user => user.id)); + return await this.apiService.postProviderUsersPublicKey(this.providerId, request); + } + + protected getCryptoKey() { + return this.cryptoService.getProviderKey(this.providerId); + } + + protected async postConfirmRequest(userIdsWithKeys: any[]) { + const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys); + return await this.apiService.postProviderUserBulkConfirm(this.providerId, request); + } +} diff --git a/bitwarden_license/src/app/providers/manage/bulk/bulk-remove.component.ts b/bitwarden_license/src/app/providers/manage/bulk/bulk-remove.component.ts new file mode 100644 index 0000000000..7a0f7cf58e --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/bulk/bulk-remove.component.ts @@ -0,0 +1,21 @@ +import { + Component, + Input, +} from '@angular/core'; + +import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest'; + +import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from 'src/app/organizations/manage/bulk/bulk-remove.component'; + +@Component({ + templateUrl: '/src/app/organizations/manage/bulk/bulk-remove.component.html', +}) +export class BulkRemoveComponent extends OrganizationBulkRemoveComponent { + + @Input() providerId: string; + + async deleteUsers() { + const request = new ProviderUserBulkRequest(this.users.map(user => user.id)); + return await this.apiService.deleteManyProviderUsers(this.providerId, request); + } +} diff --git a/bitwarden_license/src/app/providers/manage/events.component.html b/bitwarden_license/src/app/providers/manage/events.component.html new file mode 100644 index 0000000000..9b50717fe4 --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/events.component.html @@ -0,0 +1,68 @@ + + + + {{'loading' | i18n}} + + +

{{'noEventsInList' | i18n}}

+ + + + + + + + + + + + + + + + + +
{{'timestamp' | i18n}} + {{'device' | i18n}} + {{'user' | i18n}}{{'event' | i18n}}
{{e.date | date:'medium'}} + + {{e.appName}}, {{e.ip}} + + {{e.userName}} +
+ +
diff --git a/bitwarden_license/src/app/providers/manage/events.component.ts b/bitwarden_license/src/app/providers/manage/events.component.ts new file mode 100644 index 0000000000..2b9ce76d14 --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/events.component.ts @@ -0,0 +1,71 @@ +import { + Component, + OnInit, +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ToasterService } from 'angular2-toaster'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { ExportService } from 'jslib-common/abstractions/export.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { LogService } from 'jslib-common/abstractions/log.service'; +import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { UserService } from 'jslib-common/abstractions/user.service'; + +import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe'; + +import { EventResponse } from 'jslib-common/models/response/eventResponse'; + +import { EventService } from 'src/app/services/event.service'; + +import { BaseEventsComponent } from 'src/app/common/base.events.component'; + +@Component({ + selector: 'provider-events', + templateUrl: 'events.component.html', +}) +export class EventsComponent extends BaseEventsComponent implements OnInit { + exportFileName: string = 'provider-events'; + providerId: string; + + private providerUsersUserIdMap = new Map(); + private providerUsersIdMap = new Map(); + + constructor(private apiService: ApiService, private route: ActivatedRoute, eventService: EventService, + i18nService: I18nService, toasterService: ToasterService, private userService: UserService, + exportService: ExportService, platformUtilsService: PlatformUtilsService, private router: Router, + logService: LogService, private userNamePipe: UserNamePipe) { + super(eventService, i18nService, toasterService, exportService, platformUtilsService, logService); + } + + async ngOnInit() { + this.route.parent.parent.params.subscribe(async params => { + this.providerId = params.providerId; + const provider = await this.userService.getProvider(this.providerId); + if (provider == null || !provider.useEvents) { + this.router.navigate(['/providers', this.providerId]); + return; + } + await this.load(); + }); + } + + async load() { + const response = await this.apiService.getProviderUsers(this.providerId); + response.data.forEach(u => { + const name = this.userNamePipe.transform(u); + this.providerUsersIdMap.set(u.id, { name: name, email: u.email }); + this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email }); + }); + await this.loadEvents(true); + this.loaded = true; + } + + protected requestEvents(startDate: string, endDate: string, continuationToken: string) { + return this.apiService.getEventsProvider(this.providerId, startDate, endDate, continuationToken); + } + + protected getUserName(r: EventResponse, userId: string) { + return userId != null && this.providerUsersUserIdMap.has(userId) ? this.providerUsersUserIdMap.get(userId) : null; + } +} diff --git a/bitwarden_license/src/app/providers/manage/manage.component.html b/bitwarden_license/src/app/providers/manage/manage.component.html new file mode 100644 index 0000000000..8941baaa54 --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/manage.component.html @@ -0,0 +1,22 @@ +
+
+ +
+ +
+
+
diff --git a/bitwarden_license/src/app/providers/manage/manage.component.ts b/bitwarden_license/src/app/providers/manage/manage.component.ts new file mode 100644 index 0000000000..708f22064e --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/manage.component.ts @@ -0,0 +1,27 @@ +import { + Component, + OnInit, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { UserService } from 'jslib-common/abstractions/user.service'; + +import { Provider } from 'jslib-common/models/domain/provider'; + +@Component({ + selector: 'provider-manage', + templateUrl: 'manage.component.html', +}) +export class ManageComponent implements OnInit { + provider: Provider; + accessEvents = false; + + constructor(private route: ActivatedRoute, private userService: UserService) { } + + ngOnInit() { + this.route.parent.params.subscribe(async params => { + this.provider = await this.userService.getProvider(params.providerId); + this.accessEvents = this.provider.useEvents; + }); + } +} diff --git a/bitwarden_license/src/app/providers/manage/people.component.html b/bitwarden_license/src/app/providers/manage/people.component.html new file mode 100644 index 0000000000..e97b24ad50 --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/people.component.html @@ -0,0 +1,149 @@ + + + + {{'loading' | i18n}} + + +

{{'noUsersInList' | i18n}}

+ + + {{'providerUsersNeedConfirmed' | i18n}} + + + + + + + + + + + + +
+ + + + + {{u.email}} + {{'invited' | i18n}} + {{'accepted' | i18n}} + {{u.name}} + + + + {{'userUsingTwoStep' | i18n}} + + + {{'providerAdmin' | i18n}} + {{'serviceUser' | i18n}} + {{'custom' | i18n}} + + +
+
+
+ + + + + + diff --git a/bitwarden_license/src/app/providers/manage/people.component.ts b/bitwarden_license/src/app/providers/manage/people.component.ts new file mode 100644 index 0000000000..6741f99c4a --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/people.component.ts @@ -0,0 +1,286 @@ +import { + Component, + ComponentFactoryResolver, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ToasterService } from 'angular2-toaster'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { CryptoService } from 'jslib-common/abstractions/crypto.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { LogService } from 'jslib-common/abstractions/log.service'; +import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { SearchService } from 'jslib-common/abstractions/search.service'; +import { StorageService } from 'jslib-common/abstractions/storage.service'; +import { UserService } from 'jslib-common/abstractions/user.service'; + +import { ValidationService } from 'jslib-angular/services/validation.service'; + +import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType'; +import { ProviderUserType } from 'jslib-common/enums/providerUserType'; + +import { SearchPipe } from 'jslib-angular/pipes/search.pipe'; +import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe'; + +import { ListResponse } from 'jslib-common/models/response/listResponse'; +import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse'; + +import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest'; +import { ProviderUserConfirmRequest } from 'jslib-common/models/request/provider/providerUserConfirmRequest'; +import { ProviderUserBulkResponse } from 'jslib-common/models/response/provider/providerUserBulkResponse'; + +import { BasePeopleComponent } from 'src/app/common/base.people.component'; +import { ModalComponent } from 'src/app/modal.component'; +import { BulkStatusComponent } from 'src/app/organizations/manage/bulk/bulk-status.component'; +import { EntityEventsComponent } from 'src/app/organizations/manage/entity-events.component'; +import { BulkConfirmComponent } from './bulk/bulk-confirm.component'; +import { BulkRemoveComponent } from './bulk/bulk-remove.component'; +import { UserAddEditComponent } from './user-add-edit.component'; + +@Component({ + selector: 'provider-people', + templateUrl: 'people.component.html', +}) +export class PeopleComponent extends BasePeopleComponent implements OnInit { + + @ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef; + @ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef; + @ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef; + @ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: ViewContainerRef; + @ViewChild('bulkConfirmTemplate', { read: ViewContainerRef, static: true }) bulkConfirmModalRef: ViewContainerRef; + @ViewChild('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: ViewContainerRef; + + userType = ProviderUserType; + userStatusType = ProviderUserStatusType; + providerId: string; + accessEvents = false; + + constructor(apiService: ApiService, private route: ActivatedRoute, + i18nService: I18nService, componentFactoryResolver: ComponentFactoryResolver, + platformUtilsService: PlatformUtilsService, toasterService: ToasterService, + cryptoService: CryptoService, private userService: UserService, private router: Router, + storageService: StorageService, searchService: SearchService, validationService: ValidationService, + logService: LogService, searchPipe: SearchPipe, userNamePipe: UserNamePipe) { + super(apiService, searchService, i18nService, platformUtilsService, toasterService, cryptoService, + storageService, validationService, componentFactoryResolver, logService, searchPipe, userNamePipe); + } + + ngOnInit() { + this.route.parent.params.subscribe(async params => { + this.providerId = params.providerId; + const provider = await this.userService.getProvider(this.providerId); + + if (!provider.canManageUsers) { + this.router.navigate(['../'], { relativeTo: this.route }); + return; + } + + this.accessEvents = provider.useEvents; + + await this.load(); + + const queryParamsSub = this.route.queryParams.subscribe(async qParams => { + this.searchText = qParams.search; + if (qParams.viewEvents != null) { + const user = this.users.filter(u => u.id === qParams.viewEvents); + if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) { + this.events(user[0]); + } + } + if (queryParamsSub != null) { + queryParamsSub.unsubscribe(); + } + }); + }); + } + + getUsers(): Promise> { + return this.apiService.getProviderUsers(this.providerId); + } + + deleteUser(id: string): Promise { + return this.apiService.deleteProviderUser(this.providerId, id); + } + + reinviteUser(id: string): Promise { + return this.apiService.postProviderUserReinvite(this.providerId, id); + } + + async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise { + const providerKey = await this.cryptoService.getProviderKey(this.providerId); + const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer); + const request = new ProviderUserConfirmRequest(); + request.key = key.encryptedString; + await this.apiService.postProviderUserConfirm(this.providerId, user.id, request); + } + + edit(user: ProviderUserUserDetailsResponse) { + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.addEditModalRef.createComponent(factory).instance; + const childComponent = this.modal.show( + UserAddEditComponent, this.addEditModalRef); + + childComponent.name = this.userNamePipe.transform(user); + childComponent.providerId = this.providerId; + childComponent.providerUserId = user != null ? user.id : null; + childComponent.onSavedUser.subscribe(() => { + this.modal.close(); + this.load(); + }); + childComponent.onDeletedUser.subscribe(() => { + this.modal.close(); + this.removeUser(user); + }); + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + } + + async events(user: ProviderUserUserDetailsResponse) { + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.eventsModalRef.createComponent(factory).instance; + const childComponent = this.modal.show( + EntityEventsComponent, this.eventsModalRef); + + childComponent.name = this.userNamePipe.transform(user); + childComponent.providerId = this.providerId; + childComponent.entityId = user.id; + childComponent.showUser = false; + childComponent.entity = 'user'; + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + } + + async bulkRemove() { + if (this.actionPromise != null) { + return; + } + + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.bulkRemoveModalRef.createComponent(factory).instance; + const childComponent = this.modal.show(BulkRemoveComponent, this.bulkRemoveModalRef); + + childComponent.providerId = this.providerId; + childComponent.users = this.getCheckedUsers(); + + this.modal.onClosed.subscribe(async () => { + await this.load(); + this.modal = null; + }); + } + + async bulkReinvite() { + if (this.actionPromise != null) { + return; + } + + const users = this.getCheckedUsers(); + const filteredUsers = users.filter(u => u.status === ProviderUserStatusType.Invited); + + if (filteredUsers.length <= 0) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('noSelectedUsersApplicable')); + return; + } + + try { + const request = new ProviderUserBulkRequest(filteredUsers.map(user => user.id)); + const response = this.apiService.postManyProviderUserReinvite(this.providerId, request); + this.showBulkStatus(users, filteredUsers, response, this.i18nService.t('bulkReinviteMessage')); + } catch (e) { + this.validationService.showError(e); + } + this.actionPromise = null; + } + + async bulkConfirm() { + if (this.actionPromise != null) { + return; + } + + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.bulkConfirmModalRef.createComponent(factory).instance; + const childComponent = this.modal.show(BulkConfirmComponent, this.bulkConfirmModalRef); + + childComponent.providerId = this.providerId; + childComponent.users = this.getCheckedUsers(); + + this.modal.onClosed.subscribe(async () => { + await this.load(); + this.modal = null; + }); + } + + private async showBulkStatus(users: ProviderUserUserDetailsResponse[], filteredUsers: ProviderUserUserDetailsResponse[], + request: Promise>, successfullMessage: string) { + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.bulkStatusModalRef.createComponent(factory).instance; + const childComponent = this.modal.show( + BulkStatusComponent, this.bulkStatusModalRef); + + childComponent.loading = true; + + // Workaround to handle closing the modal shortly after it has been opened + let close = false; + this.modal.onShown.subscribe(() => { + if (close) { + this.modal.close(); + } + }); + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + + try { + const response = await request; + + if (this.modal) { + const keyedErrors: any = response.data.filter(r => r.error !== '').reduce((a, x) => ({ ...a, [x.id]: x.error }), {}); + const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {}); + + childComponent.users = users.map(user => { + let message = keyedErrors[user.id] ?? successfullMessage; + if (!keyedFilteredUsers.hasOwnProperty(user.id)) { + message = this.i18nService.t('bulkFilteredMessage'); + } + + return { + user: user, + error: keyedErrors.hasOwnProperty(user.id), + message: message, + }; + }); + childComponent.loading = false; + } + } catch { + close = true; + if (this.modal) { + this.modal.close(); + } + } + } +} diff --git a/bitwarden_license/src/app/providers/manage/user-add-edit.component.html b/bitwarden_license/src/app/providers/manage/user-add-edit.component.html new file mode 100644 index 0000000000..7e8e014ebb --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/user-add-edit.component.html @@ -0,0 +1,71 @@ + diff --git a/bitwarden_license/src/app/providers/manage/user-add-edit.component.ts b/bitwarden_license/src/app/providers/manage/user-add-edit.component.ts new file mode 100644 index 0000000000..e70ffd16a3 --- /dev/null +++ b/bitwarden_license/src/app/providers/manage/user-add-edit.component.ts @@ -0,0 +1,104 @@ +import { + Component, + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; + +import { ProviderUserInviteRequest } from 'jslib-common/models/request/provider/providerUserInviteRequest'; + +import { PermissionsApi } from 'jslib-common/models/api/permissionsApi'; + +import { ProviderUserType } from 'jslib-common/enums/providerUserType'; +import { ProviderUserUpdateRequest } from 'jslib-common/models/request/provider/providerUserUpdateRequest'; + +@Component({ + selector: 'provider-user-add-edit', + templateUrl: 'user-add-edit.component.html', +}) +export class UserAddEditComponent implements OnInit { + @Input() name: string; + @Input() providerUserId: string; + @Input() providerId: string; + @Output() onSavedUser = new EventEmitter(); + @Output() onDeletedUser = new EventEmitter(); + + loading = true; + editMode: boolean = false; + title: string; + emails: string; + type: ProviderUserType = ProviderUserType.ServiceUser; + permissions = new PermissionsApi(); + showCustom = false; + access: 'all' | 'selected' = 'selected'; + formPromise: Promise; + deletePromise: Promise; + userType = ProviderUserType; + + constructor(private apiService: ApiService, private i18nService: I18nService, + private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService) { } + + async ngOnInit() { + this.editMode = this.loading = this.providerUserId != null; + + if (this.editMode) { + this.editMode = true; + this.title = this.i18nService.t('editUser'); + try { + const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId); + this.type = user.type; + } catch { } + } else { + this.title = this.i18nService.t('inviteUser'); + } + + this.loading = false; + } + + async submit() { + try { + if (this.editMode) { + const request = new ProviderUserUpdateRequest(); + request.type = this.type; + this.formPromise = this.apiService.putProviderUser(this.providerId, this.providerUserId, request); + } else { + const request = new ProviderUserInviteRequest(); + request.emails = this.emails.trim().split(/\s*,\s*/); + request.type = this.type; + this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request); + } + await this.formPromise; + this.toasterService.popAsync('success', null, + this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name)); + this.onSavedUser.emit(); + } catch { } + } + + async delete() { + if (!this.editMode) { + return; + } + + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('removeUserConfirmation'), this.name, + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return false; + } + + try { + this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId); + await this.deletePromise; + this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', this.name)); + this.onDeletedUser.emit(); + } catch { } + } + +} diff --git a/bitwarden_license/src/app/providers/providers-layout.component.html b/bitwarden_license/src/app/providers/providers-layout.component.html new file mode 100644 index 0000000000..1cee05e77b --- /dev/null +++ b/bitwarden_license/src/app/providers/providers-layout.component.html @@ -0,0 +1,44 @@ + +
+
+
+
+ +
+ {{provider.name}} + {{'provider' | i18n}} +
+
+
+ + {{'providerIsDisabled' | i18n}} +
+
+
+ +
+
+
+
+ +
+ diff --git a/bitwarden_license/src/app/providers/providers-layout.component.ts b/bitwarden_license/src/app/providers/providers-layout.component.ts new file mode 100644 index 0000000000..ac63e5dd55 --- /dev/null +++ b/bitwarden_license/src/app/providers/providers-layout.component.ts @@ -0,0 +1,47 @@ +import { + Component, + NgZone, +} from '@angular/core'; + +import { ActivatedRoute } from '@angular/router'; + +import { UserService } from 'jslib-common/abstractions/user.service'; + +import { Provider } from 'jslib-common/models/domain/provider'; + +@Component({ + selector: 'providers-layout', + templateUrl: 'providers-layout.component.html', +}) +export class ProvidersLayoutComponent { + + provider: Provider; + private providerId: string; + + constructor(private route: ActivatedRoute, private userService: UserService) { } + + ngOnInit() { + document.body.classList.remove('layout_frontend'); + this.route.params.subscribe(async params => { + this.providerId = params.providerId; + await this.load(); + }); + } + + async load() { + this.provider = await this.userService.getProvider(this.providerId); + } + + get showMenuBar() { + return true; // TODO: Replace with permissions + } + + get manageRoute(): string { + switch (true) { + case this.provider.canManageUsers: + return 'manage/people'; + case this.provider.canAccessEventLogs: + return 'manage/events'; + } + } +} diff --git a/bitwarden_license/src/app/providers/providers-routing.module.ts b/bitwarden_license/src/app/providers/providers-routing.module.ts new file mode 100644 index 0000000000..f6c4c6d637 --- /dev/null +++ b/bitwarden_license/src/app/providers/providers-routing.module.ts @@ -0,0 +1,117 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { AuthGuardService } from 'jslib-angular/services/auth-guard.service'; +import { Permissions } from 'jslib-common/enums/permissions'; + +import { AddOrganizationComponent } from './clients/add-organization.component'; +import { ClientsComponent } from './clients/clients.component'; +import { CreateOrganizationComponent } from './clients/create-organization.component'; +import { AcceptProviderComponent } from './manage/accept-provider.component'; +import { EventsComponent } from './manage/events.component'; +import { ManageComponent } from './manage/manage.component'; +import { PeopleComponent } from './manage/people.component'; +import { ProvidersLayoutComponent } from './providers-layout.component'; +import { SettingsComponent } from './settings/settings.component'; +import { SetupProviderComponent } from './setup/setup-provider.component'; +import { SetupComponent } from './setup/setup.component'; + +import { FrontendLayoutComponent } from 'src/app/layouts/frontend-layout.component'; + +import { ProviderGuardService } from './services/provider-guard.service'; +import { ProviderTypeGuardService } from './services/provider-type-guard.service'; +import { AccountComponent } from './settings/account.component'; + +const routes: Routes = [ + { + path: '', + component: FrontendLayoutComponent, + children: [ + { + path: 'setup-provider', + component: SetupProviderComponent, + data: { titleId: 'setupProvider' }, + }, + { + path: 'accept-provider', + component: AcceptProviderComponent, + data: { titleId: 'acceptProvider' }, + }, + ], + }, + { + path: '', + canActivate: [AuthGuardService], + children: [ + { + path: 'setup', + component: SetupComponent, + }, + { + path: ':providerId', + component: ProvidersLayoutComponent, + canActivate: [ProviderGuardService], + children: [ + { path: '', pathMatch: 'full', redirectTo: 'clients' }, + { path: 'clients/create', component: CreateOrganizationComponent }, + { path: 'clients', component: ClientsComponent, data: { titleId: 'clients' } }, + { + path: 'manage', + component: ManageComponent, + children: [ + { + path: '', + pathMatch: 'full', + redirectTo: 'people', + }, + { + path: 'people', + component: PeopleComponent, + canActivate: [ProviderTypeGuardService], + data: { + titleId: 'people', + permissions: [Permissions.ManageUsers], + }, + }, + { + path: 'events', + component: EventsComponent, + canActivate: [ProviderTypeGuardService], + data: { + titleId: 'eventLogs', + permissions: [Permissions.AccessEventLogs], + }, + }, + ], + }, + { + path: 'settings', + component: SettingsComponent, + children: [ + { + path: '', + pathMatch: 'full', + redirectTo: 'account', + }, + { + path: 'account', + component: AccountComponent, + canActivate: [ProviderTypeGuardService], + data: { + titleId: 'myProvider', + permissions: [Permissions.ManageProvider], + }, + }, + ], + }, + ], + }, + ], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class ProvidersRoutingModule { } diff --git a/bitwarden_license/src/app/providers/providers.module.ts b/bitwarden_license/src/app/providers/providers.module.ts new file mode 100644 index 0000000000..df69031810 --- /dev/null +++ b/bitwarden_license/src/app/providers/providers.module.ts @@ -0,0 +1,62 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { ProviderGuardService } from './services/provider-guard.service'; +import { ProviderTypeGuardService } from './services/provider-type-guard.service'; +import { ProviderService } from './services/provider.service'; + +import { ProvidersLayoutComponent } from './providers-layout.component'; +import { ProvidersRoutingModule } from './providers-routing.module'; + +import { AddOrganizationComponent } from './clients/add-organization.component'; +import { ClientsComponent } from './clients/clients.component'; +import { CreateOrganizationComponent } from './clients/create-organization.component'; + +import { AcceptProviderComponent } from './manage/accept-provider.component'; +import { BulkConfirmComponent } from './manage/bulk/bulk-confirm.component'; +import { BulkRemoveComponent } from './manage/bulk/bulk-remove.component'; +import { EventsComponent } from './manage/events.component'; +import { ManageComponent } from './manage/manage.component'; +import { PeopleComponent } from './manage/people.component'; +import { UserAddEditComponent } from './manage/user-add-edit.component'; + +import { AccountComponent } from './settings/account.component'; +import { SettingsComponent } from './settings/settings.component'; + +import { SetupProviderComponent } from './setup/setup-provider.component'; +import { SetupComponent } from './setup/setup.component'; + +import { OssModule } from 'src/app/oss.module'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + OssModule, + ProvidersRoutingModule, + ], + declarations: [ + AcceptProviderComponent, + AccountComponent, + AddOrganizationComponent, + BulkConfirmComponent, + BulkRemoveComponent, + ClientsComponent, + CreateOrganizationComponent, + EventsComponent, + ManageComponent, + PeopleComponent, + ProvidersLayoutComponent, + SettingsComponent, + SetupComponent, + SetupProviderComponent, + UserAddEditComponent, + ], + providers: [ + ProviderService, + ProviderGuardService, + ProviderTypeGuardService, + ], +}) +export class ProvidersModule {} diff --git a/bitwarden_license/src/app/providers/services/provider-guard.service.ts b/bitwarden_license/src/app/providers/services/provider-guard.service.ts new file mode 100644 index 0000000000..5f1c5fc47d --- /dev/null +++ b/bitwarden_license/src/app/providers/services/provider-guard.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivate, + Router, +} from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; + +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { UserService } from 'jslib-common/abstractions/user.service'; + +@Injectable() +export class ProviderGuardService implements CanActivate { + constructor(private userService: UserService, private router: Router, + private toasterService: ToasterService, private i18nService: I18nService) { } + + async canActivate(route: ActivatedRouteSnapshot) { + const provider = await this.userService.getProvider(route.params.providerId); + if (provider == null) { + this.router.navigate(['/']); + return false; + } + if (!provider.isProviderAdmin && !provider.enabled) { + this.toasterService.popAsync('error', null, this.i18nService.t('providerIsDisabled')); + this.router.navigate(['/']); + return false; + } + + return true; + } +} diff --git a/bitwarden_license/src/app/providers/services/provider-type-guard.service.ts b/bitwarden_license/src/app/providers/services/provider-type-guard.service.ts new file mode 100644 index 0000000000..c2f9c3e4c3 --- /dev/null +++ b/bitwarden_license/src/app/providers/services/provider-type-guard.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivate, + Router, +} from '@angular/router'; + +import { UserService } from 'jslib-common/abstractions/user.service'; + +import { Permissions } from 'jslib-common/enums/permissions'; + +@Injectable() +export class ProviderTypeGuardService implements CanActivate { + constructor(private userService: UserService, private router: Router) { } + + async canActivate(route: ActivatedRouteSnapshot) { + const provider = await this.userService.getProvider(route.params.providerId); + const permissions = route.data == null ? null : route.data.permissions as Permissions[]; + + if ( + (permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) || + (permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) || + (permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers) + ) { + return true; + } + + this.router.navigate(['/providers', provider.id]); + return false; + } +} diff --git a/bitwarden_license/src/app/providers/services/provider.service.ts b/bitwarden_license/src/app/providers/services/provider.service.ts new file mode 100644 index 0000000000..2f60fffdc3 --- /dev/null +++ b/bitwarden_license/src/app/providers/services/provider.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { CryptoService } from 'jslib-common/abstractions/crypto.service'; +import { SyncService } from 'jslib-common/abstractions/sync.service'; + +import { ProviderAddOrganizationRequest } from 'jslib-common/models/request/provider/providerAddOrganizationRequest'; + +@Injectable() +export class ProviderService { + constructor(private cryptoService: CryptoService, private syncService: SyncService, private apiService: ApiService) {} + + async addOrganizationToProvider(providerId: string, organizationId: string) { + const orgKey = await this.cryptoService.getOrgKey(organizationId); + const providerKey = await this.cryptoService.getProviderKey(providerId); + + const encryptedOrgKey = await this.cryptoService.encrypt(orgKey.key, providerKey); + + const request = new ProviderAddOrganizationRequest(); + request.organizationId = organizationId; + request.key = encryptedOrgKey.encryptedString; + + const response = await this.apiService.postProviderAddOrganization(providerId, request); + await this.syncService.fullSync(true); + return response; + } + + async detachOrganizastion(providerId: string, organizationId: string): Promise { + await this.apiService.deleteProviderOrganization(providerId, organizationId); + await this.syncService.fullSync(true); + } +} diff --git a/bitwarden_license/src/app/providers/settings/account.component.html b/bitwarden_license/src/app/providers/settings/account.component.html new file mode 100644 index 0000000000..70aa5c6e3a --- /dev/null +++ b/bitwarden_license/src/app/providers/settings/account.component.html @@ -0,0 +1,35 @@ + +
+ + {{'loading' | i18n}} +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ +
diff --git a/bitwarden_license/src/app/providers/settings/account.component.ts b/bitwarden_license/src/app/providers/settings/account.component.ts new file mode 100644 index 0000000000..7e26eed4fd --- /dev/null +++ b/bitwarden_license/src/app/providers/settings/account.component.ts @@ -0,0 +1,62 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ToasterService } from 'angular2-toaster'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { LogService } from 'jslib-common/abstractions/log.service'; +import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { SyncService } from 'jslib-common/abstractions/sync.service'; + +import { ProviderUpdateRequest } from 'jslib-common/models/request/provider/providerUpdateRequest'; + +import { ProviderResponse } from 'jslib-common/models/response/provider/providerResponse'; + +@Component({ + selector: 'provider-account', + templateUrl: 'account.component.html', +}) +export class AccountComponent { + selfHosted = false; + loading = true; + provider: ProviderResponse; + formPromise: Promise; + taxFormPromise: Promise; + + private providerId: string; + + constructor(private apiService: ApiService, private i18nService: I18nService, + private toasterService: ToasterService, private route: ActivatedRoute, + private syncService: SyncService, private platformUtilsService: PlatformUtilsService, + private logService: LogService) { } + + async ngOnInit() { + this.selfHosted = this.platformUtilsService.isSelfHost(); + this.route.parent.parent.params.subscribe(async params => { + this.providerId = params.providerId; + try { + this.provider = await this.apiService.getProvider(this.providerId); + } catch (e) { + this.logService.error(`Handled exception: ${e}`); + } + }); + this.loading = false; + } + + async submit() { + try { + const request = new ProviderUpdateRequest(); + request.name = this.provider.name; + request.businessName = this.provider.businessName; + request.billingEmail = this.provider.billingEmail; + + this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => { + return this.syncService.fullSync(true); + }); + await this.formPromise; + this.toasterService.popAsync('success', null, this.i18nService.t('providerUpdated')); + } catch (e) { + this.logService.error(`Handled exception: ${e}`); + } + } +} diff --git a/bitwarden_license/src/app/providers/settings/settings.component.html b/bitwarden_license/src/app/providers/settings/settings.component.html new file mode 100644 index 0000000000..552f4fc0da --- /dev/null +++ b/bitwarden_license/src/app/providers/settings/settings.component.html @@ -0,0 +1,17 @@ +
+
+
+
+
{{'settings' | i18n}}
+ +
+
+
+ +
+
+
diff --git a/bitwarden_license/src/app/providers/settings/settings.component.ts b/bitwarden_license/src/app/providers/settings/settings.component.ts new file mode 100644 index 0000000000..0b8f2820ed --- /dev/null +++ b/bitwarden_license/src/app/providers/settings/settings.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { UserService } from 'jslib-common/abstractions/user.service'; + +@Component({ + selector: 'provider-settings', + templateUrl: 'settings.component.html', +}) +export class SettingsComponent { + constructor(private route: ActivatedRoute, private userService: UserService, + private platformUtilsService: PlatformUtilsService) { } + + ngOnInit() { + this.route.parent.params.subscribe(async params => { + const provider = await this.userService.getProvider(params.providerId); + }); + } +} diff --git a/bitwarden_license/src/app/providers/setup/setup-provider.component.html b/bitwarden_license/src/app/providers/setup/setup-provider.component.html new file mode 100644 index 0000000000..00280e9ed6 --- /dev/null +++ b/bitwarden_license/src/app/providers/setup/setup-provider.component.html @@ -0,0 +1,31 @@ +
+
+ +

+ + {{'loading' | i18n}} +

+
+
+
+
+
+

{{'setupProvider' | i18n}}

+
+
+

{{'setupProviderLoginDesc' | i18n}}

+
+ +
+
+
+
+
diff --git a/bitwarden_license/src/app/providers/setup/setup-provider.component.ts b/bitwarden_license/src/app/providers/setup/setup-provider.component.ts new file mode 100644 index 0000000000..e1ea5670a5 --- /dev/null +++ b/bitwarden_license/src/app/providers/setup/setup-provider.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +import { BaseAcceptComponent } from 'src/app/common/base.accept.component'; + +@Component({ + selector: 'app-setup-provider', + templateUrl: 'setup-provider.component.html', +}) +export class SetupProviderComponent extends BaseAcceptComponent { + + failedShortMessage = 'inviteAcceptFailedShort'; + failedMessage = 'inviteAcceptFailed'; + + requiredParameters = ['providerId', 'email', 'token']; + + async authedHandler(qParams: any) { + this.router.navigate(['/providers/setup'], {queryParams: qParams}); + } + + // tslint:disable-next-line + async unauthedHandler(qParams: any) {} +} diff --git a/bitwarden_license/src/app/providers/setup/setup.component.html b/bitwarden_license/src/app/providers/setup/setup.component.html new file mode 100644 index 0000000000..1f62512ced --- /dev/null +++ b/bitwarden_license/src/app/providers/setup/setup.component.html @@ -0,0 +1,38 @@ + +
+ +

{{'setupProviderDesc' | i18n}}

+ +
+

{{'generalInformation' | i18n}}

+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+ + +
+
+
+ diff --git a/bitwarden_license/src/app/providers/setup/setup.component.ts b/bitwarden_license/src/app/providers/setup/setup.component.ts new file mode 100644 index 0000000000..b3c68c0b25 --- /dev/null +++ b/bitwarden_license/src/app/providers/setup/setup.component.ts @@ -0,0 +1,96 @@ +import { + Component, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + Toast, + ToasterService, +} from 'angular2-toaster'; + +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { CryptoService } from 'jslib-common/abstractions/crypto.service'; + +import { ValidationService } from 'jslib-angular/services/validation.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { SyncService } from 'jslib-common/abstractions/sync.service'; +import { ProviderSetupRequest } from 'jslib-common/models/request/provider/providerSetupRequest'; + +@Component({ + selector: 'provider-setup', + templateUrl: 'setup.component.html', +}) +export class SetupComponent implements OnInit { + loading = true; + authed = false; + email: string; + formPromise: Promise; + + providerId: string; + token: string; + name: string; + billingEmail: string; + businessName: string; + + constructor(private router: Router, private toasterService: ToasterService, + private i18nService: I18nService, private route: ActivatedRoute, + private cryptoService: CryptoService, private apiService: ApiService, + private syncService: SyncService, private validationService: ValidationService) { } + + ngOnInit() { + document.body.classList.remove('layout_frontend'); + let fired = false; + this.route.queryParams.subscribe(async qParams => { + if (fired) { + return; + } + fired = true; + const error = qParams.providerId == null || qParams.email == null || qParams.token == null; + + if (error) { + const toast: Toast = { + type: 'error', + title: null, + body: this.i18nService.t('emergencyInviteAcceptFailed'), + timeout: 10000, + }; + this.toasterService.popAsync(toast); + this.router.navigate(['/']); + } else { + this.providerId = qParams.providerId; + this.token = qParams.token; + } + }); + } + + async submit() { + this.formPromise = this.doSubmit(); + await this.formPromise; + this.formPromise = null; + } + + async doSubmit() { + try { + const shareKey = await this.cryptoService.makeShareKey(); + const key = shareKey[0].encryptedString; + + const request = new ProviderSetupRequest(); + request.name = this.name; + request.billingEmail = this.billingEmail; + request.businessName = this.businessName; + request.token = this.token; + request.key = key; + + const provider = await this.apiService.postProviderSetup(this.providerId, request); + this.toasterService.popAsync('success', this.i18nService.t('providerSetup')); + await this.syncService.fullSync(true); + + this.router.navigate(['/providers', provider.id]); + } catch (e) { + this.validationService.showError(e); + } + } +} diff --git a/bitwarden_license/webpack.config.js b/bitwarden_license/webpack.config.js index e2aa15890a..44dfe325b4 100644 --- a/bitwarden_license/webpack.config.js +++ b/bitwarden_license/webpack.config.js @@ -1,5 +1,12 @@ +const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; + const webpackConfig = require('../webpack.config'); webpackConfig.entry['app/main'] = './bitwarden_license/src/app/main.ts'; +webpackConfig.plugins[webpackConfig.plugins.length -1] = new AngularCompilerPlugin({ + tsConfigPath: 'tsconfig.json', + entryModule: 'bitwarden_license/src/app/app.module#AppModule', + sourceMap: true, +}); module.exports = webpackConfig; diff --git a/package.json b/package.json index 8a19397479..b262e49658 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "dist:selfhost:oss": "npm run build:selfhost:prod:oss && gulp postdist", "deploy": "npm run dist && gh-pages -d build", "deploy:dev": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git", - "lint": "tslint 'src/**/*.ts' || true", - "lint:fix": "tslint 'src/**/*.ts' --fix" + "lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' || true", + "lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix" }, "devDependencies": { "@angular/compiler-cli": "^11.2.11", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8512425ada..1066149beb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -6,8 +6,8 @@ import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; +import { OssRoutingModule } from './oss-routing.module'; import { OssModule } from './oss.module'; import { ServicesModule } from './services/services.module'; @@ -20,7 +20,7 @@ import { ServicesModule } from './services/services.module'; ToasterModule.forRoot(), InfiniteScrollModule, DragDropModule, - AppRoutingModule, + OssRoutingModule, ], bootstrap: [AppComponent], }) diff --git a/src/app/common/base.events.component.ts b/src/app/common/base.events.component.ts new file mode 100644 index 0000000000..9b86a6853c --- /dev/null +++ b/src/app/common/base.events.component.ts @@ -0,0 +1,157 @@ +import { Directive } from '@angular/core'; +import { ToasterService } from 'angular2-toaster'; + +import { ExportService } from 'jslib-common/abstractions/export.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; + +import { EventView } from 'jslib-common/models/view/eventView'; + +import { ListResponse } from 'jslib-common/models/response'; +import { EventResponse } from 'jslib-common/models/response/eventResponse'; + +import { LogService } from 'jslib-common/abstractions'; +import { EventService } from 'src/app/services/event.service'; + +@Directive() +export abstract class BaseEventsComponent { + loading = true; + loaded = false; + events: EventView[]; + start: string; + end: string; + dirtyDates: boolean = true; + continuationToken: string; + refreshPromise: Promise; + exportPromise: Promise; + morePromise: Promise; + + abstract readonly exportFileName: string; + + constructor(protected eventService: EventService, protected i18nService: I18nService, + protected toasterService: ToasterService, protected exportService: ExportService, + protected platformUtilsService: PlatformUtilsService, protected logService: LogService) { + const defaultDates = this.eventService.getDefaultDateFilters(); + this.start = defaultDates[0]; + this.end = defaultDates[1]; + } + + async exportEvents() { + if (this.appApiPromiseUnfulfilled() || this.dirtyDates) { + return; + } + + this.loading = true; + + const dates = this.parseDates(); + if (dates == null) { + return; + } + + try { + this.exportPromise = this.export(dates[0], dates[1]); + + await this.exportPromise; + } catch (e) { + this.logService.error(`Handled exception: ${e}`); + } + + this.exportPromise = null; + this.loading = false; + } + + async loadEvents(clearExisting: boolean) { + if (this.appApiPromiseUnfulfilled()) { + return; + } + + const dates = this.parseDates(); + if (dates == null) { + return; + } + + this.loading = true; + let events: EventView[] = []; + try { + const promise = this.loadAndParseEvents(dates[0], dates[1], clearExisting ? null : this.continuationToken); + if (clearExisting) { + this.refreshPromise = promise; + } else { + this.morePromise = promise; + } + const result = await promise; + this.continuationToken = result.continuationToken; + events = result.events; + } catch (e) { + this.logService.error(`Handled exception: ${e}`); + } + + if (!clearExisting && this.events != null && this.events.length > 0) { + this.events = this.events.concat(events); + } else { + this.events = events; + } + + this.dirtyDates = false; + this.loading = false; + this.morePromise = null; + this.refreshPromise = null; + } + + protected abstract requestEvents(startDate: string, endDate: string, continuationToken: string): Promise>; + protected abstract getUserName(r: EventResponse, userId: string): { name: string, email: string }; + + protected async loadAndParseEvents(startDate: string, endDate: string, continuationToken: string) { + const response = await this.requestEvents(startDate, endDate, continuationToken); + + const events = await Promise.all(response.data.map(async r => { + const userId = r.actingUserId == null ? r.userId : r.actingUserId; + const eventInfo = await this.eventService.getEventInfo(r); + const user = this.getUserName(r, userId); + return new EventView({ + message: eventInfo.message, + humanReadableMessage: eventInfo.humanReadableMessage, + appIcon: eventInfo.appIcon, + appName: eventInfo.appName, + userId: userId, + userName: user != null ? user.name : this.i18nService.t('unknown'), + userEmail: user != null ? user.email : '', + date: r.date, + ip: r.ipAddress, + type: r.type, + }); + })); + return { continuationToken: response.continuationToken, events: events }; + } + + protected parseDates() { + let dates: string[] = null; + try { + dates = this.eventService.formatDateFilters(this.start, this.end); + } catch (e) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidDateRange')); + return null; + } + return dates; + } + + protected appApiPromiseUnfulfilled() { + return this.refreshPromise != null || this.morePromise != null || this.exportPromise != null; + } + + private async export(start: string, end: string) { + let continuationToken = this.continuationToken; + let events = [].concat(this.events); + + while (continuationToken != null) { + const result = await this.loadAndParseEvents(start, end, continuationToken); + continuationToken = result.continuationToken; + events = events.concat(result.events); + } + + const data = await this.exportService.getEventExport(events); + const fileName = this.exportService.getFileName(this.exportFileName, 'csv'); + this.platformUtilsService.saveFile(window, data, { type: 'text/plain' }, fileName); + } +} diff --git a/src/app/common/base.people.component.ts b/src/app/common/base.people.component.ts new file mode 100644 index 0000000000..0fe943d2f2 --- /dev/null +++ b/src/app/common/base.people.component.ts @@ -0,0 +1,314 @@ +import { + ComponentFactoryResolver, + Directive, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ToasterService } from 'angular2-toaster'; + +import { ValidationService } from 'jslib-angular/services/validation.service'; +import { ApiService } from 'jslib-common/abstractions/api.service'; +import { CryptoService } from 'jslib-common/abstractions/crypto.service'; +import { I18nService } from 'jslib-common/abstractions/i18n.service'; +import { LogService } from 'jslib-common/abstractions/log.service'; +import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; +import { SearchService } from 'jslib-common/abstractions/search.service'; +import { StorageService } from 'jslib-common/abstractions/storage.service'; + +import { ConstantsService } from 'jslib-common/services/constants.service'; + +import { SearchPipe } from 'jslib-angular/pipes/search.pipe'; +import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe'; + +import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType'; +import { OrganizationUserType } from 'jslib-common/enums/organizationUserType'; +import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType'; +import { ProviderUserType } from 'jslib-common/enums/providerUserType'; + +import { ListResponse } from 'jslib-common/models/response/listResponse'; +import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/response/organizationUserResponse'; +import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse'; + +import { Utils } from 'jslib-common/misc/utils'; + +import { ModalComponent } from '../modal.component'; +import { UserConfirmComponent } from '../organizations/manage/user-confirm.component'; + +type StatusType = OrganizationUserStatusType | ProviderUserStatusType; + +const MaxCheckedCount = 500; + +@Directive() +export abstract class BasePeopleComponent { + + @ViewChild('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef; + + get allCount() { + return this.allUsers != null ? this.allUsers.length : 0; + } + + get invitedCount() { + return this.statusMap.has(this.userStatusType.Invited) ? + this.statusMap.get(this.userStatusType.Invited).length : 0; + } + + get acceptedCount() { + return this.statusMap.has(this.userStatusType.Accepted) ? + this.statusMap.get(this.userStatusType.Accepted).length : 0; + } + + get confirmedCount() { + return this.statusMap.has(this.userStatusType.Confirmed) ? + this.statusMap.get(this.userStatusType.Confirmed).length : 0; + } + + get showConfirmUsers(): boolean { + return this.allUsers != null && this.statusMap != null && this.allUsers.length > 1 && + this.confirmedCount > 0 && this.confirmedCount < 3 && this.acceptedCount > 0; + } + + get showBulkConfirmUsers(): boolean { + return this.acceptedCount > 0; + } + + abstract userType: typeof OrganizationUserType | typeof ProviderUserType; + abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType; + + loading = true; + statusMap = new Map(); + status: StatusType; + users: UserType[] = []; + pagedUsers: UserType[] = []; + searchText: string; + actionPromise: Promise; + + protected allUsers: UserType[] = []; + + protected didScroll = false; + protected pageSize = 100; + protected modal: ModalComponent = null; + + private pagedUsersCount = 0; + + constructor(protected apiService: ApiService, private searchService: SearchService, + protected i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + protected toasterService: ToasterService, protected cryptoService: CryptoService, + private storageService: StorageService, protected validationService: ValidationService, + protected componentFactoryResolver: ComponentFactoryResolver, private logService: LogService, + private searchPipe: SearchPipe, protected userNamePipe: UserNamePipe) { } + + abstract edit(user: UserType): void; + abstract getUsers(): Promise>; + abstract deleteUser(id: string): Promise; + abstract reinviteUser(id: string): Promise; + abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise; + + async load() { + const response = await this.getUsers(); + this.statusMap.clear(); + this.allUsers = response.data != null && response.data.length > 0 ? response.data : []; + this.allUsers.sort(Utils.getSortFunction(this.i18nService, 'email')); + this.allUsers.forEach(u => { + if (!this.statusMap.has(u.status)) { + this.statusMap.set(u.status, [u]); + } else { + this.statusMap.get(u.status).push(u); + } + }); + this.filter(this.status); + this.loading = false; + } + + filter(status: StatusType) { + this.status = status; + if (this.status != null) { + this.users = this.statusMap.get(this.status); + } else { + this.users = this.allUsers; + } + // Reset checkbox selecton + this.selectAll(false); + this.resetPaging(); + } + + loadMore() { + if (!this.users || this.users.length <= this.pageSize) { + return; + } + const pagedLength = this.pagedUsers.length; + let pagedSize = this.pageSize; + if (pagedLength === 0 && this.pagedUsersCount > this.pageSize) { + pagedSize = this.pagedUsersCount; + } + if (this.users.length > pagedLength) { + this.pagedUsers = this.pagedUsers.concat(this.users.slice(pagedLength, pagedLength + pagedSize)); + } + this.pagedUsersCount = this.pagedUsers.length; + this.didScroll = this.pagedUsers.length > this.pageSize; + } + + checkUser(user: OrganizationUserUserDetailsResponse, select?: boolean) { + (user as any).checked = select == null ? !(user as any).checked : select; + } + + selectAll(select: boolean) { + if (select) { + this.selectAll(false); + } + + const filteredUsers = this.searchPipe.transform(this.users, this.searchText, 'name', 'email', 'id'); + + const selectCount = select && filteredUsers.length > MaxCheckedCount + ? MaxCheckedCount + : filteredUsers.length; + for (let i = 0; i < selectCount; i++) { + this.checkUser(filteredUsers[i], select); + } + } + + async resetPaging() { + this.pagedUsers = []; + this.loadMore(); + } + + invite() { + this.edit(null); + } + + async remove(user: UserType) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('removeUserConfirmation'), this.userNamePipe.transform(user), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + + if (!confirmed) { + return false; + } + + this.actionPromise = this.deleteUser(user.id); + try { + await this.actionPromise; + this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', this.userNamePipe.transform(user))); + this.removeUser(user); + } catch (e) { + this.validationService.showError(e); + } + this.actionPromise = null; + } + + async reinvite(user: UserType) { + if (this.actionPromise != null) { + return; + } + + this.actionPromise = this.reinviteUser(user.id); + try { + await this.actionPromise; + this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenReinvited', this.userNamePipe.transform(user))); + } catch (e) { + this.validationService.showError(e); + } + this.actionPromise = null; + } + + async confirm(user: UserType) { + function updateUser(self: BasePeopleComponent) { + user.status = self.userStatusType.Confirmed; + const mapIndex = self.statusMap.get(self.userStatusType.Accepted).indexOf(user); + if (mapIndex > -1) { + self.statusMap.get(self.userStatusType.Accepted).splice(mapIndex, 1); + self.statusMap.get(self.userStatusType.Confirmed).push(user); + } + } + + const confirmUser = async (publicKey: Uint8Array) => { + try { + this.actionPromise = this.confirmUser(user, publicKey); + await this.actionPromise; + updateUser(this); + this.toasterService.popAsync('success', null, this.i18nService.t('hasBeenConfirmed', this.userNamePipe.transform(user))); + } catch (e) { + this.validationService.showError(e); + throw e; + } finally { + this.actionPromise = null; + } + }; + + if (this.actionPromise != null) { + return; + } + + try { + const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId); + const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); + + const autoConfirm = await this.storageService.get(ConstantsService.autoConfirmFingerprints); + if (autoConfirm == null || !autoConfirm) { + if (this.modal != null) { + this.modal.close(); + } + + const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); + this.modal = this.confirmModalRef.createComponent(factory).instance; + const childComponent = this.modal.show( + UserConfirmComponent, this.confirmModalRef); + + childComponent.name = this.userNamePipe.transform(user); + childComponent.userId = user != null ? user.userId : null; + childComponent.publicKey = publicKey; + childComponent.onConfirmedUser.subscribe(async () => { + try { + await confirmUser(publicKey); + this.modal.close(); + } catch (e) { + this.logService.error(`Handled exception: ${e}`); + } + }); + + this.modal.onClosed.subscribe(() => { + this.modal = null; + }); + return; + } + + try { + const fingerprint = await this.cryptoService.getFingerprint(user.userId, publicKey.buffer); + this.logService.info(`User's fingerprint: ${fingerprint.join('-')}`); + } catch { } + await confirmUser(publicKey); + } catch (e) { + this.logService.error(`Handled exception: ${e}`); + } + } + + isSearching() { + return this.searchService.isSearchable(this.searchText); + } + + isPaging() { + const searching = this.isSearching(); + if (searching && this.didScroll) { + this.resetPaging(); + } + return !searching && this.users && this.users.length > this.pageSize; + } + + protected getCheckedUsers() { + return this.users.filter(u => (u as any).checked); + } + + protected removeUser(user: UserType) { + let index = this.users.indexOf(user); + if (index > -1) { + this.users.splice(index, 1); + this.resetPaging(); + } + if (this.statusMap.has(user.status)) { + index = this.statusMap.get(user.status).indexOf(user); + if (index > -1) { + this.statusMap.get(user.status).splice(index, 1); + } + } + } + +} \ No newline at end of file diff --git a/src/app/layouts/organization-layout.component.html b/src/app/layouts/organization-layout.component.html index 93f18d5d97..324e02e1cc 100644 --- a/src/app/layouts/organization-layout.component.html +++ b/src/app/layouts/organization-layout.component.html @@ -14,6 +14,12 @@ {{'organizationIsDisabled' | i18n}} +
+
+ + {{'accessingUsingProvider' | i18n : organization.providerName}} +
+