my organization page

This commit is contained in:
Kyle Spearrin 2018-07-16 12:42:49 -04:00
parent a1d52af0ba
commit 786f6953e7
12 changed files with 288 additions and 6 deletions

2
jslib

@ -1 +1 @@
Subproject commit 76ece834d1d18e9cca71bb3c182d2284dae80958
Subproject commit b2c700ad285d9336426284f766b434be8c509f0a

View File

@ -25,6 +25,9 @@ import { GroupsComponent as OrgGroupsComponent } from './organizations/manage/gr
import { ManageComponent as OrgManageComponent } from './organizations/manage/manage.component';
import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/people.component';
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
import { SettingsComponent as OrgSettingsComponent } from './organizations/settings/settings.component';
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
import { ImportComponent as OrgImportComponent } from './organizations/tools/import.component';
import { ToolsComponent as OrgToolsComponent } from './organizations/tools/tools.component';
@ -176,6 +179,16 @@ const routes: Routes = [
{ path: 'people', component: OrgPeopleComponent, data: { titleId: 'people' } },
],
},
{
path: 'settings',
component: OrgSettingsComponent,
canActivate: [OrganizationTypeGuardService],
data: { allowedTypes: [OrganizationUserType.Owner, OrganizationUserType.Admin] },
children: [
{ path: '', pathMatch: 'full', redirectTo: 'account' },
{ path: 'account', component: OrgAccountComponent, data: { titleId: 'myOrganization' } },
],
},
],
},
{ path: '**', redirectTo: '' },

View File

@ -51,6 +51,10 @@ import { PeopleComponent as OrgPeopleComponent } from './organizations/manage/pe
import { UserAddEditComponent as OrgUserAddEditComponent } from './organizations/manage/user-add-edit.component';
import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/manage/user-groups.component';
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component';
import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component';
import { ExportComponent as OrgExportComponent } from './organizations/tools/export.component';
import { ImportComponent as OrgImportComponent } from './organizations/tools/import.component';
import { ToolsComponent as OrgToolsComponent } from './organizations/tools/tools.component';
@ -164,6 +168,7 @@ import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
CreateOrganizationComponent,
DeauthorizeSessionsComponent,
DeleteAccountComponent,
DeleteOrganizationComponent,
DomainRulesComponent,
ExportComponent,
FallbackSrcDirective,
@ -181,6 +186,7 @@ import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
ModalComponent,
NavbarComponent,
OptionsComponent,
OrgAccountComponent,
OrgAddEditComponent,
OrgAttachmentsComponent,
OrgCiphersComponent,
@ -197,6 +203,7 @@ import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
OrgManageCollectionsComponent,
OrgManageComponent,
OrgPeopleComponent,
OrgSettingComponent,
OrgToolsComponent,
OrgUserAddEditComponent,
OrgUserGroupsComponent,
@ -247,6 +254,7 @@ import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
CollectionsComponent,
DeauthorizeSessionsComponent,
DeleteAccountComponent,
DeleteOrganizationComponent,
FolderAddEditComponent,
ModalComponent,
OrgAddEditComponent,

View File

@ -8,20 +8,20 @@
<small class="text-muted">{{'organization' | i18n}}</small>
</div>
</div>
<ul class="nav nav-tabs">
<ul class="nav nav-tabs" *ngIf="organization.isAdmin">
<li class="nav-item">
<a class="nav-link" routerLink="vault" routerLinkActive="active">
<i class="fa fa-lock"></i>
{{'vault' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="organization.isAdmin">
<li class="nav-item">
<a class="nav-link" routerLink="manage" routerLinkActive="active">
<i class="fa fa-sliders"></i>
{{'manage' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="organization.isAdmin">
<li class="nav-item">
<a class="nav-link" routerLink="tools" routerLinkActive="active">
<i class="fa fa-wrench"></i>
{{'tools' | i18n}}

View File

@ -0,0 +1,55 @@
<div class="page-header">
<h1>{{'myOrganization' | i18n}}</h1>
</div>
<div *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted"></i>
</div>
<form *ngIf="org && !loading" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="name">{{'organizationName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="org.name">
</div>
<div class="form-group">
<label for="billingEmail">{{'billingEmail' | i18n}}</label>
<input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="org.billingEmail">
</div>
<div class="form-group">
<label for="businessName">{{'businessName' | i18n}}</label>
<input id="businessName" class="form-control" type="text" name="BusinessName" [(ngModel)]="org.businessName">
</div>
</div>
<div class="col-6">
<app-avatar data="{{org.name}}" dynamic="true" width="75" height="75" fontSize="35"></app-avatar>
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" appBlurClick [disabled]="form.loading">
<i class="fa fa-spinner fa-spin"></i>
<span>{{'save' | i18n}}</span>
</button>
</form>
<div class="secondary-header border-0 mb-0">
<h1>{{'taxInformation' | i18n}}</h1>
</div>
<div class="mb-3" *ngIf="org && (org.businessAddress1 || org.businessTaxNumber)">
<div>{{org.businessAddress1}}</div>
<div>{{org.businessAddress2}}</div>
<div>{{org.businessAddress3}}</div>
<div>{{org.businessCountry}}</div>
<div>{{org.businessTaxNumber}}</div>
</div>
<p>{{'taxInformationDesc' | i18n}}</p>
<a href="https://bitwarden.com/contact/" target="_blank" rel="noopener" class="btn btn-outline-secondary">
{{'contactSupport' | i18n}}
</a>
<div class="secondary-header text-danger border-0 mb-0">
<h1>{{'dangerZone' | i18n}}</h1>
</div>
<div class="card border-danger">
<div class="card-body">
<p>{{'dangerZoneDesc' | i18n}}</p>
<button type="button" class="btn btn-outline-danger" (click)="deleteOrganization()" appBlurClick>{{'deleteOrganization' | i18n}}</button>
</div>
</div>
<ng-template #deleteOrganizationTemplate></ng-template>

View File

@ -0,0 +1,81 @@
import {
Component,
ComponentFactoryResolver,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { SyncService } from 'jslib/abstractions/sync.service';
import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpdateRequest';
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
import { ModalComponent } from '../../modal.component';
import { DeleteOrganizationComponent } from './delete-organization.component';
@Component({
selector: 'app-org-account',
templateUrl: 'account.component.html',
})
export class AccountComponent {
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
loading = true;
org: OrganizationResponse;
formPromise: Promise<any>;
private organizationId: string;
private modal: ModalComponent = null;
constructor(private componentFactoryResolver: ComponentFactoryResolver,
private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private route: ActivatedRoute, private syncService: SyncService) { }
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
try {
this.org = await this.apiService.getOrganization(this.organizationId);
} catch { }
});
this.loading = false;
}
async submit() {
try {
const request = new OrganizationUpdateRequest();
request.name = this.org.name;
request.businessName = this.org.businessName;
request.billingEmail = this.org.billingEmail;
this.formPromise = this.apiService.putOrganization(this.organizationId, request).then(() => {
return this.syncService.fullSync(true);
});
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Updated Organization Settings' });
this.toasterService.popAsync('success', null, this.i18nService.t('organizationUpdated'));
} catch { }
}
deleteOrganization() {
if (this.modal != null) {
this.modal.close();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
this.modal = this.deleteModalRef.createComponent(factory).instance;
const childComponent = this.modal.show<DeleteOrganizationComponent>(
DeleteOrganizationComponent, this.deleteModalRef);
childComponent.organizationId = this.organizationId;
this.modal.onClosed.subscribe(async () => {
this.modal = null;
});
}
}

View File

@ -0,0 +1,26 @@
<div class="modal fade">
<div class="modal-dialog">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="modal-header">
<h2 class="modal-title">{{'deleteOrganization' | i18n}}</h2>
<button type="button" class="close" data-dismiss="modal" attr.aria-label="{{'close' | i18n}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>{{'deleteOrganizationDesc' | i18n}}</p>
<app-callout type="warning">{{'deleteOrganizationWarning' | i18n}}</app-callout>
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control" [(ngModel)]="masterPassword" required
appAutofocus appInputVerbatim>
</div>
<div class="modal-footer">
<button appBlurClick type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin"></i>
<span>{{'deleteOrganization' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,45 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoService } from 'jslib/abstractions/crypto.service';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
@Component({
selector: 'app-delete-organization',
templateUrl: 'delete-organization.component.html',
})
export class DeleteOrganizationComponent {
organizationId: string;
masterPassword: string;
formPromise: Promise<any>;
constructor(private apiService: ApiService, private i18nService: I18nService,
private analytics: Angulartics2, private toasterService: ToasterService,
private cryptoService: CryptoService, private router: Router) { }
async submit() {
if (this.masterPassword == null || this.masterPassword === '') {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPassRequired'));
return;
}
const request = new PasswordVerificationRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
try {
this.formPromise = this.apiService.postDeleteOrganization(this.organizationId, request);
await this.formPromise;
this.analytics.eventTrack.next({ action: 'Deleted Organization' });
this.toasterService.popAsync('success', this.i18nService.t('organizationDeleted'),
this.i18nService.t('organizationDeletedDesc'));
this.router.navigate(['/']);
} catch { }
}
}

View File

@ -0,0 +1,20 @@
<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">
{{'myOrganization' | i18n}}
</a>
<a routerLink="billing" class="list-group-item" routerLinkActive="active">
{{'billingAndLicensing' | i18n}}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-org-settings',
templateUrl: 'settings.component.html',
})
export class SettingsComponent { }

View File

@ -11,7 +11,7 @@
{{'options' | i18n}}
</a>
<a routerLink="organizations" class="list-group-item" routerLinkActive="active">
Organizations
{{'organizations' | i18n}}
</a>
<a routerLink="billing" class="list-group-item" routerLinkActive="active" *ngIf="premium">
{{'billingAndLicensing' | i18n}}
@ -23,7 +23,7 @@
{{'twoStepLogin' | i18n}}
</a>
<a routerLink="domain-rules" class="list-group-item" routerLinkActive="active">
Domain Rules
{{'domainRules' | i18n}}
</a>
</div>
</div>

View File

@ -2143,5 +2143,32 @@
},
"deleteRecoverConfirmDesc": {
"message": "You have requested to delete your Bitwarden account. Click the button below to confirm."
},
"myOrganization": {
"message": "My Organization"
},
"deleteOrganization": {
"message": "Delete Organization"
},
"deleteOrganizationDesc": {
"message": "Proceed below to delete this organization and all associated data. Individual user accounts will remain, though they will not be associated to this organization anymore. "
},
"deleteOrganizationWarning": {
"message": "Deleting the organization is permanent. It cannot be undone."
},
"organizationDeleted": {
"message": "Organization Deleted"
},
"organizationDeletedDesc": {
"message": "The organization and all associated data has been deleted."
},
"organizationUpdated": {
"message": "Organization updated"
},
"taxInformation": {
"message": "Tax Information"
},
"taxInformationDesc": {
"message": "Please contact support to provide (or update) tax information for your invoices."
}
}