1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-26 13:13:22 +00:00

[Provider] Add support for managing providers (#1014)

This commit is contained in:
Oscar Hinton
2021-07-21 11:32:27 +02:00
committed by GitHub
parent ebe08535e0
commit a94faf06a9
64 changed files with 2910 additions and 491 deletions

View File

@@ -6,8 +6,8 @@ import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { OssRoutingModule } from './oss-routing.module';
import { OssModule } from './oss.module';
import { ServicesModule } from './services/services.module';
@@ -20,7 +20,7 @@ import { ServicesModule } from './services/services.module';
ToasterModule.forRoot(),
InfiniteScrollModule,
DragDropModule,
AppRoutingModule,
OssRoutingModule,
],
bootstrap: [AppComponent],
})

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -14,6 +14,12 @@
{{'organizationIsDisabled' | i18n}}
</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>
<ul class="nav nav-tabs" *ngIf="showMenuBar">
<li class="nav-item">

View File

@@ -11,11 +11,10 @@ import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { OrganizationUserBulkConfirmRequest } from 'jslib-common/models/request/organizationUserBulkConfirmRequest';
import { OrganizationUserBulkRequest } from 'jslib-common/models/request/organizationUserBulkRequest';
import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/response/organizationUserResponse';
import { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType';
import { Utils } from 'jslib-common/misc/utils';
import { BulkUserDetails } from './bulk-status.component';
@Component({
selector: 'app-bulk-confirm',
@@ -24,10 +23,10 @@ import { Utils } from 'jslib-common/misc/utils';
export class BulkConfirmComponent implements OnInit {
@Input() organizationId: string;
@Input() users: OrganizationUserUserDetailsResponse[];
@Input() users: BulkUserDetails[];
excludedUsers: OrganizationUserUserDetailsResponse[];
filteredUsers: OrganizationUserUserDetailsResponse[];
excludedUsers: BulkUserDetails[];
filteredUsers: BulkUserDetails[];
publicKeys: Map<string, Uint8Array> = new Map();
fingerprints: Map<string, string> = new Map();
statuses: Map<string, string> = new Map();
@@ -36,19 +35,18 @@ export class BulkConfirmComponent implements OnInit {
done: boolean = false;
error: string;
constructor(private cryptoService: CryptoService, private apiService: ApiService,
constructor(protected cryptoService: CryptoService, protected apiService: ApiService,
private i18nService: I18nService) { }
async ngOnInit() {
this.excludedUsers = this.users.filter(user => user.status !== OrganizationUserStatusType.Accepted);
this.filteredUsers = this.users.filter(user => user.status === OrganizationUserStatusType.Accepted);
this.excludedUsers = this.users.filter(u => !this.isAccepted(u));
this.filteredUsers = this.users.filter(u => this.isAccepted(u));
if (this.filteredUsers.length <= 0) {
this.done = true;
}
const request = new OrganizationUserBulkRequest(this.filteredUsers.map(user => user.id));
const response = await this.apiService.postOrganizationUsersPublicKey(this.organizationId, request);
const response = await this.getPublicKeys();
for (const entry of response.data) {
const publicKey = Utils.fromB64ToArray(entry.key);
@@ -65,21 +63,20 @@ export class BulkConfirmComponent implements OnInit {
async submit() {
this.loading = true;
try {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const key = await this.getCryptoKey();
const userIdsWithKeys: any[] = [];
for (const user of this.filteredUsers) {
const publicKey = this.publicKeys.get(user.id);
if (publicKey == null) {
continue;
}
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
const encryptedKey = await this.cryptoService.rsaEncrypt(key.key, publicKey.buffer);
userIdsWithKeys.push({
id: user.id,
key: key.encryptedString,
key: encryptedKey.encryptedString,
});
}
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
const response = await this.apiService.postOrganizationUserBulkConfirm(this.organizationId, request);
const response = await this.postConfirmRequest(userIdsWithKeys);
response.data.forEach(entry => {
const error = entry.error !== '' ? entry.error : this.i18nService.t('bulkConfirmMessage');
@@ -92,4 +89,22 @@ export class BulkConfirmComponent implements OnInit {
}
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);
}
}

View File

@@ -10,17 +10,13 @@
</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>
<app-callout type="danger" *ngIf="users.length <= 0">
{{'noSelectedUsersApplicable' | i18n}}
</app-callout>
<app-callout type="error" *ngIf="error">
{{error}}
</app-callout>
<ng-container *ngIf="!loading && !done">
<ng-container *ngIf="!done">
<app-callout type="warning" *ngIf="users.length > 0 && !error">
{{'removeUsersWarning' | i18n}}
</app-callout>
@@ -42,7 +38,7 @@
</tr>
</table>
</ng-container>
<ng-container *ngIf="!loading && done">
<ng-container *ngIf="done">
<table class="table table-hover table-list">
<thead>
<tr>

View File

@@ -7,7 +7,7 @@ import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { OrganizationUserBulkRequest } from 'jslib-common/models/request/organizationUserBulkRequest';
import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/response/organizationUserResponse';
import { BulkUserDetails } from './bulk-status.component';
@Component({
selector: 'app-bulk-remove',
@@ -16,7 +16,7 @@ import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/respons
export class BulkRemoveComponent {
@Input() organizationId: string;
@Input() users: OrganizationUserUserDetailsResponse[];
@Input() users: BulkUserDetails[];
statuses: Map<string, string> = new Map();
@@ -24,13 +24,12 @@ export class BulkRemoveComponent {
done: boolean = false;
error: string;
constructor(private apiService: ApiService, private i18nService: I18nService) { }
constructor(protected apiService: ApiService, protected i18nService: I18nService) { }
async submit() {
this.loading = true;
try {
const request = new OrganizationUserBulkRequest(this.users.map(user => user.id));
const response = await this.apiService.deleteManyOrganizationUsers(this.organizationId, request);
const response = await this.deleteUsers();
response.data.forEach(entry => {
const error = entry.error !== '' ? entry.error : this.i18nService.t('bulkRemovedMessage');
@@ -43,4 +42,9 @@ export class BulkRemoveComponent {
this.loading = false;
}
protected async deleteUsers() {
const request = new OrganizationUserBulkRequest(this.users.map(user => user.id));
return await this.apiService.deleteManyOrganizationUsers(this.organizationId, request);
}
}

View File

@@ -1,9 +1,16 @@
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 = {
user: OrganizationUserUserDetailsResponse,
user: BulkUserDetails,
error: boolean,
message: string,
};

View File

@@ -25,6 +25,7 @@ export class EntityEventsComponent implements OnInit {
@Input() entity: 'user' | 'cipher';
@Input() entityId: string;
@Input() organizationId: string;
@Input() providerId: string;
@Input() showUser = false;
loading = true;
@@ -81,7 +82,10 @@ export class EntityEventsComponent implements OnInit {
let response: ListResponse<EventResponse>;
try {
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,
dates[0], dates[1], clearExisting ? null : this.continuationToken);
} else {

View File

@@ -3,7 +3,6 @@ import {
OnInit,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
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 { 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 { 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 { BaseEventsComponent } from '../../common/base.events.component';
@Component({
selector: 'app-org-events',
templateUrl: 'events.component.html',
})
export class EventsComponent implements OnInit {
loading = true;
loaded = false;
export class EventsComponent extends BaseEventsComponent implements OnInit {
exportFileName: string = 'org-events';
organizationId: string;
events: EventView[];
start: string;
end: string;
dirtyDates: boolean = true;
continuationToken: string;
refreshPromise: Promise<any>;
exportPromise: Promise<any>;
morePromise: Promise<any>;
organization: Organization;
private orgUsersUserIdMap = new Map<string, any>();
private orgUsersIdMap = new Map<string, any>();
constructor(private apiService: ApiService, private route: ActivatedRoute, private eventService: EventService,
private i18nService: I18nService, private toasterService: ToasterService, private userService: UserService,
private exportService: ExportService, private platformUtilsService: PlatformUtilsService,
private router: Router, private userNamePipe: UserNamePipe) { }
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.organizationId = params.organizationId;
const organization = await this.userService.getOrganization(this.organizationId);
if (organization == null || !organization.useEvents) {
this.organization = await this.userService.getOrganization(this.organizationId);
if (this.organization == null || !this.organization.useEvents) {
this.router.navigate(['/organizations', this.organizationId]);
return;
}
const defaultDates = this.eventService.getDefaultDateFilters();
this.start = defaultDates[0];
this.end = defaultDates[1];
await this.load();
});
}
@@ -62,124 +56,40 @@ export class EventsComponent implements OnInit {
const response = await this.apiService.getOrganizationUsers(this.organizationId);
response.data.forEach(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 });
});
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);
this.loaded = true;
}
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 { }
this.exportPromise = null;
this.loading = false;
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
return this.apiService.getEventsOrganization(this.organizationId, startDate, endDate, continuationToken);
}
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 { }
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'));
protected getUserName(r: EventResponse, userId: string) {
if (userId == null) {
return null;
}
return dates;
}
private appApiPromiseUnfulfilled() {
return this.refreshPromise != null || this.morePromise != null || this.exportPromise != null;
if (this.orgUsersUserIdMap.has(userId)) {
return this.orgUsersUserIdMap.get(userId);
}
if (r.providerId != null && r.providerId === this.organization.providerId) {
return {
'name': this.organization.providerName,
};
}
return null;
}
}

View File

@@ -8,14 +8,14 @@
<span class="badge badge-pill badge-info" *ngIf="allCount">{{allCount}}</span>
</button>
<button type="button" class="btn btn-outline-secondary"
[ngClass]="{active: status == organizationUserStatusType.Invited}"
(click)="filter(organizationUserStatusType.Invited)">
[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 == organizationUserStatusType.Accepted}"
(click)="filter(organizationUserStatusType.Accepted)">
[ngClass]="{active: status == userStatusType.Accepted}"
(click)="filter(userStatusType.Accepted)">
{{'accepted' | i18n}}
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
</button>
@@ -86,9 +86,9 @@
<td>
<a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
<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"
*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>
</td>
<td>
@@ -102,11 +102,11 @@
</ng-container>
</td>
<td>
<span *ngIf="u.type === organizationUserType.Owner">{{'owner' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
<span *ngIf="u.type === organizationUserType.Custom">{{'custom' | i18n}}</span>
<span *ngIf="u.type === userType.Owner">{{'owner' | i18n}}</span>
<span *ngIf="u.type === userType.Admin">{{'admin' | i18n}}</span>
<span *ngIf="u.type === userType.Manager">{{'manager' | i18n}}</span>
<span *ngIf="u.type === userType.User">{{'user' | i18n}}</span>
<span *ngIf="u.type === userType.Custom">{{'custom' | i18n}}</span>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
@@ -117,12 +117,12 @@
</button>
<div class="dropdown-menu dropdown-menu-right">
<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>
{{'resendInvitation' | i18n}}
</a>
<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>
{{'confirm' | i18n}}
</a>
@@ -131,7 +131,7 @@
{{'groups' | i18n}}
</a>
<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>
{{'eventLogs' | i18n}}
</a>

View File

@@ -14,11 +14,11 @@ import {
import { ToasterService } from 'angular2-toaster';
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 { 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 { PolicyService } from 'jslib-common/abstractions/policy.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 { OrganizationUserType } from 'jslib-common/enums/organizationUserType';
import { PolicyType } from 'jslib-common/enums/policyType';
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 { Utils } from 'jslib-common/misc/utils';
import { BasePeopleComponent } from '../../common/base.people.component';
import { ModalComponent } from '../../modal.component';
import { BulkConfirmComponent } from './bulk/bulk-confirm.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 { ResetPasswordComponent } from './reset-password.component';
import { UserAddEditComponent } from './user-add-edit.component';
import { UserConfirmComponent } from './user-confirm.component';
import { UserGroupsComponent } from './user-groups.component';
const MaxCheckedCount = 500;
@Component({
selector: 'app-org-people',
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('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: 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('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: ViewContainerRef;
loading = true;
userType = ProviderUserType;
userStatusType = OrganizationUserStatusType;
organizationId: string;
users: OrganizationUserUserDetailsResponse[];
pagedUsers: OrganizationUserUserDetailsResponse[];
searchText: string;
status: OrganizationUserStatusType = null;
statusMap = new Map<OrganizationUserStatusType, OrganizationUserUserDetailsResponse[]>();
organizationUserType = OrganizationUserType;
organizationUserStatusType = OrganizationUserStatusType;
actionPromise: Promise<any>;
accessEvents = false;
accessGroups = false;
canResetPassword = false; // User permission (admin/custom)
@@ -87,20 +79,16 @@ export class PeopleComponent implements OnInit {
orgResetPasswordPolicyEnabled = false;
callingUserType: OrganizationUserType = null;
protected didScroll = false;
protected pageSize = 100;
private pagedUsersCount = 0;
private modal: ModalComponent = null;
private allUsers: OrganizationUserUserDetailsResponse[];
constructor(private apiService: ApiService, private route: ActivatedRoute,
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
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) { }
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, private policyService: PolicyService,
logService: LogService, searchPipe: SearchPipe, userNamePipe: UserNamePipe, private syncService: SyncService) {
super(apiService, searchService, i18nService, platformUtilsService, toasterService, cryptoService,
storageService, validationService, componentFactoryResolver, logService, searchPipe, userNamePipe);
}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
@@ -149,21 +137,29 @@ export class PeopleComponent implements OnInit {
}
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);
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 {
@@ -193,62 +189,6 @@ export class PeopleComponent implements OnInit {
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) {
if (this.modal != null) {
this.modal.close();
@@ -276,10 +216,6 @@ export class PeopleComponent implements OnInit {
});
}
invite() {
this.edit(null);
}
groups(user: OrganizationUserUserDetailsResponse) {
if (this.modal != null) {
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() {
if (this.actionPromise != null) {
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) {
if (this.modal != null) {
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) {
if (this.modal != null) {
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[],
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);
}
}

View File

@@ -8,12 +8,9 @@ import {
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 { StorageService } from 'jslib-common/abstractions/storage.service';
import { Utils } from 'jslib-common/misc/utils';
@Component({
selector: 'app-user-confirm',
templateUrl: 'user-confirm.component.html',
@@ -21,24 +18,18 @@ import { Utils } from 'jslib-common/misc/utils';
export class UserConfirmComponent implements OnInit {
@Input() name: string;
@Input() userId: string;
@Input() organizationUserId: string;
@Input() organizationId: string;
@Input() publicKey: Uint8Array;
@Output() onConfirmedUser = new EventEmitter();
dontAskAgain = false;
loading = true;
fingerprint: string;
private publicKey: Uint8Array = null;
constructor(private apiService: ApiService, private cryptoService: CryptoService,
private storageService: StorageService) { }
constructor(private cryptoService: CryptoService, private storageService: StorageService) { }
async ngOnInit() {
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(this.userId);
if (publicKeyResponse != null) {
this.publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
if (this.publicKey != null) {
const fingerprint = await this.cryptoService.getFingerprint(this.userId, this.publicKey.buffer);
if (fingerprint != null) {
this.fingerprint = fingerprint.join('-');
@@ -57,6 +48,6 @@ export class UserConfirmComponent implements OnInit {
await this.storageService.save(ConstantsService.autoConfirmFingerprints, true);
}
this.onConfirmedUser.emit(this.publicKey);
this.onConfirmedUser.emit();
}
}

View File

@@ -11,6 +11,7 @@
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
</ng-container>
<ng-container *ngIf="sub">
<app-callout type="warning" title="{{'canceled' | i18n}}" *ngIf="subscription && subscription.cancelled">
{{'subscriptionCanceled' | i18n}}</app-callout>
@@ -150,5 +151,12 @@
(onCanceled)="closeStorage(false)" *ngIf="showAdjustStorage"></app-adjust-storage>
</div>
</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>

View File

@@ -3,9 +3,9 @@ import {
OnInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Organization } from 'jslib-common/models/domain/organization';
import { OrganizationSubscriptionResponse } from 'jslib-common/models/response/organizationSubscriptionResponse';
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 { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions';
import { PlanType } from 'jslib-common/enums/planType';
@Component({
@@ -33,12 +34,15 @@ export class OrganizationSubscriptionComponent implements OnInit {
sub: OrganizationSubscriptionResponse;
selfHosted = false;
userOrg: Organization;
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
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();
}
@@ -54,7 +58,9 @@ export class OrganizationSubscriptionComponent implements OnInit {
if (this.loading) {
return;
}
this.loading = true;
this.userOrg = await this.userService.getOrganization(this.organizationId);
this.sub = await this.apiService.getOrganizationSubscription(this.organizationId);
this.loading = false;
}

View File

@@ -438,4 +438,4 @@ const routes: Routes = [
})],
exports: [RouterModule],
})
export class AppRoutingModule { }
export class OssRoutingModule { }

View File

@@ -171,6 +171,8 @@ import { GroupingsComponent } from './vault/groupings.component';
import { ShareComponent } from './vault/share.component';
import { VaultComponent } from './vault/vault.component';
import { ProvidersComponent } from './providers/providers.component';
import { CalloutComponent } from 'jslib-angular/components/callout.component';
import { IconComponent } from 'jslib-angular/components/icon.component';
@@ -433,6 +435,22 @@ registerLocaleData(localeZhTw, 'zh-TW');
VerifyEmailTokenComponent,
VerifyRecoverDeleteComponent,
WeakPasswordsReportComponent,
ProvidersComponent,
],
exports: [
A11yTitleDirective,
AvatarComponent,
CalloutComponent,
ApiActionDirective,
StopClickDirective,
StopPropDirective,
I18nPipe,
SearchPipe,
UserNamePipe,
ModalComponent,
NavbarComponent,
FooterComponent,
OrganizationPlansComponent,
],
providers: [DatePipe, SearchPipe, UserNamePipe],
bootstrap: [],

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -228,7 +228,23 @@ export class EventService {
humanReadableMsg = this.i18nService.t('modifiedPolicyId', p1);
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:
break;
}
@@ -318,6 +334,14 @@ export class EventService {
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) {
const shortId = this.getShortId(ev.policyId);
const a = this.makeAnchor(shortId);

View File

@@ -54,6 +54,7 @@ import { UserService } from 'jslib-common/services/user.service';
import { VaultTimeoutService } from 'jslib-common/services/vaultTimeout.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 { AuditService as AuditServiceAbstraction } from 'jslib-common/abstractions/audit.service';
import { AuthService as AuthServiceAbstraction } from 'jslib-common/abstractions/auth.service';
@@ -223,6 +224,7 @@ export function initFactory(): Function {
{ provide: PolicyServiceAbstraction, useValue: policyService },
{ provide: SendServiceAbstraction, useValue: sendService },
{ provide: PasswordRepromptServiceAbstraction, useValue: passwordRepromptService },
{ provide: LogService, useValue: consoleLogService },
{
provide: APP_INITIALIZER,
useFactory: initFactory,

View File

@@ -10,8 +10,6 @@ import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { PaymentMethodType } from 'jslib-common/enums/paymentMethodType';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.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 { 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 { OrganizationUserType } from 'jslib-common/enums/organizationUserType';
import { PaymentMethodType } from 'jslib-common/enums/paymentMethodType';
import { PlanType } from 'jslib-common/enums/planType';
import { PolicyType } from 'jslib-common/enums/policyType';
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 { OrganizationUpgradeRequest } from 'jslib-common/models/request/organizationUpgradeRequest';
import { EncString } from 'jslib-common/models/domain';
import { PlanResponse } from 'jslib-common/models/response/planResponse';
@Component({
@@ -49,6 +50,7 @@ export class OrganizationPlansComponent implements OnInit {
@Input() showCancel = false;
@Input() product: ProductType = ProductType.Free;
@Input() plan: PlanType = PlanType.Free;
@Input() providerId: string;
@Output() onSuccess = new EventEmitter();
@Output() onCanceled = new EventEmitter();
@@ -237,7 +239,7 @@ export class OrganizationPlansComponent implements OnInit {
if (this.selfHosted) {
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
} 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'));
@@ -296,7 +298,7 @@ export class OrganizationPlansComponent implements OnInit {
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();
request.key = key;
request.collectionName = collectionCt;
@@ -327,8 +329,14 @@ export class OrganizationPlansComponent implements OnInit {
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]) {

View File

@@ -85,6 +85,18 @@
<app-organizations [vault]="true"></app-organizations>
</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>

View File

@@ -64,6 +64,7 @@ export class VaultComponent implements OnInit, OnDestroy {
showBrowserOutdated = false;
showUpdateKey = false;
showPremiumCallout = false;
showProviders = false;
deleted: boolean = false;
trashCleanupWarning: string = null;
@@ -92,6 +93,8 @@ export class VaultComponent implements OnInit, OnDestroy {
this.showPremiumCallout = !this.showVerifyEmail && !canAccessPremium &&
!this.platformUtilsService.isSelfHost();
this.showProviders = (await this.userService.getAllProviders()).length > 0;
await Promise.all([
this.groupingsComponent.load(),
this.organizationsComponent.load(),

View File

@@ -4017,5 +4017,123 @@
},
"resetPasswordManageUsers": {
"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"
}
}