[Provider] Add support for managing providers (#1014)
This commit is contained in:
parent
ebe08535e0
commit
a94faf06a9
|
@ -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 { }
|
|
@ -6,10 +6,12 @@ import { NgModule } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
import { AppRoutingModule } from '../../../src/app/app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from '../../../src/app/app.component';
|
|
||||||
import { OssModule } from '../../../src/app/oss.module';
|
import { AppComponent } from 'src/app/app.component';
|
||||||
import { ServicesModule } from '../../../src/app/services/services.module';
|
import { OssRoutingModule } from 'src/app/oss-routing.module';
|
||||||
|
import { OssModule } from 'src/app/oss.module';
|
||||||
|
import { ServicesModule } from 'src/app/services/services.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -21,6 +23,7 @@ import { ServicesModule } from '../../../src/app/services/services.module';
|
||||||
InfiniteScrollModule,
|
InfiniteScrollModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
|
OssRoutingModule,
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,7 +6,7 @@ import 'jquery';
|
||||||
import 'popper.js';
|
import 'popper.js';
|
||||||
|
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
require('../../../src/scss/styles.scss');
|
require('src/scss/styles.scss');
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="addTitle">
|
||||||
|
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title" id="addTitle">
|
||||||
|
{{'addExistingOrganization' | i18n}}
|
||||||
|
</h2>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="card-body text-center" *ngIf="loading">
|
||||||
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
{{'loading' | i18n}}
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="!loading">
|
||||||
|
<table class="table table-hover table-list">
|
||||||
|
<tr *ngFor="let o of organizations">
|
||||||
|
<td width="30">
|
||||||
|
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{o.name}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-outline-secondary pull-right" (click)="add(o)" [disabled]="formPromise">Add</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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<any>;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
<div class="page-header d-flex">
|
||||||
|
<h1>{{'clients' | i18n}}</h1>
|
||||||
|
|
||||||
|
<div class="ml-auto d-flex">
|
||||||
|
<div>
|
||||||
|
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||||
|
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||||
|
[(ngModel)]="searchText">
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create">
|
||||||
|
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||||
|
{{'newClientOrganization' | i18n}}
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-outline-primary ml-3" (click)="addExistingOrganization()" *ngIf="showAddExisting">
|
||||||
|
<i class="fa fa-arrow-circle-o-right fa-fw" aria-hidden="true"></i>
|
||||||
|
{{'addExistingOrganization' | i18n}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="loading">
|
||||||
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container
|
||||||
|
*ngIf="!loading && (clients | search:searchText:'organizationName':'id') as searchedClients">
|
||||||
|
<p *ngIf="!searchedClients.length">{{'noClientsInList' | i18n}}</p>
|
||||||
|
<ng-container *ngIf="searchedClients.length">
|
||||||
|
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
|
||||||
|
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let o of searchedClients">
|
||||||
|
<td width="30">
|
||||||
|
<app-avatar [data]="o.organizationName" size="25" [circle]="true" [fontSize]="14"></app-avatar>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a [routerLink]="['/organizations', o.organizationId]">{{o.organizationName}}</a>
|
||||||
|
</td>
|
||||||
|
<td class="table-list-options">
|
||||||
|
<div class="dropdown" appListDropdown>
|
||||||
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||||
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||||
|
appA11yTitle="{{'options' | i18n}}">
|
||||||
|
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
|
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
|
||||||
|
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||||
|
{{'remove' | i18n}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #add></ng-template>
|
|
@ -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<any>;
|
||||||
|
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>(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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>{{'newClientOrganization' | i18n}}</h1>
|
||||||
|
</div>
|
||||||
|
<p>{{'newClientOrganizationDesc' | i18n}}</p>
|
||||||
|
<app-organization-plans [providerId]="providerId"></app-organization-plans>
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||||
|
<div>
|
||||||
|
<img src="/src/images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
|
||||||
|
<p class="text-center">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container" *ngIf="!loading && !authed">
|
||||||
|
<div class="row justify-content-md-center mt-5">
|
||||||
|
<div class="col-5">
|
||||||
|
<p class="lead text-center mb-4">{{'joinProvider' | i18n}}</p>
|
||||||
|
<div class="card d-block">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-center">
|
||||||
|
{{providerName}}
|
||||||
|
<strong class="d-block mt-2">{{email}}</strong>
|
||||||
|
</p>
|
||||||
|
<p>{{'joinProviderDesc' | i18n}}</p>
|
||||||
|
<hr>
|
||||||
|
<div class="d-flex">
|
||||||
|
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
|
||||||
|
{{'logIn' | i18n}}
|
||||||
|
</a>
|
||||||
|
<a routerLink="/register" [queryParams]="{email: email}"
|
||||||
|
class="btn btn-primary btn-block ml-2 mt-0">
|
||||||
|
{{'createAccount' | i18n}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
<div class="page-header d-flex">
|
||||||
|
<h1>{{'eventLogs' | i18n}}</h1>
|
||||||
|
<div class="ml-auto d-flex">
|
||||||
|
<div class="form-inline">
|
||||||
|
<label class="sr-only" for="start">{{'startDate' | i18n}}</label>
|
||||||
|
<input type="datetime-local" class="form-control form-control-sm" id="start"
|
||||||
|
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM"
|
||||||
|
(change)="dirtyDates = true">
|
||||||
|
<span class="mx-2">-</span>
|
||||||
|
<label class="sr-only" for="end">{{'endDate' | i18n}}</label>
|
||||||
|
<input type="datetime-local" class="form-control form-control-sm" id="end"
|
||||||
|
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM"
|
||||||
|
(change)="dirtyDates = true">
|
||||||
|
</div>
|
||||||
|
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
|
||||||
|
[disabled]="loaded && refreshForm.loading">
|
||||||
|
<i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshForm.loading}"></i>
|
||||||
|
{{'refresh' | i18n}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
|
||||||
|
[ngClass]="{loading:exportForm.loading}" (click)="exportEvents()"
|
||||||
|
[disabled]="loaded && exportForm.loading || dirtyDates">
|
||||||
|
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
|
||||||
|
<span>{{'export' | i18n}}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="!loaded">
|
||||||
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="loaded">
|
||||||
|
<p *ngIf="!events || !events.length">{{'noEventsInList' | i18n}}</p>
|
||||||
|
<table class="table table-hover" *ngIf="events && events.length">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="border-top-0" width="210">{{'timestamp' | i18n}}</th>
|
||||||
|
<th class="border-top-0" width="40">
|
||||||
|
<span class="sr-only">{{'device' | i18n}}</span>
|
||||||
|
</th>
|
||||||
|
<th class="border-top-0" width="150">{{'user' | i18n}}</th>
|
||||||
|
<th class="border-top-0">{{'event' | i18n}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let e of events">
|
||||||
|
<td>{{e.date | date:'medium'}}</td>
|
||||||
|
<td>
|
||||||
|
<i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{e.appName}}, {{e.ip}}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span title="{{e.userEmail}}">{{e.userName}}</span>
|
||||||
|
</td>
|
||||||
|
<td [innerHTML]="e.message"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit"
|
||||||
|
(click)="loadEvents(false)" [disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
|
||||||
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span>{{'loadMore' | i18n}}</span>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
|
@ -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<string, any>();
|
||||||
|
private providerUsersIdMap = new Map<string, any>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<div class="container page-content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3">
|
||||||
|
<div class="card" *ngIf="provider">
|
||||||
|
<div class="card-header">{{'manage' | i18n}}</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<a routerLink="people" class="list-group-item" routerLinkActive="active"
|
||||||
|
*ngIf="provider.canManageUsers">
|
||||||
|
{{'people' | i18n}}
|
||||||
|
</a>
|
||||||
|
<a routerLink="events" class="list-group-item" routerLinkActive="active"
|
||||||
|
*ngIf="provider.canAccessEventLogs && accessEvents">
|
||||||
|
{{'eventLogs' | i18n}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
<div class="page-header d-flex">
|
||||||
|
<h1>{{'people' | i18n}}</h1>
|
||||||
|
<div class="ml-auto d-flex">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == null}"
|
||||||
|
(click)="filter(null)">
|
||||||
|
{{'all' | i18n}}
|
||||||
|
<span class="badge badge-pill badge-info" *ngIf="allCount">{{allCount}}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
[ngClass]="{active: status == userStatusType.Invited}"
|
||||||
|
(click)="filter(userStatusType.Invited)">
|
||||||
|
{{'invited' | i18n}}
|
||||||
|
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{invitedCount}}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
[ngClass]="{active: status == userStatusType.Accepted}"
|
||||||
|
(click)="filter(userStatusType.Accepted)">
|
||||||
|
{{'accepted' | i18n}}
|
||||||
|
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<label class="sr-only" for="search">{{'search' | i18n}}</label>
|
||||||
|
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
|
||||||
|
[(ngModel)]="searchText">
|
||||||
|
</div>
|
||||||
|
<div class="dropdown ml-3" appListDropdown>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
|
||||||
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
|
||||||
|
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
|
||||||
|
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
|
||||||
|
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||||
|
{{'reinviteSelected' | i18n}}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item text-success" appStopClick (click)="bulkConfirm()"
|
||||||
|
*ngIf="showBulkConfirmUsers">
|
||||||
|
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||||
|
{{'confirmSelected' | i18n}}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
|
||||||
|
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||||
|
{{'remove' | i18n}}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
||||||
|
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
|
||||||
|
{{'selectAll' | i18n}}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
||||||
|
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
|
||||||
|
{{'unselectAll' | i18n}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
|
||||||
|
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||||
|
{{'inviteUser' | i18n}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="loading">
|
||||||
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container
|
||||||
|
*ngIf="!loading && (isPaging() ? pagedUsers : users | search:searchText:'name':'email':'id') as searchedUsers">
|
||||||
|
<p *ngIf="!searchedUsers.length">{{'noUsersInList' | i18n}}</p>
|
||||||
|
<ng-container *ngIf="searchedUsers.length">
|
||||||
|
<app-callout type="info" title="{{'confirmUsers' | i18n}}" icon="fa-check-circle" *ngIf="showConfirmUsers">
|
||||||
|
{{'providerUsersNeedConfirmed' | i18n}}
|
||||||
|
</app-callout>
|
||||||
|
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
|
||||||
|
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let u of searchedUsers">
|
||||||
|
<td (click)="checkUser(u)" class="table-list-checkbox">
|
||||||
|
<input type="checkbox" [(ngModel)]="u.checked" appStopProp>
|
||||||
|
</td>
|
||||||
|
<td width="30">
|
||||||
|
<app-avatar [data]="u | userName" [email]="u.email" size="25" [circle]="true"
|
||||||
|
[fontSize]="14"></app-avatar>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
|
||||||
|
<span class="badge badge-secondary"
|
||||||
|
*ngIf="u.status === userStatusType.Invited">{{'invited' | i18n}}</span>
|
||||||
|
<span class="badge badge-warning"
|
||||||
|
*ngIf="u.status === userStatusType.Accepted">{{'accepted' | i18n}}</span>
|
||||||
|
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<ng-container *ngIf="u.twoFactorEnabled">
|
||||||
|
<i class="fa fa-lock" title="{{'userUsingTwoStep' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{'userUsingTwoStep' | i18n}}</span>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span *ngIf="u.type === userType.ProviderAdmin">{{'providerAdmin' | i18n}}</span>
|
||||||
|
<span *ngIf="u.type === userType.ServiceUser">{{'serviceUser' | i18n}}</span>
|
||||||
|
<span *ngIf="u.type === userType.Custom">{{'custom' | i18n}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="table-list-options">
|
||||||
|
<div class="dropdown" appListDropdown>
|
||||||
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||||
|
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||||
|
appA11yTitle="{{'options' | i18n}}">
|
||||||
|
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
|
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)"
|
||||||
|
*ngIf="u.status === userStatusType.Invited">
|
||||||
|
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||||
|
{{'resendInvitation' | i18n}}
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)"
|
||||||
|
*ngIf="u.status === userStatusType.Accepted">
|
||||||
|
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||||
|
{{'confirm' | i18n}}
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item" href="#" appStopClick (click)="groups(u)" *ngIf="accessGroups">
|
||||||
|
<i class="fa fa-fw fa-sitemap" aria-hidden="true"></i>
|
||||||
|
{{'groups' | i18n}}
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item" href="#" appStopClick (click)="events(u)"
|
||||||
|
*ngIf="accessEvents && u.status === userStatusType.Confirmed">
|
||||||
|
<i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
|
||||||
|
{{'eventLogs' | i18n}}
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
||||||
|
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
|
||||||
|
{{'remove' | i18n}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #addEdit></ng-template>
|
||||||
|
<ng-template #eventsTemplate></ng-template>
|
||||||
|
<ng-template #confirmTemplate></ng-template>
|
||||||
|
<ng-template #bulkStatusTemplate></ng-template>
|
||||||
|
<ng-template #bulkConfirmTemplate></ng-template>
|
||||||
|
<ng-template #bulkRemoveTemplate></ng-template>
|
|
@ -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<ProviderUserUserDetailsResponse> 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<ListResponse<ProviderUserUserDetailsResponse>> {
|
||||||
|
return this.apiService.getProviderUsers(this.providerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUser(id: string): Promise<any> {
|
||||||
|
return this.apiService.deleteProviderUser(this.providerId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
reinviteUser(id: string): Promise<any> {
|
||||||
|
return this.apiService.postProviderUserReinvite(this.providerId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
|
||||||
|
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>(
|
||||||
|
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>(
|
||||||
|
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<ListResponse<ProviderUserBulkResponse>>, successfullMessage: string) {
|
||||||
|
|
||||||
|
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||||
|
this.modal = this.bulkStatusModalRef.createComponent(factory).instance;
|
||||||
|
const childComponent = this.modal.show<BulkStatusComponent>(
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||||
|
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||||
|
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title" id="userAddEditTitle">
|
||||||
|
{{title}}
|
||||||
|
<small class="text-muted" *ngIf="name">{{name}}</small>
|
||||||
|
</h2>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" *ngIf="loading">
|
||||||
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" *ngIf="!loading">
|
||||||
|
<ng-container *ngIf="!editMode">
|
||||||
|
<p>{{'providerInviteUserDesc' | i18n}}</p>
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label for="emails">{{'email' | i18n}}</label>
|
||||||
|
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required
|
||||||
|
appAutoFocus>
|
||||||
|
<small class="text-muted">{{'inviteMultipleEmailDesc' | i18n : '20'}}</small>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<h3>
|
||||||
|
{{'userType' | i18n}}
|
||||||
|
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
||||||
|
href="https://bitwarden.com/help/article/user-types-access-control/#user-types">
|
||||||
|
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<div class="form-check mt-2 form-check-block">
|
||||||
|
<input class="form-check-input" type="radio" name="userType" id="userTypeServiceUser"
|
||||||
|
[value]="userType.ServiceUser" [(ngModel)]="type">
|
||||||
|
<label class="form-check-label" for="userTypeServiceUser">
|
||||||
|
{{'serviceUser' | i18n}}
|
||||||
|
<small>{{'serviceUserDesc' | i18n}}</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mt-2 form-check-block">
|
||||||
|
<input class="form-check-input" type="radio" name="userType" id="userTypeProviderAdmin"
|
||||||
|
[value]="userType.ProviderAdmin" [(ngModel)]="type">
|
||||||
|
<label class="form-check-label" for="userTypeProviderAdmin">
|
||||||
|
{{'providerAdmin' | i18n}}
|
||||||
|
<small>{{'providerAdminDesc' | i18n}}</small>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span>{{'save' | i18n}}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||||
|
{{'cancel' | i18n}}
|
||||||
|
</button>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||||
|
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
|
||||||
|
[appApiAction]="deletePromise">
|
||||||
|
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
|
||||||
|
title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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<any>;
|
||||||
|
deletePromise: Promise<any>;
|
||||||
|
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 { }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
<app-navbar></app-navbar>
|
||||||
|
<div class="org-nav" *ngIf="provider">
|
||||||
|
<div class="container d-flex">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="my-auto d-flex align-items-center pl-1">
|
||||||
|
<app-avatar [data]="provider.name" size="45" [circle]="true"></app-avatar>
|
||||||
|
<div class="org-name ml-3">
|
||||||
|
<span>{{provider.name}}</span>
|
||||||
|
<small class="text-muted">{{'provider' | i18n}}</small>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
|
{{'providerIsDisabled' | i18n}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="nav nav-tabs" *ngIf="showMenuBar">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" routerLink="clients" routerLinkActive="active">
|
||||||
|
<i class="fa fa-university" aria-hidden="true"></i>
|
||||||
|
{{'clients' | i18n}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
|
||||||
|
<i class="fa fa-sliders" aria-hidden="true"></i>
|
||||||
|
{{'manage' | i18n}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||||
|
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||||
|
{{'settings' | i18n}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container page-content">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { }
|
|
@ -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 {}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<any> {
|
||||||
|
await this.apiService.deleteProviderOrganization(providerId, organizationId);
|
||||||
|
await this.syncService.fullSync(true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>{{'myProvider' | i18n}}</h1>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="loading">
|
||||||
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
|
</div>
|
||||||
|
<form *ngIf="provider && !loading" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">{{'providerName' | i18n}}</label>
|
||||||
|
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="provider.name"
|
||||||
|
[disabled]="selfHosted">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||||
|
<input id="billingEmail" class="form-control" type="text" name="BillingEmail"
|
||||||
|
[(ngModel)]="provider.billingEmail" [disabled]="selfHosted">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="businessName">{{'businessName' | i18n}}</label>
|
||||||
|
<input id="businessName" class="form-control" type="text" name="BusinessName"
|
||||||
|
[(ngModel)]="provider.businessName" [disabled]="selfHosted">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<app-avatar data="{{provider.name}}" dynamic="true" size="75" fontSize="35"></app-avatar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span>{{'save' | i18n}}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
|
@ -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<any>;
|
||||||
|
taxFormPromise: Promise<any>;
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
<div class="container page-content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">{{'settings' | i18n}}</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<a routerLink="account" class="list-group-item" routerLinkActive="active">
|
||||||
|
{{'myProvider' | i18n}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
|
||||||
|
<div>
|
||||||
|
<img src="/src/images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
|
||||||
|
<p class="text-center">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container" *ngIf="!loading && !authed">
|
||||||
|
<div class="row justify-content-md-center mt-5">
|
||||||
|
<div class="col-5">
|
||||||
|
<p class="lead text-center mb-4">{{'setupProvider' | i18n}}</p>
|
||||||
|
<div class="card d-block">
|
||||||
|
<div class="card-body">
|
||||||
|
<p>{{'setupProviderLoginDesc' | i18n}}</p>
|
||||||
|
<hr>
|
||||||
|
<div class="d-flex">
|
||||||
|
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
|
||||||
|
{{'logIn' | i18n}}
|
||||||
|
</a>
|
||||||
|
<a routerLink="/register" [queryParams]="{email: email}"
|
||||||
|
class="btn btn-primary btn-block ml-2 mt-0">
|
||||||
|
{{'createAccount' | i18n}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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) {}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
<app-navbar></app-navbar>
|
||||||
|
<div class="container page-content">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>{{'setupProvider' | i18n}}</h1>
|
||||||
|
</div>
|
||||||
|
<p>{{'setupProviderDesc' | i18n}}</p>
|
||||||
|
|
||||||
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
|
||||||
|
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<label for="name">{{'providerName' | i18n}}</label>
|
||||||
|
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
|
||||||
|
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="billingEmail" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-6">
|
||||||
|
<label for="businessName">{{'businessName' | i18n}}</label>
|
||||||
|
<input id="businessName" class="form-control" type="text" name="BusinessName" [(ngModel)]="businessName">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||||
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span>{{'submit' | i18n}}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
|
||||||
|
{{'cancel' | i18n}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<app-footer></app-footer>
|
|
@ -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<any>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,12 @@
|
||||||
|
const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
|
||||||
|
|
||||||
const webpackConfig = require('../webpack.config');
|
const webpackConfig = require('../webpack.config');
|
||||||
|
|
||||||
webpackConfig.entry['app/main'] = './bitwarden_license/src/app/main.ts';
|
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;
|
module.exports = webpackConfig;
|
||||||
|
|
|
@ -34,8 +34,8 @@
|
||||||
"dist:selfhost:oss": "npm run build:selfhost:prod:oss && gulp postdist",
|
"dist:selfhost:oss": "npm run build:selfhost:prod:oss && gulp postdist",
|
||||||
"deploy": "npm run dist && gh-pages -d build",
|
"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",
|
"deploy:dev": "npm run dist && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
|
||||||
"lint": "tslint 'src/**/*.ts' || true",
|
"lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' || true",
|
||||||
"lint:fix": "tslint 'src/**/*.ts' --fix"
|
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/compiler-cli": "^11.2.11",
|
"@angular/compiler-cli": "^11.2.11",
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { NgModule } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
|
import { OssRoutingModule } from './oss-routing.module';
|
||||||
import { OssModule } from './oss.module';
|
import { OssModule } from './oss.module';
|
||||||
import { ServicesModule } from './services/services.module';
|
import { ServicesModule } from './services/services.module';
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ import { ServicesModule } from './services/services.module';
|
||||||
ToasterModule.forRoot(),
|
ToasterModule.forRoot(),
|
||||||
InfiniteScrollModule,
|
InfiniteScrollModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
AppRoutingModule,
|
OssRoutingModule,
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
|
|
|
@ -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<any>;
|
||||||
|
exportPromise: Promise<any>;
|
||||||
|
morePromise: Promise<any>;
|
||||||
|
|
||||||
|
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<ListResponse<EventResponse>>;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<UserType extends ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse> {
|
||||||
|
|
||||||
|
@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<StatusType, UserType[]>();
|
||||||
|
status: StatusType;
|
||||||
|
users: UserType[] = [];
|
||||||
|
pagedUsers: UserType[] = [];
|
||||||
|
searchText: string;
|
||||||
|
actionPromise: Promise<any>;
|
||||||
|
|
||||||
|
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<ListResponse<UserType>>;
|
||||||
|
abstract deleteUser(id: string): Promise<any>;
|
||||||
|
abstract reinviteUser(id: string): Promise<any>;
|
||||||
|
abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<any>;
|
||||||
|
|
||||||
|
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<UserType>) {
|
||||||
|
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<boolean>(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>(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,6 +14,12 @@
|
||||||
{{'organizationIsDisabled' | i18n}}
|
{{'organizationIsDisabled' | i18n}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ml-3 card border-info text-info bg-transparent" *ngIf="organization.isProviderUser">
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
|
{{'accessingUsingProvider' | i18n : organization.providerName}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav nav-tabs" *ngIf="showMenuBar">
|
<ul class="nav nav-tabs" *ngIf="showMenuBar">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
|
|
@ -11,11 +11,10 @@ import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
import { OrganizationUserBulkConfirmRequest } from 'jslib-common/models/request/organizationUserBulkConfirmRequest';
|
import { OrganizationUserBulkConfirmRequest } from 'jslib-common/models/request/organizationUserBulkConfirmRequest';
|
||||||
import { OrganizationUserBulkRequest } from 'jslib-common/models/request/organizationUserBulkRequest';
|
import { OrganizationUserBulkRequest } from 'jslib-common/models/request/organizationUserBulkRequest';
|
||||||
|
|
||||||
import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/response/organizationUserResponse';
|
|
||||||
|
|
||||||
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
|
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
|
||||||
|
|
||||||
import { Utils } from 'jslib-common/misc/utils';
|
import { Utils } from 'jslib-common/misc/utils';
|
||||||
|
import { BulkUserDetails } from './bulk-status.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-bulk-confirm',
|
selector: 'app-bulk-confirm',
|
||||||
|
@ -24,10 +23,10 @@ import { Utils } from 'jslib-common/misc/utils';
|
||||||
export class BulkConfirmComponent implements OnInit {
|
export class BulkConfirmComponent implements OnInit {
|
||||||
|
|
||||||
@Input() organizationId: string;
|
@Input() organizationId: string;
|
||||||
@Input() users: OrganizationUserUserDetailsResponse[];
|
@Input() users: BulkUserDetails[];
|
||||||
|
|
||||||
excludedUsers: OrganizationUserUserDetailsResponse[];
|
excludedUsers: BulkUserDetails[];
|
||||||
filteredUsers: OrganizationUserUserDetailsResponse[];
|
filteredUsers: BulkUserDetails[];
|
||||||
publicKeys: Map<string, Uint8Array> = new Map();
|
publicKeys: Map<string, Uint8Array> = new Map();
|
||||||
fingerprints: Map<string, string> = new Map();
|
fingerprints: Map<string, string> = new Map();
|
||||||
statuses: Map<string, string> = new Map();
|
statuses: Map<string, string> = new Map();
|
||||||
|
@ -36,19 +35,18 @@ export class BulkConfirmComponent implements OnInit {
|
||||||
done: boolean = false;
|
done: boolean = false;
|
||||||
error: string;
|
error: string;
|
||||||
|
|
||||||
constructor(private cryptoService: CryptoService, private apiService: ApiService,
|
constructor(protected cryptoService: CryptoService, protected apiService: ApiService,
|
||||||
private i18nService: I18nService) { }
|
private i18nService: I18nService) { }
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.excludedUsers = this.users.filter(user => user.status !== OrganizationUserStatusType.Accepted);
|
this.excludedUsers = this.users.filter(u => !this.isAccepted(u));
|
||||||
this.filteredUsers = this.users.filter(user => user.status === OrganizationUserStatusType.Accepted);
|
this.filteredUsers = this.users.filter(u => this.isAccepted(u));
|
||||||
|
|
||||||
if (this.filteredUsers.length <= 0) {
|
if (this.filteredUsers.length <= 0) {
|
||||||
this.done = true;
|
this.done = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = new OrganizationUserBulkRequest(this.filteredUsers.map(user => user.id));
|
const response = await this.getPublicKeys();
|
||||||
const response = await this.apiService.postOrganizationUsersPublicKey(this.organizationId, request);
|
|
||||||
|
|
||||||
for (const entry of response.data) {
|
for (const entry of response.data) {
|
||||||
const publicKey = Utils.fromB64ToArray(entry.key);
|
const publicKey = Utils.fromB64ToArray(entry.key);
|
||||||
|
@ -65,21 +63,20 @@ export class BulkConfirmComponent implements OnInit {
|
||||||
async submit() {
|
async submit() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
const key = await this.getCryptoKey();
|
||||||
const userIdsWithKeys: any[] = [];
|
const userIdsWithKeys: any[] = [];
|
||||||
for (const user of this.filteredUsers) {
|
for (const user of this.filteredUsers) {
|
||||||
const publicKey = this.publicKeys.get(user.id);
|
const publicKey = this.publicKeys.get(user.id);
|
||||||
if (publicKey == null) {
|
if (publicKey == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
|
const encryptedKey = await this.cryptoService.rsaEncrypt(key.key, publicKey.buffer);
|
||||||
userIdsWithKeys.push({
|
userIdsWithKeys.push({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
key: key.encryptedString,
|
key: encryptedKey.encryptedString,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
|
const response = await this.postConfirmRequest(userIdsWithKeys);
|
||||||
const response = await this.apiService.postOrganizationUserBulkConfirm(this.organizationId, request);
|
|
||||||
|
|
||||||
response.data.forEach(entry => {
|
response.data.forEach(entry => {
|
||||||
const error = entry.error !== '' ? entry.error : this.i18nService.t('bulkConfirmMessage');
|
const error = entry.error !== '' ? entry.error : this.i18nService.t('bulkConfirmMessage');
|
||||||
|
@ -92,4 +89,22 @@ export class BulkConfirmComponent implements OnInit {
|
||||||
}
|
}
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected isAccepted(user: BulkUserDetails) {
|
||||||
|
return user.status === OrganizationUserStatusType.Accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getPublicKeys() {
|
||||||
|
const request = new OrganizationUserBulkRequest(this.filteredUsers.map(user => user.id));
|
||||||
|
return await this.apiService.postOrganizationUsersPublicKey(this.organizationId, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getCryptoKey() {
|
||||||
|
return this.cryptoService.getOrgKey(this.organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async postConfirmRequest(userIdsWithKeys: any[]) {
|
||||||
|
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
|
||||||
|
return await this.apiService.postOrganizationUserBulkConfirm(this.organizationId, request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,17 +10,13 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="card-body text-center" *ngIf="loading">
|
|
||||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
|
||||||
{{'loading' | i18n}}
|
|
||||||
</div>
|
|
||||||
<app-callout type="danger" *ngIf="users.length <= 0">
|
<app-callout type="danger" *ngIf="users.length <= 0">
|
||||||
{{'noSelectedUsersApplicable' | i18n}}
|
{{'noSelectedUsersApplicable' | i18n}}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<app-callout type="error" *ngIf="error">
|
<app-callout type="error" *ngIf="error">
|
||||||
{{error}}
|
{{error}}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<ng-container *ngIf="!loading && !done">
|
<ng-container *ngIf="!done">
|
||||||
<app-callout type="warning" *ngIf="users.length > 0 && !error">
|
<app-callout type="warning" *ngIf="users.length > 0 && !error">
|
||||||
{{'removeUsersWarning' | i18n}}
|
{{'removeUsersWarning' | i18n}}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
@ -42,7 +38,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!loading && done">
|
<ng-container *ngIf="done">
|
||||||
<table class="table table-hover table-list">
|
<table class="table table-hover table-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
import { OrganizationUserBulkRequest } from 'jslib-common/models/request/organizationUserBulkRequest';
|
import { OrganizationUserBulkRequest } from 'jslib-common/models/request/organizationUserBulkRequest';
|
||||||
|
|
||||||
import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/response/organizationUserResponse';
|
import { BulkUserDetails } from './bulk-status.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-bulk-remove',
|
selector: 'app-bulk-remove',
|
||||||
|
@ -16,7 +16,7 @@ import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/respons
|
||||||
export class BulkRemoveComponent {
|
export class BulkRemoveComponent {
|
||||||
|
|
||||||
@Input() organizationId: string;
|
@Input() organizationId: string;
|
||||||
@Input() users: OrganizationUserUserDetailsResponse[];
|
@Input() users: BulkUserDetails[];
|
||||||
|
|
||||||
statuses: Map<string, string> = new Map();
|
statuses: Map<string, string> = new Map();
|
||||||
|
|
||||||
|
@ -24,13 +24,12 @@ export class BulkRemoveComponent {
|
||||||
done: boolean = false;
|
done: boolean = false;
|
||||||
error: string;
|
error: string;
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private i18nService: I18nService) { }
|
constructor(protected apiService: ApiService, protected i18nService: I18nService) { }
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const request = new OrganizationUserBulkRequest(this.users.map(user => user.id));
|
const response = await this.deleteUsers();
|
||||||
const response = await this.apiService.deleteManyOrganizationUsers(this.organizationId, request);
|
|
||||||
|
|
||||||
response.data.forEach(entry => {
|
response.data.forEach(entry => {
|
||||||
const error = entry.error !== '' ? entry.error : this.i18nService.t('bulkRemovedMessage');
|
const error = entry.error !== '' ? entry.error : this.i18nService.t('bulkRemovedMessage');
|
||||||
|
@ -43,4 +42,9 @@ export class BulkRemoveComponent {
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async deleteUsers() {
|
||||||
|
const request = new OrganizationUserBulkRequest(this.users.map(user => user.id));
|
||||||
|
return await this.apiService.deleteManyOrganizationUsers(this.organizationId, request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
|
||||||
|
import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
|
||||||
|
|
||||||
import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/response/organizationUserResponse';
|
export interface BulkUserDetails {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
status: OrganizationUserStatusType | ProviderUserStatusType;
|
||||||
|
}
|
||||||
|
|
||||||
type BulkStatusEntry = {
|
type BulkStatusEntry = {
|
||||||
user: OrganizationUserUserDetailsResponse,
|
user: BulkUserDetails,
|
||||||
error: boolean,
|
error: boolean,
|
||||||
message: string,
|
message: string,
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,6 +25,7 @@ export class EntityEventsComponent implements OnInit {
|
||||||
@Input() entity: 'user' | 'cipher';
|
@Input() entity: 'user' | 'cipher';
|
||||||
@Input() entityId: string;
|
@Input() entityId: string;
|
||||||
@Input() organizationId: string;
|
@Input() organizationId: string;
|
||||||
|
@Input() providerId: string;
|
||||||
@Input() showUser = false;
|
@Input() showUser = false;
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
|
@ -81,7 +82,10 @@ export class EntityEventsComponent implements OnInit {
|
||||||
let response: ListResponse<EventResponse>;
|
let response: ListResponse<EventResponse>;
|
||||||
try {
|
try {
|
||||||
let promise: Promise<any>;
|
let promise: Promise<any>;
|
||||||
if (this.entity === 'user') {
|
if (this.entity === 'user' && this.providerId) {
|
||||||
|
promise = this.apiService.getEventsProviderUser(this.providerId, this.entityId,
|
||||||
|
dates[0], dates[1], clearExisting ? null : this.continuationToken);
|
||||||
|
} else if (this.entity === 'user') {
|
||||||
promise = this.apiService.getEventsOrganizationUser(this.organizationId, this.entityId,
|
promise = this.apiService.getEventsOrganizationUser(this.organizationId, this.entityId,
|
||||||
dates[0], dates[1], clearExisting ? null : this.continuationToken);
|
dates[0], dates[1], clearExisting ? null : this.continuationToken);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import {
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
|
||||||
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
|
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
|
||||||
|
@ -11,49 +10,44 @@ import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
import { ExportService } from 'jslib-common/abstractions/export.service';
|
import { ExportService } from 'jslib-common/abstractions/export.service';
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.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 { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||||
import { UserService } from 'jslib-common/abstractions/user.service';
|
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||||
|
|
||||||
import { EventView } from 'jslib-common/models/view/eventView';
|
import { Organization } from 'jslib-common/models/domain/organization';
|
||||||
|
import { EventResponse } from 'jslib-common/models/response/eventResponse';
|
||||||
|
|
||||||
import { EventService } from '../../services/event.service';
|
import { EventService } from '../../services/event.service';
|
||||||
|
|
||||||
|
import { BaseEventsComponent } from '../../common/base.events.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-org-events',
|
selector: 'app-org-events',
|
||||||
templateUrl: 'events.component.html',
|
templateUrl: 'events.component.html',
|
||||||
})
|
})
|
||||||
export class EventsComponent implements OnInit {
|
export class EventsComponent extends BaseEventsComponent implements OnInit {
|
||||||
loading = true;
|
exportFileName: string = 'org-events';
|
||||||
loaded = false;
|
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
events: EventView[];
|
organization: Organization;
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
dirtyDates: boolean = true;
|
|
||||||
continuationToken: string;
|
|
||||||
refreshPromise: Promise<any>;
|
|
||||||
exportPromise: Promise<any>;
|
|
||||||
morePromise: Promise<any>;
|
|
||||||
|
|
||||||
private orgUsersUserIdMap = new Map<string, any>();
|
private orgUsersUserIdMap = new Map<string, any>();
|
||||||
private orgUsersIdMap = new Map<string, any>();
|
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private route: ActivatedRoute, private eventService: EventService,
|
constructor(private apiService: ApiService, private route: ActivatedRoute, eventService: EventService,
|
||||||
private i18nService: I18nService, private toasterService: ToasterService, private userService: UserService,
|
i18nService: I18nService, toasterService: ToasterService, private userService: UserService,
|
||||||
private exportService: ExportService, private platformUtilsService: PlatformUtilsService,
|
exportService: ExportService, platformUtilsService: PlatformUtilsService, private router: Router,
|
||||||
private router: Router, private userNamePipe: UserNamePipe) { }
|
logService: LogService, private userNamePipe: UserNamePipe) {
|
||||||
|
super(eventService, i18nService, toasterService, exportService, platformUtilsService, logService);
|
||||||
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.route.parent.parent.params.subscribe(async params => {
|
this.route.parent.parent.params.subscribe(async params => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
const organization = await this.userService.getOrganization(this.organizationId);
|
this.organization = await this.userService.getOrganization(this.organizationId);
|
||||||
if (organization == null || !organization.useEvents) {
|
if (this.organization == null || !this.organization.useEvents) {
|
||||||
this.router.navigate(['/organizations', this.organizationId]);
|
this.router.navigate(['/organizations', this.organizationId]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const defaultDates = this.eventService.getDefaultDateFilters();
|
|
||||||
this.start = defaultDates[0];
|
|
||||||
this.end = defaultDates[1];
|
|
||||||
await this.load();
|
await this.load();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -62,124 +56,40 @@ export class EventsComponent implements OnInit {
|
||||||
const response = await this.apiService.getOrganizationUsers(this.organizationId);
|
const response = await this.apiService.getOrganizationUsers(this.organizationId);
|
||||||
response.data.forEach(u => {
|
response.data.forEach(u => {
|
||||||
const name = this.userNamePipe.transform(u);
|
const name = this.userNamePipe.transform(u);
|
||||||
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });
|
|
||||||
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.organization.providerId != null && (await this.userService.getProvider(this.organization.providerId)) != null) {
|
||||||
|
const providerUsersResponse = await this.apiService.getProviderUsers(this.organization.providerId);
|
||||||
|
providerUsersResponse.data.forEach(u => {
|
||||||
|
const name = this.userNamePipe.transform(u);
|
||||||
|
this.orgUsersUserIdMap.set(u.userId, { name: `${name} (${this.organization.providerName})`, email: u.email });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await this.loadEvents(true);
|
await this.loadEvents(true);
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportEvents() {
|
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
|
||||||
if (this.appApiPromiseUnfulfilled() || this.dirtyDates) {
|
return this.apiService.getEventsOrganization(this.organizationId, startDate, endDate, continuationToken);
|
||||||
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 { }
|
|
||||||
|
|
||||||
this.exportPromise = null;
|
|
||||||
this.loading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadEvents(clearExisting: boolean) {
|
protected getUserName(r: EventResponse, userId: string) {
|
||||||
if (this.appApiPromiseUnfulfilled()) {
|
if (userId == null) {
|
||||||
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 { }
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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('org-events', 'csv');
|
|
||||||
this.platformUtilsService.saveFile(window, data, { type: 'text/plain' }, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadAndParseEvents(startDate: string, endDate: string, continuationToken: string) {
|
|
||||||
const response = await this.apiService.getEventsOrganization(this.organizationId, 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 = userId != null && this.orgUsersUserIdMap.has(userId) ?
|
|
||||||
this.orgUsersUserIdMap.get(userId) : null;
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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 null;
|
||||||
}
|
}
|
||||||
return dates;
|
|
||||||
}
|
|
||||||
|
|
||||||
private appApiPromiseUnfulfilled() {
|
if (this.orgUsersUserIdMap.has(userId)) {
|
||||||
return this.refreshPromise != null || this.morePromise != null || this.exportPromise != null;
|
return this.orgUsersUserIdMap.get(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.providerId != null && r.providerId === this.organization.providerId) {
|
||||||
|
return {
|
||||||
|
'name': this.organization.providerName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,14 @@
|
||||||
<span class="badge badge-pill badge-info" *ngIf="allCount">{{allCount}}</span>
|
<span class="badge badge-pill badge-info" *ngIf="allCount">{{allCount}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
[ngClass]="{active: status == organizationUserStatusType.Invited}"
|
[ngClass]="{active: status == userStatusType.Invited}"
|
||||||
(click)="filter(organizationUserStatusType.Invited)">
|
(click)="filter(userStatusType.Invited)">
|
||||||
{{'invited' | i18n}}
|
{{'invited' | i18n}}
|
||||||
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{invitedCount}}</span>
|
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{invitedCount}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary"
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
[ngClass]="{active: status == organizationUserStatusType.Accepted}"
|
[ngClass]="{active: status == userStatusType.Accepted}"
|
||||||
(click)="filter(organizationUserStatusType.Accepted)">
|
(click)="filter(userStatusType.Accepted)">
|
||||||
{{'accepted' | i18n}}
|
{{'accepted' | i18n}}
|
||||||
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
|
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -86,9 +86,9 @@
|
||||||
<td>
|
<td>
|
||||||
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
|
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
|
||||||
<span class="badge badge-secondary"
|
<span class="badge badge-secondary"
|
||||||
*ngIf="u.status === organizationUserStatusType.Invited">{{'invited' | i18n}}</span>
|
*ngIf="u.status === userStatusType.Invited">{{'invited' | i18n}}</span>
|
||||||
<span class="badge badge-warning"
|
<span class="badge badge-warning"
|
||||||
*ngIf="u.status === organizationUserStatusType.Accepted">{{'accepted' | i18n}}</span>
|
*ngIf="u.status === userStatusType.Accepted">{{'accepted' | i18n}}</span>
|
||||||
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
|
<small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -102,11 +102,11 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
|
<span *ngIf="u.type === userType.Owner">{{'owner' | i18n}}</span>
|
||||||
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
|
<span *ngIf="u.type === userType.Admin">{{'admin' | i18n}}</span>
|
||||||
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
|
<span *ngIf="u.type === userType.Manager">{{'manager' | i18n}}</span>
|
||||||
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
|
<span *ngIf="u.type === userType.User">{{'user' | i18n}}</span>
|
||||||
<span *ngIf="u.type === organizationUserType.Custom">{{'custom' | i18n}}</span>
|
<span *ngIf="u.type === userType.Custom">{{'custom' | i18n}}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-list-options">
|
<td class="table-list-options">
|
||||||
<div class="dropdown" appListDropdown>
|
<div class="dropdown" appListDropdown>
|
||||||
|
@ -117,12 +117,12 @@
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)"
|
<a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)"
|
||||||
*ngIf="u.status === organizationUserStatusType.Invited">
|
*ngIf="u.status === userStatusType.Invited">
|
||||||
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
|
||||||
{{'resendInvitation' | i18n}}
|
{{'resendInvitation' | i18n}}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)"
|
<a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)"
|
||||||
*ngIf="u.status === organizationUserStatusType.Accepted">
|
*ngIf="u.status === userStatusType.Accepted">
|
||||||
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
<i class="fa fa-fw fa-check" aria-hidden="true"></i>
|
||||||
{{'confirm' | i18n}}
|
{{'confirm' | i18n}}
|
||||||
</a>
|
</a>
|
||||||
|
@ -131,7 +131,7 @@
|
||||||
{{'groups' | i18n}}
|
{{'groups' | i18n}}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item" href="#" appStopClick (click)="events(u)"
|
<a class="dropdown-item" href="#" appStopClick (click)="events(u)"
|
||||||
*ngIf="accessEvents && u.status === organizationUserStatusType.Confirmed">
|
*ngIf="accessEvents && u.status === userStatusType.Confirmed">
|
||||||
<i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
|
<i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
|
||||||
{{'eventLogs' | i18n}}
|
{{'eventLogs' | i18n}}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -14,11 +14,11 @@ import {
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
|
||||||
import { ValidationService } from 'jslib-angular/services/validation.service';
|
import { ValidationService } from 'jslib-angular/services/validation.service';
|
||||||
import { ConstantsService } from 'jslib-common/services/constants.service';
|
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.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 { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||||
import { PolicyService } from 'jslib-common/abstractions/policy.service';
|
import { PolicyService } from 'jslib-common/abstractions/policy.service';
|
||||||
import { SearchService } from 'jslib-common/abstractions/search.service';
|
import { SearchService } from 'jslib-common/abstractions/search.service';
|
||||||
|
@ -37,12 +37,12 @@ import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/respons
|
||||||
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
|
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
|
||||||
import { OrganizationUserType } from 'jslib-common/enums/organizationUserType';
|
import { OrganizationUserType } from 'jslib-common/enums/organizationUserType';
|
||||||
import { PolicyType } from 'jslib-common/enums/policyType';
|
import { PolicyType } from 'jslib-common/enums/policyType';
|
||||||
|
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
|
||||||
|
|
||||||
import { SearchPipe } from 'jslib-angular/pipes/search.pipe';
|
import { SearchPipe } from 'jslib-angular/pipes/search.pipe';
|
||||||
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
|
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
|
||||||
|
|
||||||
import { Utils } from 'jslib-common/misc/utils';
|
import { BasePeopleComponent } from '../../common/base.people.component';
|
||||||
|
|
||||||
import { ModalComponent } from '../../modal.component';
|
import { ModalComponent } from '../../modal.component';
|
||||||
import { BulkConfirmComponent } from './bulk/bulk-confirm.component';
|
import { BulkConfirmComponent } from './bulk/bulk-confirm.component';
|
||||||
import { BulkRemoveComponent } from './bulk/bulk-remove.component';
|
import { BulkRemoveComponent } from './bulk/bulk-remove.component';
|
||||||
|
@ -50,16 +50,13 @@ import { BulkStatusComponent } from './bulk/bulk-status.component';
|
||||||
import { EntityEventsComponent } from './entity-events.component';
|
import { EntityEventsComponent } from './entity-events.component';
|
||||||
import { ResetPasswordComponent } from './reset-password.component';
|
import { ResetPasswordComponent } from './reset-password.component';
|
||||||
import { UserAddEditComponent } from './user-add-edit.component';
|
import { UserAddEditComponent } from './user-add-edit.component';
|
||||||
import { UserConfirmComponent } from './user-confirm.component';
|
|
||||||
import { UserGroupsComponent } from './user-groups.component';
|
import { UserGroupsComponent } from './user-groups.component';
|
||||||
|
|
||||||
const MaxCheckedCount = 500;
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-org-people',
|
selector: 'app-org-people',
|
||||||
templateUrl: 'people.component.html',
|
templateUrl: 'people.component.html',
|
||||||
})
|
})
|
||||||
export class PeopleComponent implements OnInit {
|
export class PeopleComponent extends BasePeopleComponent<OrganizationUserUserDetailsResponse> implements OnInit {
|
||||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||||
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
|
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
|
||||||
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
||||||
|
@ -69,16 +66,11 @@ export class PeopleComponent implements OnInit {
|
||||||
@ViewChild('bulkConfirmTemplate', { read: ViewContainerRef, static: true }) bulkConfirmModalRef: ViewContainerRef;
|
@ViewChild('bulkConfirmTemplate', { read: ViewContainerRef, static: true }) bulkConfirmModalRef: ViewContainerRef;
|
||||||
@ViewChild('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: ViewContainerRef;
|
@ViewChild('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: ViewContainerRef;
|
||||||
|
|
||||||
loading = true;
|
userType = ProviderUserType;
|
||||||
|
userStatusType = OrganizationUserStatusType;
|
||||||
|
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
users: OrganizationUserUserDetailsResponse[];
|
|
||||||
pagedUsers: OrganizationUserUserDetailsResponse[];
|
|
||||||
searchText: string;
|
|
||||||
status: OrganizationUserStatusType = null;
|
status: OrganizationUserStatusType = null;
|
||||||
statusMap = new Map<OrganizationUserStatusType, OrganizationUserUserDetailsResponse[]>();
|
|
||||||
organizationUserType = OrganizationUserType;
|
|
||||||
organizationUserStatusType = OrganizationUserStatusType;
|
|
||||||
actionPromise: Promise<any>;
|
|
||||||
accessEvents = false;
|
accessEvents = false;
|
||||||
accessGroups = false;
|
accessGroups = false;
|
||||||
canResetPassword = false; // User permission (admin/custom)
|
canResetPassword = false; // User permission (admin/custom)
|
||||||
|
@ -87,20 +79,16 @@ export class PeopleComponent implements OnInit {
|
||||||
orgResetPasswordPolicyEnabled = false;
|
orgResetPasswordPolicyEnabled = false;
|
||||||
callingUserType: OrganizationUserType = null;
|
callingUserType: OrganizationUserType = null;
|
||||||
|
|
||||||
protected didScroll = false;
|
constructor(apiService: ApiService, private route: ActivatedRoute,
|
||||||
protected pageSize = 100;
|
i18nService: I18nService, componentFactoryResolver: ComponentFactoryResolver,
|
||||||
|
platformUtilsService: PlatformUtilsService, toasterService: ToasterService,
|
||||||
private pagedUsersCount = 0;
|
cryptoService: CryptoService, private userService: UserService, private router: Router,
|
||||||
private modal: ModalComponent = null;
|
storageService: StorageService, searchService: SearchService,
|
||||||
private allUsers: OrganizationUserUserDetailsResponse[];
|
validationService: ValidationService, private policyService: PolicyService,
|
||||||
|
logService: LogService, searchPipe: SearchPipe, userNamePipe: UserNamePipe, private syncService: SyncService) {
|
||||||
constructor(private apiService: ApiService, private route: ActivatedRoute,
|
super(apiService, searchService, i18nService, platformUtilsService, toasterService, cryptoService,
|
||||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
storageService, validationService, componentFactoryResolver, logService, searchPipe, userNamePipe);
|
||||||
private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService,
|
}
|
||||||
private cryptoService: CryptoService, private userService: UserService, private router: Router,
|
|
||||||
private storageService: StorageService, private searchService: SearchService,
|
|
||||||
private validationService: ValidationService, private policyService: PolicyService,
|
|
||||||
private searchPipe: SearchPipe, private userNamePipe: UserNamePipe, private syncService: SyncService) { }
|
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.route.parent.parent.params.subscribe(async params => {
|
this.route.parent.parent.params.subscribe(async params => {
|
||||||
|
@ -149,21 +137,29 @@ export class PeopleComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const response = await this.apiService.getOrganizationUsers(this.organizationId);
|
|
||||||
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);
|
|
||||||
const policies = await this.policyService.getAll(PolicyType.ResetPassword);
|
const policies = await this.policyService.getAll(PolicyType.ResetPassword);
|
||||||
this.orgResetPasswordPolicyEnabled = policies.some(p => p.organizationId === this.organizationId && p.enabled);
|
this.orgResetPasswordPolicyEnabled = policies.some(p => p.organizationId === this.organizationId && p.enabled);
|
||||||
this.loading = false;
|
super.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsers(): Promise<ListResponse<OrganizationUserUserDetailsResponse>> {
|
||||||
|
return this.apiService.getOrganizationUsers(this.organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUser(id: string): Promise<any> {
|
||||||
|
return this.apiService.deleteOrganizationUser(this.organizationId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
reinviteUser(id: string): Promise<any> {
|
||||||
|
return this.apiService.postOrganizationUserReinvite(this.organizationId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmUser(user: OrganizationUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
|
||||||
|
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||||
|
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
|
||||||
|
const request = new OrganizationUserConfirmRequest();
|
||||||
|
request.key = key.encryptedString;
|
||||||
|
await this.apiService.postOrganizationUserConfirm(this.organizationId, user.id, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
allowResetPassword(orgUser: OrganizationUserUserDetailsResponse): boolean {
|
allowResetPassword(orgUser: OrganizationUserUserDetailsResponse): boolean {
|
||||||
|
@ -193,62 +189,6 @@ export class PeopleComponent implements OnInit {
|
||||||
return this.orgUseResetPassword && orgUser.resetPasswordEnrolled && this.orgResetPasswordPolicyEnabled;
|
return this.orgUseResetPassword && orgUser.resetPasswordEnrolled && this.orgResetPasswordPolicyEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
filter(status: OrganizationUserStatusType) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
get allCount() {
|
|
||||||
return this.allUsers != null ? this.allUsers.length : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get invitedCount() {
|
|
||||||
return this.statusMap.has(OrganizationUserStatusType.Invited) ?
|
|
||||||
this.statusMap.get(OrganizationUserStatusType.Invited).length : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get acceptedCount() {
|
|
||||||
return this.statusMap.has(OrganizationUserStatusType.Accepted) ?
|
|
||||||
this.statusMap.get(OrganizationUserStatusType.Accepted).length : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get confirmedCount() {
|
|
||||||
return this.statusMap.has(OrganizationUserStatusType.Confirmed) ?
|
|
||||||
this.statusMap.get(OrganizationUserStatusType.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
edit(user: OrganizationUserUserDetailsResponse) {
|
edit(user: OrganizationUserUserDetailsResponse) {
|
||||||
if (this.modal != null) {
|
if (this.modal != null) {
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
|
@ -276,10 +216,6 @@ export class PeopleComponent implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
invite() {
|
|
||||||
this.edit(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
groups(user: OrganizationUserUserDetailsResponse) {
|
groups(user: OrganizationUserUserDetailsResponse) {
|
||||||
if (this.modal != null) {
|
if (this.modal != null) {
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
|
@ -302,40 +238,6 @@ export class PeopleComponent implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(user: OrganizationUserUserDetailsResponse) {
|
|
||||||
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.apiService.deleteOrganizationUser(this.organizationId, 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: OrganizationUserUserDetailsResponse) {
|
|
||||||
if (this.actionPromise != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.actionPromise = this.apiService.postOrganizationUserReinvite(this.organizationId, 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 bulkRemove() {
|
async bulkRemove() {
|
||||||
if (this.actionPromise != null) {
|
if (this.actionPromise != null) {
|
||||||
return;
|
return;
|
||||||
|
@ -405,80 +307,6 @@ export class PeopleComponent implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirm(user: OrganizationUserUserDetailsResponse) {
|
|
||||||
function updateUser(self: PeopleComponent) {
|
|
||||||
user.status = OrganizationUserStatusType.Confirmed;
|
|
||||||
const mapIndex = self.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
|
|
||||||
if (mapIndex > -1) {
|
|
||||||
self.statusMap.get(OrganizationUserStatusType.Accepted).splice(mapIndex, 1);
|
|
||||||
self.statusMap.get(OrganizationUserStatusType.Confirmed).push(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmUser = async (publicKey: Uint8Array) => {
|
|
||||||
try {
|
|
||||||
this.actionPromise = this.doConfirmation(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const autoConfirm = await this.storageService.get<boolean>(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>(
|
|
||||||
UserConfirmComponent, this.confirmModalRef);
|
|
||||||
|
|
||||||
childComponent.name = this.userNamePipe.transform(user);
|
|
||||||
childComponent.organizationId = this.organizationId;
|
|
||||||
childComponent.organizationUserId = user != null ? user.id : null;
|
|
||||||
childComponent.userId = user != null ? user.userId : null;
|
|
||||||
childComponent.onConfirmedUser.subscribe(async (publicKey: Uint8Array) => {
|
|
||||||
try {
|
|
||||||
await confirmUser(publicKey);
|
|
||||||
this.modal.close();
|
|
||||||
} catch (e) {
|
|
||||||
// tslint:disable-next-line
|
|
||||||
console.error('Handled exception:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.modal.onClosed.subscribe(() => {
|
|
||||||
this.modal = null;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
|
|
||||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
|
||||||
try {
|
|
||||||
// tslint:disable-next-line
|
|
||||||
console.log('User\'s fingerprint: ' +
|
|
||||||
(await this.cryptoService.getFingerprint(user.userId, publicKey.buffer)).join('-'));
|
|
||||||
} catch { }
|
|
||||||
await confirmUser(publicKey);
|
|
||||||
} catch (e) {
|
|
||||||
// tslint:disable-next-line
|
|
||||||
console.error('Handled exception:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async events(user: OrganizationUserUserDetailsResponse) {
|
async events(user: OrganizationUserUserDetailsResponse) {
|
||||||
if (this.modal != null) {
|
if (this.modal != null) {
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
|
@ -500,23 +328,6 @@ export class PeopleComponent implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetPaging() {
|
|
||||||
this.pagedUsers = [];
|
|
||||||
this.loadMore();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
async resetPassword(user: OrganizationUserUserDetailsResponse) {
|
async resetPassword(user: OrganizationUserUserDetailsResponse) {
|
||||||
if (this.modal != null) {
|
if (this.modal != null) {
|
||||||
this.modal.close();
|
this.modal.close();
|
||||||
|
@ -542,25 +353,6 @@ export class PeopleComponent implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async showBulkStatus(users: OrganizationUserUserDetailsResponse[], filteredUsers: OrganizationUserUserDetailsResponse[],
|
private async showBulkStatus(users: OrganizationUserUserDetailsResponse[], filteredUsers: OrganizationUserUserDetailsResponse[],
|
||||||
request: Promise<ListResponse<OrganizationUserBulkResponse>>, successfullMessage: string) {
|
request: Promise<ListResponse<OrganizationUserBulkResponse>>, successfullMessage: string) {
|
||||||
|
|
||||||
|
@ -611,42 +403,4 @@ export class PeopleComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doConfirmation(user: OrganizationUserUserDetailsResponse, publicKey: Uint8Array) {
|
|
||||||
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
|
||||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
|
|
||||||
const request = new OrganizationUserConfirmRequest();
|
|
||||||
request.key = key.encryptedString;
|
|
||||||
await this.apiService.postOrganizationUserConfirm(this.organizationId, user.id, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeUser(user: OrganizationUserUserDetailsResponse) {
|
|
||||||
let index = this.users.indexOf(user);
|
|
||||||
if (index > -1) {
|
|
||||||
this.users.splice(index, 1);
|
|
||||||
this.resetPaging();
|
|
||||||
}
|
|
||||||
if (this.statusMap.has(OrganizationUserStatusType.Accepted)) {
|
|
||||||
index = this.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
|
|
||||||
if (index > -1) {
|
|
||||||
this.statusMap.get(OrganizationUserStatusType.Accepted).splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.statusMap.has(OrganizationUserStatusType.Invited)) {
|
|
||||||
index = this.statusMap.get(OrganizationUserStatusType.Invited).indexOf(user);
|
|
||||||
if (index > -1) {
|
|
||||||
this.statusMap.get(OrganizationUserStatusType.Invited).splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.statusMap.has(OrganizationUserStatusType.Confirmed)) {
|
|
||||||
index = this.statusMap.get(OrganizationUserStatusType.Confirmed).indexOf(user);
|
|
||||||
if (index > -1) {
|
|
||||||
this.statusMap.get(OrganizationUserStatusType.Confirmed).splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCheckedUsers() {
|
|
||||||
return this.users.filter(u => (u as any).checked);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,9 @@ import {
|
||||||
|
|
||||||
import { ConstantsService } from 'jslib-common/services/constants.service';
|
import { ConstantsService } from 'jslib-common/services/constants.service';
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
|
||||||
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||||
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
import { StorageService } from 'jslib-common/abstractions/storage.service';
|
||||||
|
|
||||||
import { Utils } from 'jslib-common/misc/utils';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-user-confirm',
|
selector: 'app-user-confirm',
|
||||||
templateUrl: 'user-confirm.component.html',
|
templateUrl: 'user-confirm.component.html',
|
||||||
|
@ -21,24 +18,18 @@ import { Utils } from 'jslib-common/misc/utils';
|
||||||
export class UserConfirmComponent implements OnInit {
|
export class UserConfirmComponent implements OnInit {
|
||||||
@Input() name: string;
|
@Input() name: string;
|
||||||
@Input() userId: string;
|
@Input() userId: string;
|
||||||
@Input() organizationUserId: string;
|
@Input() publicKey: Uint8Array;
|
||||||
@Input() organizationId: string;
|
|
||||||
@Output() onConfirmedUser = new EventEmitter();
|
@Output() onConfirmedUser = new EventEmitter();
|
||||||
|
|
||||||
dontAskAgain = false;
|
dontAskAgain = false;
|
||||||
loading = true;
|
loading = true;
|
||||||
fingerprint: string;
|
fingerprint: string;
|
||||||
|
|
||||||
private publicKey: Uint8Array = null;
|
constructor(private cryptoService: CryptoService, private storageService: StorageService) { }
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private cryptoService: CryptoService,
|
|
||||||
private storageService: StorageService) { }
|
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
try {
|
try {
|
||||||
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId);
|
if (this.publicKey != null) {
|
||||||
if (publicKeyResponse != null) {
|
|
||||||
this.publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
|
||||||
const fingerprint = await this.cryptoService.getFingerprint(this.userId, this.publicKey.buffer);
|
const fingerprint = await this.cryptoService.getFingerprint(this.userId, this.publicKey.buffer);
|
||||||
if (fingerprint != null) {
|
if (fingerprint != null) {
|
||||||
this.fingerprint = fingerprint.join('-');
|
this.fingerprint = fingerprint.join('-');
|
||||||
|
@ -57,6 +48,6 @@ export class UserConfirmComponent implements OnInit {
|
||||||
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
|
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onConfirmedUser.emit(this.publicKey);
|
this.onConfirmedUser.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
|
||||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="sub">
|
<ng-container *ngIf="sub">
|
||||||
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription && subscription.cancelled">
|
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription && subscription.cancelled">
|
||||||
{{'subscriptionCanceled' | i18n}}</app-callout>
|
{{'subscriptionCanceled' | i18n}}</app-callout>
|
||||||
|
@ -150,5 +151,12 @@
|
||||||
(onCanceled)="closeStorage(false)" *ngIf="showAdjustStorage"></app-adjust-storage>
|
(onCanceled)="closeStorage(false)" *ngIf="showAdjustStorage"></app-adjust-storage>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="userOrg?.providerId != null">
|
||||||
|
<div class="secondary-header border-0 mb-0">
|
||||||
|
<h1>{{'provider' | i18n}}</h1>
|
||||||
|
</div>
|
||||||
|
{{'yourProviderIs' | i18n : userOrg.providerName}}
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -3,9 +3,9 @@ import {
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
|
||||||
|
import { Organization } from 'jslib-common/models/domain/organization';
|
||||||
import { OrganizationSubscriptionResponse } from 'jslib-common/models/response/organizationSubscriptionResponse';
|
import { OrganizationSubscriptionResponse } from 'jslib-common/models/response/organizationSubscriptionResponse';
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
|
@ -13,6 +13,7 @@ import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
import { MessagingService } from 'jslib-common/abstractions/messaging.service';
|
||||||
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
|
||||||
|
|
||||||
|
import { UserService } from 'jslib-common/abstractions';
|
||||||
import { PlanType } from 'jslib-common/enums/planType';
|
import { PlanType } from 'jslib-common/enums/planType';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -33,12 +34,15 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||||
sub: OrganizationSubscriptionResponse;
|
sub: OrganizationSubscriptionResponse;
|
||||||
selfHosted = false;
|
selfHosted = false;
|
||||||
|
|
||||||
|
userOrg: Organization;
|
||||||
|
|
||||||
cancelPromise: Promise<any>;
|
cancelPromise: Promise<any>;
|
||||||
reinstatePromise: Promise<any>;
|
reinstatePromise: Promise<any>;
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
|
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService, private toasterService: ToasterService,
|
private i18nService: I18nService, private toasterService: ToasterService,
|
||||||
private messagingService: MessagingService, private route: ActivatedRoute) {
|
private messagingService: MessagingService, private route: ActivatedRoute,
|
||||||
|
private userService: UserService) {
|
||||||
this.selfHosted = platformUtilsService.isSelfHost();
|
this.selfHosted = platformUtilsService.isSelfHost();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +58,9 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.userOrg = await this.userService.getOrganization(this.organizationId);
|
||||||
this.sub = await this.apiService.getOrganizationSubscription(this.organizationId);
|
this.sub = await this.apiService.getOrganizationSubscription(this.organizationId);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -438,4 +438,4 @@ const routes: Routes = [
|
||||||
})],
|
})],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule { }
|
export class OssRoutingModule { }
|
|
@ -171,6 +171,8 @@ import { GroupingsComponent } from './vault/groupings.component';
|
||||||
import { ShareComponent } from './vault/share.component';
|
import { ShareComponent } from './vault/share.component';
|
||||||
import { VaultComponent } from './vault/vault.component';
|
import { VaultComponent } from './vault/vault.component';
|
||||||
|
|
||||||
|
import { ProvidersComponent } from './providers/providers.component';
|
||||||
|
|
||||||
import { CalloutComponent } from 'jslib-angular/components/callout.component';
|
import { CalloutComponent } from 'jslib-angular/components/callout.component';
|
||||||
import { IconComponent } from 'jslib-angular/components/icon.component';
|
import { IconComponent } from 'jslib-angular/components/icon.component';
|
||||||
|
|
||||||
|
@ -433,6 +435,22 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||||
VerifyEmailTokenComponent,
|
VerifyEmailTokenComponent,
|
||||||
VerifyRecoverDeleteComponent,
|
VerifyRecoverDeleteComponent,
|
||||||
WeakPasswordsReportComponent,
|
WeakPasswordsReportComponent,
|
||||||
|
ProvidersComponent,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
A11yTitleDirective,
|
||||||
|
AvatarComponent,
|
||||||
|
CalloutComponent,
|
||||||
|
ApiActionDirective,
|
||||||
|
StopClickDirective,
|
||||||
|
StopPropDirective,
|
||||||
|
I18nPipe,
|
||||||
|
SearchPipe,
|
||||||
|
UserNamePipe,
|
||||||
|
ModalComponent,
|
||||||
|
NavbarComponent,
|
||||||
|
FooterComponent,
|
||||||
|
OrganizationPlansComponent,
|
||||||
],
|
],
|
||||||
providers: [DatePipe, SearchPipe, UserNamePipe],
|
providers: [DatePipe, SearchPipe, UserNamePipe],
|
||||||
bootstrap: [],
|
bootstrap: [],
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<ng-container>
|
||||||
|
<p *ngIf="!loaded" class="text-muted">
|
||||||
|
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||||
|
</p>
|
||||||
|
<ng-container *ngIf="loaded">
|
||||||
|
<ul class="fa-ul card-ul carets" *ngIf="providers && providers.length">
|
||||||
|
<li *ngFor="let o of providers">
|
||||||
|
<a [routerLink]="['/providers', o.id]" class="text-body">
|
||||||
|
<i class="fa-li fa fa-caret-right" aria-hidden="true"></i> {{o.name}}
|
||||||
|
<ng-container *ngIf="!o.enabled">
|
||||||
|
<i class="fa fa-exclamation-triangle text-danger" title="{{'organizationIsDisabled' | i18n}}"
|
||||||
|
aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">{{'organizationIsDisabled' | i18n}}</span>
|
||||||
|
</ng-container>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
|
@ -0,0 +1,36 @@
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
import { SyncService } from 'jslib-common/abstractions/sync.service';
|
||||||
|
import { UserService } from 'jslib-common/abstractions/user.service';
|
||||||
|
|
||||||
|
import { Provider } from 'jslib-common/models/domain/provider';
|
||||||
|
|
||||||
|
import { Utils } from 'jslib-common/misc/utils';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-providers',
|
||||||
|
templateUrl: 'providers.component.html',
|
||||||
|
})
|
||||||
|
export class ProvidersComponent implements OnInit {
|
||||||
|
providers: Provider[];
|
||||||
|
loaded: boolean = false;
|
||||||
|
actionPromise: Promise<any>;
|
||||||
|
|
||||||
|
constructor(private userService: UserService, private i18nService: I18nService, private syncService: SyncService) { }
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
await this.syncService.fullSync(false);
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
const providers = await this.userService.getAllProviders();
|
||||||
|
providers.sort(Utils.getSortFunction(this.i18nService, 'name'));
|
||||||
|
this.providers = providers;
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -228,7 +228,23 @@ export class EventService {
|
||||||
|
|
||||||
humanReadableMsg = this.i18nService.t('modifiedPolicyId', p1);
|
humanReadableMsg = this.i18nService.t('modifiedPolicyId', p1);
|
||||||
break;
|
break;
|
||||||
|
// Provider users:
|
||||||
|
case EventType.ProviderUser_Invited:
|
||||||
|
msg = this.i18nService.t('invitedUserId', this.formatProviderUserId(ev));
|
||||||
|
humanReadableMsg = this.i18nService.t('invitedUserId', this.getShortId(ev.providerUserId));
|
||||||
|
break;
|
||||||
|
case EventType.ProviderUser_Confirmed:
|
||||||
|
msg = this.i18nService.t('confirmedUserId', this.formatProviderUserId(ev));
|
||||||
|
humanReadableMsg = this.i18nService.t('confirmedUserId', this.getShortId(ev.providerUserId));
|
||||||
|
break;
|
||||||
|
case EventType.ProviderUser_Updated:
|
||||||
|
msg = this.i18nService.t('editedUserId', this.formatProviderUserId(ev));
|
||||||
|
humanReadableMsg = this.i18nService.t('editedUserId', this.getShortId(ev.providerUserId));
|
||||||
|
break;
|
||||||
|
case EventType.ProviderUser_Removed:
|
||||||
|
msg = this.i18nService.t('removedUserId', this.formatProviderUserId(ev));
|
||||||
|
humanReadableMsg = this.i18nService.t('removedUserId', this.getShortId(ev.providerUserId));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -318,6 +334,14 @@ export class EventService {
|
||||||
return a.outerHTML;
|
return a.outerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatProviderUserId(ev: EventResponse) {
|
||||||
|
const shortId = this.getShortId(ev.providerUserId);
|
||||||
|
const a = this.makeAnchor(shortId);
|
||||||
|
a.setAttribute('href', '#/providers/' + ev.providerId + '/manage/people?search=' + shortId +
|
||||||
|
'&viewEvents=' + ev.providerUserId);
|
||||||
|
return a.outerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
private formatPolicyId(ev: EventResponse) {
|
private formatPolicyId(ev: EventResponse) {
|
||||||
const shortId = this.getShortId(ev.policyId);
|
const shortId = this.getShortId(ev.policyId);
|
||||||
const a = this.makeAnchor(shortId);
|
const a = this.makeAnchor(shortId);
|
||||||
|
|
|
@ -54,6 +54,7 @@ import { UserService } from 'jslib-common/services/user.service';
|
||||||
import { VaultTimeoutService } from 'jslib-common/services/vaultTimeout.service';
|
import { VaultTimeoutService } from 'jslib-common/services/vaultTimeout.service';
|
||||||
import { WebCryptoFunctionService } from 'jslib-common/services/webCryptoFunction.service';
|
import { WebCryptoFunctionService } from 'jslib-common/services/webCryptoFunction.service';
|
||||||
|
|
||||||
|
import { LogService } from 'jslib-common/abstractions';
|
||||||
import { ApiService as ApiServiceAbstraction } from 'jslib-common/abstractions/api.service';
|
import { ApiService as ApiServiceAbstraction } from 'jslib-common/abstractions/api.service';
|
||||||
import { AuditService as AuditServiceAbstraction } from 'jslib-common/abstractions/audit.service';
|
import { AuditService as AuditServiceAbstraction } from 'jslib-common/abstractions/audit.service';
|
||||||
import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service';
|
import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service';
|
||||||
|
@ -223,6 +224,7 @@ export function initFactory(): Function {
|
||||||
{ provide: PolicyServiceAbstraction, useValue: policyService },
|
{ provide: PolicyServiceAbstraction, useValue: policyService },
|
||||||
{ provide: SendServiceAbstraction, useValue: sendService },
|
{ provide: SendServiceAbstraction, useValue: sendService },
|
||||||
{ provide: PasswordRepromptServiceAbstraction, useValue: passwordRepromptService },
|
{ provide: PasswordRepromptServiceAbstraction, useValue: passwordRepromptService },
|
||||||
|
{ provide: LogService, useValue: consoleLogService },
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
useFactory: initFactory,
|
useFactory: initFactory,
|
||||||
|
|
|
@ -10,8 +10,6 @@ import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { ToasterService } from 'angular2-toaster';
|
import { ToasterService } from 'angular2-toaster';
|
||||||
|
|
||||||
import { PaymentMethodType } from 'jslib-common/enums/paymentMethodType';
|
|
||||||
|
|
||||||
import { ApiService } from 'jslib-common/abstractions/api.service';
|
import { ApiService } from 'jslib-common/abstractions/api.service';
|
||||||
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
|
||||||
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
import { I18nService } from 'jslib-common/abstractions/i18n.service';
|
||||||
|
@ -23,8 +21,12 @@ import { UserService } from 'jslib-common/abstractions/user.service';
|
||||||
import { PaymentComponent } from './payment.component';
|
import { PaymentComponent } from './payment.component';
|
||||||
import { TaxInfoComponent } from './tax-info.component';
|
import { TaxInfoComponent } from './tax-info.component';
|
||||||
|
|
||||||
|
import { EncString } from 'jslib-common/models/domain/encString';
|
||||||
|
import { SymmetricCryptoKey } from 'jslib-common/models/domain/symmetricCryptoKey';
|
||||||
|
|
||||||
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
|
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
|
||||||
import { OrganizationUserType } from 'jslib-common/enums/organizationUserType';
|
import { OrganizationUserType } from 'jslib-common/enums/organizationUserType';
|
||||||
|
import { PaymentMethodType } from 'jslib-common/enums/paymentMethodType';
|
||||||
import { PlanType } from 'jslib-common/enums/planType';
|
import { PlanType } from 'jslib-common/enums/planType';
|
||||||
import { PolicyType } from 'jslib-common/enums/policyType';
|
import { PolicyType } from 'jslib-common/enums/policyType';
|
||||||
import { ProductType } from 'jslib-common/enums/productType';
|
import { ProductType } from 'jslib-common/enums/productType';
|
||||||
|
@ -33,7 +35,6 @@ import { OrganizationCreateRequest } from 'jslib-common/models/request/organizat
|
||||||
import { OrganizationKeysRequest } from 'jslib-common/models/request/organizationKeysRequest';
|
import { OrganizationKeysRequest } from 'jslib-common/models/request/organizationKeysRequest';
|
||||||
import { OrganizationUpgradeRequest } from 'jslib-common/models/request/organizationUpgradeRequest';
|
import { OrganizationUpgradeRequest } from 'jslib-common/models/request/organizationUpgradeRequest';
|
||||||
|
|
||||||
import { EncString } from 'jslib-common/models/domain';
|
|
||||||
import { PlanResponse } from 'jslib-common/models/response/planResponse';
|
import { PlanResponse } from 'jslib-common/models/response/planResponse';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -49,6 +50,7 @@ export class OrganizationPlansComponent implements OnInit {
|
||||||
@Input() showCancel = false;
|
@Input() showCancel = false;
|
||||||
@Input() product: ProductType = ProductType.Free;
|
@Input() product: ProductType = ProductType.Free;
|
||||||
@Input() plan: PlanType = PlanType.Free;
|
@Input() plan: PlanType = PlanType.Free;
|
||||||
|
@Input() providerId: string;
|
||||||
@Output() onSuccess = new EventEmitter();
|
@Output() onSuccess = new EventEmitter();
|
||||||
@Output() onCanceled = new EventEmitter();
|
@Output() onCanceled = new EventEmitter();
|
||||||
|
|
||||||
|
@ -237,7 +239,7 @@ export class OrganizationPlansComponent implements OnInit {
|
||||||
if (this.selfHosted) {
|
if (this.selfHosted) {
|
||||||
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
|
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
|
||||||
} else {
|
} else {
|
||||||
orgId = await this.createCloudHosted(key, collectionCt, orgKeys);
|
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, shareKey[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toasterService.popAsync('success', this.i18nService.t('organizationCreated'), this.i18nService.t('organizationReadyToGo'));
|
this.toasterService.popAsync('success', this.i18nService.t('organizationCreated'), this.i18nService.t('organizationReadyToGo'));
|
||||||
|
@ -296,7 +298,7 @@ export class OrganizationPlansComponent implements OnInit {
|
||||||
return this.organizationId;
|
return this.organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createCloudHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
|
private async createCloudHosted(key: string, collectionCt: string, orgKeys: [string, EncString], orgKey: SymmetricCryptoKey) {
|
||||||
const request = new OrganizationCreateRequest();
|
const request = new OrganizationCreateRequest();
|
||||||
request.key = key;
|
request.key = key;
|
||||||
request.collectionName = collectionCt;
|
request.collectionName = collectionCt;
|
||||||
|
@ -327,8 +329,14 @@ export class OrganizationPlansComponent implements OnInit {
|
||||||
request.billingAddressState = this.taxComponent.taxInfo.state;
|
request.billingAddressState = this.taxComponent.taxInfo.state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const response = await this.apiService.postOrganization(request);
|
|
||||||
return response.id;
|
if (this.providerId) {
|
||||||
|
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
|
||||||
|
request.key = (await this.cryptoService.encrypt(orgKey.key, providerKey)).encryptedString;
|
||||||
|
return (await this.apiService.postProviderCreateOrganization(this.providerId, request)).id;
|
||||||
|
} else {
|
||||||
|
return (await this.apiService.postOrganization(request)).id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
|
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
|
||||||
|
|
|
@ -85,6 +85,18 @@
|
||||||
<app-organizations [vault]="true"></app-organizations>
|
<app-organizations [vault]="true"></app-organizations>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card mt-4" *ngIf="showProviders">
|
||||||
|
<div class="card-header d-flex">
|
||||||
|
{{'providers' | i18n}}
|
||||||
|
<a class="ml-auto" href="https://bitwarden.com/help/article/about-providers/"
|
||||||
|
target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}">
|
||||||
|
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<app-providers></app-providers>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -64,6 +64,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
showBrowserOutdated = false;
|
showBrowserOutdated = false;
|
||||||
showUpdateKey = false;
|
showUpdateKey = false;
|
||||||
showPremiumCallout = false;
|
showPremiumCallout = false;
|
||||||
|
showProviders = false;
|
||||||
deleted: boolean = false;
|
deleted: boolean = false;
|
||||||
trashCleanupWarning: string = null;
|
trashCleanupWarning: string = null;
|
||||||
|
|
||||||
|
@ -92,6 +93,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
this.showPremiumCallout = !this.showVerifyEmail && !canAccessPremium &&
|
this.showPremiumCallout = !this.showVerifyEmail && !canAccessPremium &&
|
||||||
!this.platformUtilsService.isSelfHost();
|
!this.platformUtilsService.isSelfHost();
|
||||||
|
|
||||||
|
this.showProviders = (await this.userService.getAllProviders()).length > 0;
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.groupingsComponent.load(),
|
this.groupingsComponent.load(),
|
||||||
this.organizationsComponent.load(),
|
this.organizationsComponent.load(),
|
||||||
|
|
|
@ -4017,5 +4017,123 @@
|
||||||
},
|
},
|
||||||
"resetPasswordManageUsers": {
|
"resetPasswordManageUsers": {
|
||||||
"message": "Manage Users must also be enabled with the Manage Password Reset permission"
|
"message": "Manage Users must also be enabled with the Manage Password Reset permission"
|
||||||
|
},
|
||||||
|
"setupProvider": {
|
||||||
|
"message": "Setup Provider"
|
||||||
|
},
|
||||||
|
"setupProviderLoginDesc": {
|
||||||
|
"message": "You've been invited to setup a new provider. To continue, you need to log in or create a new Bitwarden account."
|
||||||
|
},
|
||||||
|
"setupProviderDesc": {
|
||||||
|
"message": "You have been invited to create a Provider, please enter the details below to complete the setup. Contact customer support if you have any questions."
|
||||||
|
},
|
||||||
|
"providerName": {
|
||||||
|
"message": "Provider Name"
|
||||||
|
},
|
||||||
|
"providerSetup": {
|
||||||
|
"message": "The provider has been setup."
|
||||||
|
},
|
||||||
|
"clients": {
|
||||||
|
"message": "Clients"
|
||||||
|
},
|
||||||
|
"providerAdmin": {
|
||||||
|
"message": "Provider Admin"
|
||||||
|
},
|
||||||
|
"providerAdminDesc": {
|
||||||
|
"message": "The highest access user that can manage all aspects of your provider, and client organizations."
|
||||||
|
},
|
||||||
|
"serviceUser": {
|
||||||
|
"message": "Service User"
|
||||||
|
},
|
||||||
|
"serviceUserDesc": {
|
||||||
|
"message": "Service users can access and manage all client organizations."
|
||||||
|
},
|
||||||
|
"providerInviteUserDesc": {
|
||||||
|
"message": "Invite a new user to your provider by entering their Bitwarden account email address below. If they do not have a Bitwarden account already, they will be prompted to create a new account."
|
||||||
|
},
|
||||||
|
"joinProvider": {
|
||||||
|
"message": "Join Provider"
|
||||||
|
},
|
||||||
|
"joinProviderDesc": {
|
||||||
|
"message": "You've been invited to join the provider listed above. To accept the invitation, you need to log in or create a new Bitwarden account."
|
||||||
|
},
|
||||||
|
"providerInviteAcceptFailed": {
|
||||||
|
"message": "Unable to accept invitation. Ask a provider admin to send a new invitation."
|
||||||
|
},
|
||||||
|
"providerInviteAcceptedDesc": {
|
||||||
|
"message": "You can access this provider once an administrator confirms your membership. We'll send you an email when that happens."
|
||||||
|
},
|
||||||
|
"providerUsersNeedConfirmed": {
|
||||||
|
"message": "You have users that have accepted their invitation, but still need to be confirmed. Users will not have access to the provider until they are confirmed."
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"message": "Provider"
|
||||||
|
},
|
||||||
|
"newClientOrganization": {
|
||||||
|
"message": "New client organization"
|
||||||
|
},
|
||||||
|
"newClientOrganizationDesc": {
|
||||||
|
"message": "Organizations allow you to share parts of your vault with others as well as manage related users for a specific entity such as a family, small team, or large company."
|
||||||
|
},
|
||||||
|
"addExistingOrganization": {
|
||||||
|
"message": "Add existing organization"
|
||||||
|
},
|
||||||
|
"myProvider": {
|
||||||
|
"message": "My Provider"
|
||||||
|
},
|
||||||
|
"addOrganizationConfirmation": {
|
||||||
|
"message": "Are you sure you want to add $ORGANIZATION$ as a client to $PROVIDER$?",
|
||||||
|
"placeholders": {
|
||||||
|
"organization": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "My Org Name"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "My Provider Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"organizationJoinedProvider": {
|
||||||
|
"message": "Organization was successfully added to the provider"
|
||||||
|
},
|
||||||
|
"accessingUsingProvider": {
|
||||||
|
"message": "Accessing organization using provider $PROVIDER$",
|
||||||
|
"placeholders": {
|
||||||
|
"provider": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "My Provider Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providerIsDisabled": {
|
||||||
|
"message": "Provider is disabled."
|
||||||
|
},
|
||||||
|
"providerUpdated": {
|
||||||
|
"message": "Provider updated"
|
||||||
|
},
|
||||||
|
"yourProviderIs": {
|
||||||
|
"message": "Your provider is $PROVIDER$. They have administrative and billing privileges for your organization.",
|
||||||
|
"placeholders": {
|
||||||
|
"provider": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "My Provider Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detachedOrganization": {
|
||||||
|
"message": "The organization $ORGANIZATION$ has been detached from your provider.",
|
||||||
|
"placeholders": {
|
||||||
|
"organization": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "My Org Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detachOrganizationConfirmation": {
|
||||||
|
"message": "Are you sure you want to detach this organization? The organization will continue to exist but will no longer be managed by the provider."
|
||||||
|
},
|
||||||
|
"add": {
|
||||||
|
"message": "Add"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
],
|
],
|
||||||
"jslib-angular/*": [
|
"jslib-angular/*": [
|
||||||
"jslib/angular/src/*"
|
"jslib/angular/src/*"
|
||||||
|
],
|
||||||
|
"src/*": [
|
||||||
|
"src/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue