mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[EC-8] Restructure Tabs (#3109)
* Cherry pick pending PR for tabs component [CL-17] Tabs - Routing * Update organization tabs from 4 to 6 * Create initial 'Members' tab * Create initial 'Groups' tab * Add initial "Reporting" tab * Use correct report label/layout by product type * Create initial 'Billing' tab * Breakup billing payment and billing history pages * Cleanup org routing and nav permission service * More org tab permission cleanup * Refactor organization billing to use a module * Refactor organization reporting to use module * Cherry pick finished/merged tabs component [CL-17] Tabs - Router (#2952) * This partially reverts commit24bb775to fix tracking of people.component.html rename. * Fix people component file rename * Recover lost member page changes * Undo members component rename as it was causing difficult merge conflicts * Fix member and group page container * Remove unnecessary organization lookup * [EC-8] Some PR suggestions * [EC-8] Reuse user billing history for orgs * [EC-8] Renamed user billing history component * [EC-8] Repurpose payment method component Update end user payment method component to be usable for organizations. * [EC-8] Fix missing verify bank condition * [EC-8] Remove org payment method component * [EC-8] Use CL in payment method component * [EC-8] Extend maxWidth Tailwind theme config * [EC-8] Add lazy loading to org reports * [EC-8] Add lazy loading to org billing * [EC-8] Prettier * [EC-8] Cleanup org reporting component redundancy * [EC-8] Use different class for negative margin * [EC-8] Make billing history component "dumb" * Revert "[EC-8] Cleanup org reporting component redundancy" This reverts commiteca337e89b. * [EC-8] Create and export shared reports module * [EC-8] Use shared reports module in orgs * [EC-8] Use takeUntil pattern * [EC-8] Move org reporting module out of old modules folder * [EC-8] Move org billing module out of old modules folder * [EC-8] Fix some remaining merge conflicts * [EC-8] Move maxWidth into 'extend' key for Tailwind config * [EC-8] Remove unused module * [EC-8] Rename org report list component * Prettier Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
<div class="d-flex page-header">
|
||||||
|
<h1>
|
||||||
|
{{ "billingHistory" | i18n }}
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="secondary"
|
||||||
|
(click)="load()"
|
||||||
|
class="tw-ml-auto"
|
||||||
|
*ngIf="firstLoaded"
|
||||||
|
[disabled]="loading"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': loading }" aria-hidden="true"></i>
|
||||||
|
{{ "refresh" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="!firstLoaded && loading">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="billing">
|
||||||
|
<app-billing-history [billing]="billing"></app-billing-history>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { BillingHistoryResponse } from "@bitwarden/common/models/response/billingHistoryResponse";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-org-billing-history-view",
|
||||||
|
templateUrl: "organization-billing-history-view.component.html",
|
||||||
|
})
|
||||||
|
export class OrgBillingHistoryViewComponent implements OnInit {
|
||||||
|
loading = false;
|
||||||
|
firstLoaded = false;
|
||||||
|
billing: BillingHistoryResponse;
|
||||||
|
organizationId: string;
|
||||||
|
|
||||||
|
constructor(private apiService: ApiService, private route: ActivatedRoute) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.route.params.subscribe(async (params) => {
|
||||||
|
this.organizationId = params.organizationId;
|
||||||
|
await this.load();
|
||||||
|
this.firstLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
this.billing = await this.apiService.getOrganizationBilling(this.organizationId);
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
|
import { Permissions } from "@bitwarden/common/enums/permissions";
|
||||||
|
|
||||||
|
import { PaymentMethodComponent } from "../../settings/payment-method.component";
|
||||||
|
import { PermissionsGuard } from "../guards/permissions.guard";
|
||||||
|
import { NavigationPermissionsService } from "../services/navigation-permissions.service";
|
||||||
|
|
||||||
|
import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component";
|
||||||
|
import { OrganizationBillingTabComponent } from "./organization-billing-tab.component";
|
||||||
|
import { OrganizationSubscriptionComponent } from "./organization-subscription.component";
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: OrganizationBillingTabComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: { permissions: NavigationPermissionsService.getPermissions("billing") },
|
||||||
|
children: [
|
||||||
|
{ path: "", pathMatch: "full", redirectTo: "subscription" },
|
||||||
|
{
|
||||||
|
path: "subscription",
|
||||||
|
component: OrganizationSubscriptionComponent,
|
||||||
|
data: { titleId: "subscription" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "payment-method",
|
||||||
|
component: PaymentMethodComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: { titleId: "paymentMethod", permissions: [Permissions.ManageBilling] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "history",
|
||||||
|
component: OrgBillingHistoryViewComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: { titleId: "billingHistory", permissions: [Permissions.ManageBilling] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class OrganizationBillingRoutingModule {}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<div class="container page-content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">{{ "billing" | i18n }}</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<a routerLink="subscription" class="list-group-item" routerLinkActive="active">
|
||||||
|
{{ "subscription" | i18n }}
|
||||||
|
</a>
|
||||||
|
<a routerLink="payment-method" class="list-group-item" routerLinkActive="active">
|
||||||
|
{{ "paymentMethod" | i18n }}
|
||||||
|
</a>
|
||||||
|
<a routerLink="history" class="list-group-item" routerLinkActive="active">
|
||||||
|
{{ "billingHistory" | i18n }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-org-billing-tab",
|
||||||
|
templateUrl: "organization-billing-tab.component.html",
|
||||||
|
})
|
||||||
|
export class OrganizationBillingTabComponent {}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { LooseComponentsModule } from "../../shared/loose-components.module";
|
||||||
|
import { SharedModule } from "../../shared/shared.module";
|
||||||
|
|
||||||
|
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
||||||
|
import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component";
|
||||||
|
import { OrganizationBillingRoutingModule } from "./organization-billing-routing.module";
|
||||||
|
import { OrganizationBillingTabComponent } from "./organization-billing-tab.component";
|
||||||
|
import { OrganizationSubscriptionComponent } from "./organization-subscription.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [SharedModule, LooseComponentsModule, OrganizationBillingRoutingModule],
|
||||||
|
declarations: [
|
||||||
|
BillingSyncApiKeyComponent,
|
||||||
|
OrganizationBillingTabComponent,
|
||||||
|
OrganizationSubscriptionComponent,
|
||||||
|
OrgBillingHistoryViewComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class OrganizationBillingModule {}
|
||||||
@@ -6,34 +6,27 @@
|
|||||||
class="my-auto pl-1"
|
class="my-auto pl-1"
|
||||||
[activeOrganization]="organization"
|
[activeOrganization]="organization"
|
||||||
></app-organization-switcher>
|
></app-organization-switcher>
|
||||||
<ul class="nav nav-tabs">
|
<bit-tab-group class="-tw-mb-px">
|
||||||
<li class="nav-item">
|
<bit-tab-item [route]="['vault']">{{ "vault" | i18n }}</bit-tab-item>
|
||||||
<a class="nav-link" routerLink="vault" routerLinkActive="active">
|
<bit-tab-item *ngIf="showMembersTab" [route]="['members']">{{
|
||||||
<i class="bwi bwi-lock" aria-hidden="true"></i>
|
"members" | i18n
|
||||||
{{ "vault" | i18n }}
|
}}</bit-tab-item>
|
||||||
</a>
|
<bit-tab-item *ngIf="showGroupsTab" [route]="['groups']">{{
|
||||||
</li>
|
"groups" | i18n
|
||||||
<li class="nav-item" *ngIf="showManageTab">
|
}}</bit-tab-item>
|
||||||
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
|
<bit-tab-item *ngIf="showReportsTab" [route]="['reporting']">{{
|
||||||
<i class="bwi bwi-sliders" aria-hidden="true"></i>
|
reportTabLabel | i18n
|
||||||
{{ "manage" | i18n }}
|
}}</bit-tab-item>
|
||||||
</a>
|
<bit-tab-item *ngIf="showBillingTab" [route]="['billing']">{{
|
||||||
</li>
|
"billing" | i18n
|
||||||
<li class="nav-item" *ngIf="showToolsTab">
|
}}</bit-tab-item>
|
||||||
<a class="nav-link" [routerLink]="toolsRoute" routerLinkActive="active">
|
<bit-tab-item *ngIf="showSettingsTab" [route]="['settings']">{{
|
||||||
<i class="bwi bwi-wrench" aria-hidden="true"></i>
|
"settings" | i18n
|
||||||
{{ "tools" | i18n }}
|
}}</bit-tab-item>
|
||||||
</a>
|
</bit-tab-group>
|
||||||
</li>
|
|
||||||
<li class="nav-item" *ngIf="showSettingsTab">
|
|
||||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
|
||||||
<i class="bwi bwi-cogs" aria-hidden="true"></i>
|
|
||||||
{{ "settings" | i18n }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
<app-footer></app-footer>
|
<app-footer></app-footer>
|
||||||
|
|||||||
@@ -50,49 +50,29 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
|||||||
this.organization = await this.organizationService.get(this.organizationId);
|
this.organization = await this.organizationService.get(this.organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showManageTab(): boolean {
|
|
||||||
return NavigationPermissionsService.canAccessManage(this.organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
get showToolsTab(): boolean {
|
|
||||||
return NavigationPermissionsService.canAccessTools(this.organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
get showSettingsTab(): boolean {
|
get showSettingsTab(): boolean {
|
||||||
return NavigationPermissionsService.canAccessSettings(this.organization);
|
return NavigationPermissionsService.canAccessSettings(this.organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
get toolsRoute(): string {
|
get showMembersTab(): boolean {
|
||||||
return this.organization.canAccessImportExport
|
return NavigationPermissionsService.canAccessMembers(this.organization);
|
||||||
? "tools/import"
|
|
||||||
: "tools/exposed-passwords-report";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get manageRoute(): string {
|
get showGroupsTab(): boolean {
|
||||||
let route: string;
|
return (
|
||||||
switch (true) {
|
this.organization.useGroups && NavigationPermissionsService.canAccessGroups(this.organization)
|
||||||
case this.organization.canManageUsers:
|
);
|
||||||
route = "manage/people";
|
}
|
||||||
break;
|
|
||||||
case this.organization.canViewAssignedCollections || this.organization.canViewAllCollections:
|
get showReportsTab(): boolean {
|
||||||
route = "manage/collections";
|
return NavigationPermissionsService.canAccessReporting(this.organization);
|
||||||
break;
|
}
|
||||||
case this.organization.canManageGroups:
|
|
||||||
route = "manage/groups";
|
get showBillingTab(): boolean {
|
||||||
break;
|
return NavigationPermissionsService.canAccessBilling(this.organization);
|
||||||
case this.organization.canManagePolicies:
|
}
|
||||||
route = "manage/policies";
|
|
||||||
break;
|
get reportTabLabel(): string {
|
||||||
case this.organization.canManageSso:
|
return this.organization.useEvents ? "reporting" : "reports";
|
||||||
route = "manage/sso";
|
|
||||||
break;
|
|
||||||
case this.organization.canManageScim:
|
|
||||||
route = "manage/scim";
|
|
||||||
break;
|
|
||||||
case this.organization.canAccessEventLogs:
|
|
||||||
route = "manage/events";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return route;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,79 @@
|
|||||||
<div class="page-header d-flex">
|
<div class="container page-content">
|
||||||
<h1>{{ "groups" | i18n }}</h1>
|
<div class="page-header d-flex">
|
||||||
<div class="ml-auto d-flex">
|
<h1>{{ "groups" | i18n }}</h1>
|
||||||
<div>
|
<div class="ml-auto d-flex">
|
||||||
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
<div>
|
||||||
<input
|
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
||||||
type="search"
|
<input
|
||||||
class="form-control form-control-sm"
|
type="search"
|
||||||
id="search"
|
class="form-control form-control-sm"
|
||||||
placeholder="{{ 'search' | i18n }}"
|
id="search"
|
||||||
[(ngModel)]="searchText"
|
placeholder="{{ 'search' | i18n }}"
|
||||||
/>
|
[(ngModel)]="searchText"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="add()">
|
||||||
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
|
{{ "newGroup" | i18n }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="add()">
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
|
||||||
{{ "newGroup" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<ng-container *ngIf="loading">
|
||||||
<ng-container *ngIf="loading">
|
<i
|
||||||
<i
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
title="{{ 'loading' | i18n }}"
|
||||||
title="{{ 'loading' | i18n }}"
|
aria-hidden="true"
|
||||||
aria-hidden="true"
|
></i>
|
||||||
></i>
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
</ng-container>
|
||||||
</ng-container>
|
<ng-container
|
||||||
<ng-container
|
*ngIf="
|
||||||
*ngIf="
|
!loading &&
|
||||||
!loading &&
|
(isPaging() ? pagedGroups : (groups | search: searchText:'name':'id')) as searchedGroups
|
||||||
(isPaging() ? pagedGroups : (groups | search: searchText:'name':'id')) as searchedGroups
|
"
|
||||||
"
|
|
||||||
>
|
|
||||||
<p *ngIf="!searchedGroups.length">{{ "noGroupsInList" | i18n }}</p>
|
|
||||||
<table
|
|
||||||
class="table table-hover table-list"
|
|
||||||
*ngIf="searchedGroups.length"
|
|
||||||
infiniteScroll
|
|
||||||
[infiniteScrollDistance]="1"
|
|
||||||
[infiniteScrollDisabled]="!isPaging()"
|
|
||||||
(scrolled)="loadMore()"
|
|
||||||
>
|
>
|
||||||
<tbody>
|
<p *ngIf="!searchedGroups.length">{{ "noGroupsInList" | i18n }}</p>
|
||||||
<tr *ngFor="let g of searchedGroups">
|
<table
|
||||||
<td>
|
class="table table-hover table-list"
|
||||||
<a href="#" appStopClick (click)="edit(g)">{{ g.name }}</a>
|
*ngIf="searchedGroups.length"
|
||||||
</td>
|
infiniteScroll
|
||||||
<td class="table-list-options">
|
[infiniteScrollDistance]="1"
|
||||||
<div class="dropdown" appListDropdown>
|
[infiniteScrollDisabled]="!isPaging()"
|
||||||
<button
|
(scrolled)="loadMore()"
|
||||||
class="btn btn-outline-secondary dropdown-toggle"
|
>
|
||||||
type="button"
|
<tbody>
|
||||||
data-toggle="dropdown"
|
<tr *ngFor="let g of searchedGroups">
|
||||||
aria-haspopup="true"
|
<td>
|
||||||
aria-expanded="false"
|
<a href="#" appStopClick (click)="edit(g)">{{ g.name }}</a>
|
||||||
appA11yTitle="{{ 'options' | i18n }}"
|
</td>
|
||||||
>
|
<td class="table-list-options">
|
||||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
<div class="dropdown" appListDropdown>
|
||||||
</button>
|
<button
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
class="btn btn-outline-secondary dropdown-toggle"
|
||||||
<a class="dropdown-item" href="#" appStopClick (click)="users(g)">
|
type="button"
|
||||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
data-toggle="dropdown"
|
||||||
{{ "users" | i18n }}
|
aria-haspopup="true"
|
||||||
</a>
|
aria-expanded="false"
|
||||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(g)">
|
appA11yTitle="{{ 'options' | i18n }}"
|
||||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
>
|
||||||
{{ "delete" | i18n }}
|
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
||||||
</a>
|
</button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right">
|
||||||
|
<a class="dropdown-item" href="#" appStopClick (click)="users(g)">
|
||||||
|
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||||
|
{{ "users" | i18n }}
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(g)">
|
||||||
|
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||||
|
{{ "delete" | i18n }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
</ng-container>
|
||||||
</ng-container>
|
<ng-template #addEdit></ng-template>
|
||||||
<ng-template #addEdit></ng-template>
|
<ng-template #usersTemplate></ng-template>
|
||||||
<ng-template #usersTemplate></ng-template>
|
</div>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export class GroupsComponent implements OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.params.subscribe(async (params) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
const organization = await this.organizationService.get(this.organizationId);
|
const organization = await this.organizationService.get(this.organizationId);
|
||||||
if (organization == null || !organization.useGroups) {
|
if (organization == null || !organization.useGroups) {
|
||||||
|
|||||||
@@ -1,293 +1,295 @@
|
|||||||
<div class="page-header">
|
<div class="container page-content">
|
||||||
<h1>{{ "people" | i18n }}</h1>
|
<div class="page-header d-flex">
|
||||||
<div class="mt-2 d-flex">
|
<h1>{{ "members" | i18n }}</h1>
|
||||||
<div class="btn-group btn-group-sm" role="group">
|
<div class="ml-auto d-flex">
|
||||||
<button
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
type="button"
|
<button
|
||||||
class="btn btn-outline-secondary"
|
type="button"
|
||||||
[ngClass]="{ active: status == null }"
|
class="btn btn-outline-secondary"
|
||||||
(click)="filter(null)"
|
[ngClass]="{ active: status == null }"
|
||||||
>
|
(click)="filter(null)"
|
||||||
{{ "all" | i18n }}
|
>
|
||||||
<span bitBadge badgeType="info" *ngIf="allCount">{{ allCount }}</span>
|
{{ "all" | i18n }}
|
||||||
</button>
|
<span bitBadge badgeType="info" *ngIf="allCount">{{ allCount }}</span>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
[ngClass]="{ active: status == userStatusType.Invited }"
|
|
||||||
(click)="filter(userStatusType.Invited)"
|
|
||||||
>
|
|
||||||
{{ "invited" | i18n }}
|
|
||||||
<span bitBadge badgeType="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 bitBadge badgeType="warning" *ngIf="acceptedCount">{{ acceptedCount }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
[ngClass]="{ active: status == userStatusType.Revoked }"
|
|
||||||
(click)="filter(userStatusType.Revoked)"
|
|
||||||
>
|
|
||||||
{{ "revoked" | i18n }}
|
|
||||||
<span bitBadge badgeType="info" *ngIf="revokedCount">{{ revokedCount }}</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="bwi bwi-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="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
|
||||||
{{ "reinviteSelected" | i18n }}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="dropdown-item text-success"
|
type="button"
|
||||||
appStopClick
|
class="btn btn-outline-secondary"
|
||||||
(click)="bulkConfirm()"
|
[ngClass]="{ active: status == userStatusType.Invited }"
|
||||||
*ngIf="showBulkConfirmUsers"
|
(click)="filter(userStatusType.Invited)"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
{{ "invited" | i18n }}
|
||||||
{{ "confirmSelected" | i18n }}
|
<span bitBadge badgeType="info" *ngIf="invitedCount">{{ invitedCount }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="dropdown-item" appStopClick (click)="bulkRestore()">
|
<button
|
||||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
type="button"
|
||||||
{{ "restoreAccess" | i18n }}
|
class="btn btn-outline-secondary"
|
||||||
|
[ngClass]="{ active: status == userStatusType.Accepted }"
|
||||||
|
(click)="filter(userStatusType.Accepted)"
|
||||||
|
>
|
||||||
|
{{ "accepted" | i18n }}
|
||||||
|
<span bitBadge badgeType="warning" *ngIf="acceptedCount">{{ acceptedCount }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="dropdown-item" appStopClick (click)="bulkRevoke()">
|
<button
|
||||||
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
type="button"
|
||||||
{{ "revokeAccess" | i18n }}
|
class="btn btn-outline-secondary"
|
||||||
</button>
|
[ngClass]="{ active: status == userStatusType.Revoked }"
|
||||||
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
|
(click)="filter(userStatusType.Revoked)"
|
||||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
>
|
||||||
{{ "remove" | i18n }}
|
{{ "revoked" | i18n }}
|
||||||
</button>
|
<span bitBadge badgeType="info" *ngIf="revokedCount">{{ revokedCount }}</span>
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
|
||||||
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i>
|
|
||||||
{{ "selectAll" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
|
||||||
<i class="bwi bwi-fw bwi-minus-square" aria-hidden="true"></i>
|
|
||||||
{{ "unselectAll" | i18n }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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="bwi bwi-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="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||||
|
{{ "reinviteSelected" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="dropdown-item text-success"
|
||||||
|
appStopClick
|
||||||
|
(click)="bulkConfirm()"
|
||||||
|
*ngIf="showBulkConfirmUsers"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||||
|
{{ "confirmSelected" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" appStopClick (click)="bulkRestore()">
|
||||||
|
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||||
|
{{ "restoreAccess" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" appStopClick (click)="bulkRevoke()">
|
||||||
|
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
||||||
|
{{ "revokeAccess" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
|
||||||
|
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||||
|
{{ "remove" | i18n }}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
||||||
|
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i>
|
||||||
|
{{ "selectAll" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
||||||
|
<i class="bwi bwi-fw bwi-minus-square" 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="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
|
{{ "inviteUser" | i18n }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
|
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
|
||||||
{{ "inviteUser" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<ng-container *ngIf="loading">
|
||||||
<ng-container *ngIf="loading">
|
<i
|
||||||
<i
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
title="{{ 'loading' | i18n }}"
|
||||||
title="{{ 'loading' | i18n }}"
|
aria-hidden="true"
|
||||||
aria-hidden="true"
|
></i>
|
||||||
></i>
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
<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="bwi bwi-check-circle"
|
|
||||||
*ngIf="showConfirmUsers"
|
|
||||||
>
|
|
||||||
{{ "usersNeedConfirmed" | 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 bitBadge badgeType="secondary" *ngIf="u.status === userStatusType.Invited">{{
|
|
||||||
"invited" | i18n
|
|
||||||
}}</span>
|
|
||||||
<span bitBadge badgeType="warning" *ngIf="u.status === userStatusType.Accepted">{{
|
|
||||||
"accepted" | i18n
|
|
||||||
}}</span>
|
|
||||||
<span bitBadge badgeType="secondary" *ngIf="u.status === userStatusType.Revoked">{{
|
|
||||||
"revoked" | i18n
|
|
||||||
}}</span>
|
|
||||||
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<ng-container *ngIf="u.twoFactorEnabled">
|
|
||||||
<i
|
|
||||||
class="bwi bwi-lock"
|
|
||||||
title="{{ 'userUsingTwoStep' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="showEnrolledStatus(u)">
|
|
||||||
<i
|
|
||||||
class="bwi bwi-key"
|
|
||||||
title="{{ 'enrolledPasswordReset' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "enrolledPasswordReset" | i18n }}</span>
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<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>
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-secondary dropdown-toggle"
|
|
||||||
type="button"
|
|
||||||
data-toggle="dropdown"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-expanded="false"
|
|
||||||
appA11yTitle="{{ 'options' | i18n }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-cog bwi-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="bwi bwi-fw bwi-envelope" 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="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
|
||||||
{{ "confirm" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="#"
|
|
||||||
appStopClick
|
|
||||||
(click)="groups(u)"
|
|
||||||
*ngIf="accessGroups"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-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="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
|
||||||
{{ "eventLogs" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="#"
|
|
||||||
appStopClick
|
|
||||||
(click)="resetPassword(u)"
|
|
||||||
*ngIf="allowResetPassword(u)"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
|
|
||||||
{{ "resetPassword" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="#"
|
|
||||||
appStopClick
|
|
||||||
(click)="restore(u)"
|
|
||||||
*ngIf="u.status === userStatusType.Revoked"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
|
||||||
{{ "restoreAccess" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="dropdown-item"
|
|
||||||
href="#"
|
|
||||||
appStopClick
|
|
||||||
(click)="revoke(u)"
|
|
||||||
*ngIf="u.status !== userStatusType.Revoked"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
|
||||||
{{ "revokeAccess" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
|
||||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
|
||||||
{{ "remove" | i18n }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
<ng-container
|
||||||
<ng-template #addEdit></ng-template>
|
*ngIf="
|
||||||
<ng-template #groupsTemplate></ng-template>
|
!loading &&
|
||||||
<ng-template #eventsTemplate></ng-template>
|
(isPaging() ? pagedUsers : (users | search: searchText:'name':'email':'id')) as searchedUsers
|
||||||
<ng-template #confirmTemplate></ng-template>
|
"
|
||||||
<ng-template #resetPasswordTemplate></ng-template>
|
>
|
||||||
<ng-template #bulkStatusTemplate></ng-template>
|
<p *ngIf="!searchedUsers.length">{{ "noUsersInList" | i18n }}</p>
|
||||||
<ng-template #bulkConfirmTemplate></ng-template>
|
<ng-container *ngIf="searchedUsers.length">
|
||||||
<ng-template #bulkRemoveTemplate></ng-template>
|
<app-callout
|
||||||
|
type="info"
|
||||||
|
title="{{ 'confirmUsers' | i18n }}"
|
||||||
|
icon="bwi bwi-check-circle"
|
||||||
|
*ngIf="showConfirmUsers"
|
||||||
|
>
|
||||||
|
{{ "usersNeedConfirmed" | 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 bitBadge badgeType="secondary" *ngIf="u.status === userStatusType.Invited">{{
|
||||||
|
"invited" | i18n
|
||||||
|
}}</span>
|
||||||
|
<span bitBadge badgeType="warning" *ngIf="u.status === userStatusType.Accepted">{{
|
||||||
|
"accepted" | i18n
|
||||||
|
}}</span>
|
||||||
|
<span bitBadge badgeType="secondary" *ngIf="u.status === userStatusType.Revoked">{{
|
||||||
|
"revoked" | i18n
|
||||||
|
}}</span>
|
||||||
|
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<ng-container *ngIf="u.twoFactorEnabled">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-lock"
|
||||||
|
title="{{ 'userUsingTwoStep' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="showEnrolledStatus(u)">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-key"
|
||||||
|
title="{{ 'enrolledPasswordReset' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "enrolledPasswordReset" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<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>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary dropdown-toggle"
|
||||||
|
type="button"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false"
|
||||||
|
appA11yTitle="{{ 'options' | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-cog bwi-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="bwi bwi-fw bwi-envelope" 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="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||||
|
{{ "confirm" | i18n }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href="#"
|
||||||
|
appStopClick
|
||||||
|
(click)="groups(u)"
|
||||||
|
*ngIf="accessGroups"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-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="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
||||||
|
{{ "eventLogs" | i18n }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href="#"
|
||||||
|
appStopClick
|
||||||
|
(click)="resetPassword(u)"
|
||||||
|
*ngIf="allowResetPassword(u)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
|
||||||
|
{{ "resetPassword" | i18n }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href="#"
|
||||||
|
appStopClick
|
||||||
|
(click)="restore(u)"
|
||||||
|
*ngIf="u.status === userStatusType.Revoked"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||||
|
{{ "restoreAccess" | i18n }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="dropdown-item"
|
||||||
|
href="#"
|
||||||
|
appStopClick
|
||||||
|
(click)="revoke(u)"
|
||||||
|
*ngIf="u.status !== userStatusType.Revoked"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
||||||
|
{{ "revokeAccess" | i18n }}
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
||||||
|
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||||
|
{{ "remove" | i18n }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #addEdit></ng-template>
|
||||||
|
<ng-template #groupsTemplate></ng-template>
|
||||||
|
<ng-template #eventsTemplate></ng-template>
|
||||||
|
<ng-template #confirmTemplate></ng-template>
|
||||||
|
<ng-template #resetPasswordTemplate></ng-template>
|
||||||
|
<ng-template #bulkStatusTemplate></ng-template>
|
||||||
|
<ng-template #bulkConfirmTemplate></ng-template>
|
||||||
|
<ng-template #bulkRemoveTemplate></ng-template>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export class PeopleComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
this.route.parent.params.subscribe(async (params) => {
|
||||||
this.organizationId = params.organizationId;
|
this.organizationId = params.organizationId;
|
||||||
const organization = await this.organizationService.get(this.organizationId);
|
const organization = await this.organizationService.get(this.organizationId);
|
||||||
if (!organization.canManageUsers) {
|
if (!organization.canManageUsers) {
|
||||||
|
|||||||
@@ -2,28 +2,15 @@ import { NgModule } from "@angular/core";
|
|||||||
import { RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
|
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
|
||||||
import { Permissions } from "@bitwarden/common/enums/permissions";
|
|
||||||
|
|
||||||
import { PermissionsGuard } from "./guards/permissions.guard";
|
import { PermissionsGuard } from "./guards/permissions.guard";
|
||||||
import { OrganizationLayoutComponent } from "./layouts/organization-layout.component";
|
import { OrganizationLayoutComponent } from "./layouts/organization-layout.component";
|
||||||
import { CollectionsComponent } from "./manage/collections.component";
|
|
||||||
import { EventsComponent } from "./manage/events.component";
|
|
||||||
import { GroupsComponent } from "./manage/groups.component";
|
import { GroupsComponent } from "./manage/groups.component";
|
||||||
import { ManageComponent } from "./manage/manage.component";
|
|
||||||
import { PeopleComponent } from "./manage/people.component";
|
import { PeopleComponent } from "./manage/people.component";
|
||||||
import { PoliciesComponent } from "./manage/policies.component";
|
|
||||||
import { NavigationPermissionsService } from "./services/navigation-permissions.service";
|
import { NavigationPermissionsService } from "./services/navigation-permissions.service";
|
||||||
import { AccountComponent } from "./settings/account.component";
|
import { AccountComponent } from "./settings/account.component";
|
||||||
import { OrganizationBillingComponent } from "./settings/organization-billing.component";
|
|
||||||
import { OrganizationSubscriptionComponent } from "./settings/organization-subscription.component";
|
|
||||||
import { SettingsComponent } from "./settings/settings.component";
|
import { SettingsComponent } from "./settings/settings.component";
|
||||||
import { TwoFactorSetupComponent } from "./settings/two-factor-setup.component";
|
import { TwoFactorSetupComponent } from "./settings/two-factor-setup.component";
|
||||||
import { ExposedPasswordsReportComponent } from "./tools/exposed-passwords-report.component";
|
|
||||||
import { InactiveTwoFactorReportComponent } from "./tools/inactive-two-factor-report.component";
|
|
||||||
import { ReusedPasswordsReportComponent } from "./tools/reused-passwords-report.component";
|
|
||||||
import { ToolsComponent } from "./tools/tools.component";
|
|
||||||
import { UnsecuredWebsitesReportComponent } from "./tools/unsecured-websites-report.component";
|
|
||||||
import { WeakPasswordsReportComponent } from "./tools/weak-passwords-report.component";
|
|
||||||
import { VaultModule } from "./vault/vault.module";
|
import { VaultModule } from "./vault/vault.module";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -40,137 +27,6 @@ const routes: Routes = [
|
|||||||
path: "vault",
|
path: "vault",
|
||||||
loadChildren: () => VaultModule,
|
loadChildren: () => VaultModule,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "tools",
|
|
||||||
component: ToolsComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: { permissions: NavigationPermissionsService.getPermissions("tools") },
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
pathMatch: "full",
|
|
||||||
redirectTo: "import",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
loadChildren: () =>
|
|
||||||
import("./tools/import-export/org-import-export.module").then(
|
|
||||||
(m) => m.OrganizationImportExportModule
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "exposed-passwords-report",
|
|
||||||
component: ExposedPasswordsReportComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
titleId: "exposedPasswordsReport",
|
|
||||||
permissions: [Permissions.AccessReports],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "inactive-two-factor-report",
|
|
||||||
component: InactiveTwoFactorReportComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
titleId: "inactive2faReport",
|
|
||||||
permissions: [Permissions.AccessReports],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "reused-passwords-report",
|
|
||||||
component: ReusedPasswordsReportComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
titleId: "reusedPasswordsReport",
|
|
||||||
permissions: [Permissions.AccessReports],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "unsecured-websites-report",
|
|
||||||
component: UnsecuredWebsitesReportComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
titleId: "unsecuredWebsitesReport",
|
|
||||||
permissions: [Permissions.AccessReports],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "weak-passwords-report",
|
|
||||||
component: WeakPasswordsReportComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
titleId: "weakPasswordsReport",
|
|
||||||
permissions: [Permissions.AccessReports],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "manage",
|
|
||||||
component: ManageComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
permissions: NavigationPermissionsService.getPermissions("manage"),
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: "",
|
|
||||||
pathMatch: "full",
|
|
||||||
redirectTo: "people",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "collections",
|
|
||||||
component: CollectionsComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
titleId: "collections",
|
|
||||||
permissions: [
|
|
||||||
Permissions.CreateNewCollections,
|
|
||||||
Permissions.EditAnyCollection,
|
|
||||||
Permissions.DeleteAnyCollection,
|
|
||||||
Permissions.EditAssignedCollections,
|
|
||||||
Permissions.DeleteAssignedCollections,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "events",
|
|
||||||
component: EventsComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
titleId: "eventLogs",
|
|
||||||
permissions: [Permissions.AccessEventLogs],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "groups",
|
|
||||||
component: GroupsComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
titleId: "groups",
|
|
||||||
permissions: [Permissions.ManageGroups],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "people",
|
|
||||||
component: PeopleComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
titleId: "people",
|
|
||||||
permissions: [Permissions.ManageUsers, Permissions.ManageUsersPassword],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "policies",
|
|
||||||
component: PoliciesComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: {
|
|
||||||
titleId: "policies",
|
|
||||||
permissions: [Permissions.ManagePolicies],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "settings",
|
path: "settings",
|
||||||
component: SettingsComponent,
|
component: SettingsComponent,
|
||||||
@@ -178,25 +34,44 @@ const routes: Routes = [
|
|||||||
data: { permissions: NavigationPermissionsService.getPermissions("settings") },
|
data: { permissions: NavigationPermissionsService.getPermissions("settings") },
|
||||||
children: [
|
children: [
|
||||||
{ path: "", pathMatch: "full", redirectTo: "account" },
|
{ path: "", pathMatch: "full", redirectTo: "account" },
|
||||||
{ path: "account", component: AccountComponent, data: { titleId: "myOrganization" } },
|
{ path: "account", component: AccountComponent, data: { titleId: "organizationInfo" } },
|
||||||
{
|
{
|
||||||
path: "two-factor",
|
path: "two-factor",
|
||||||
component: TwoFactorSetupComponent,
|
component: TwoFactorSetupComponent,
|
||||||
data: { titleId: "twoStepLogin" },
|
data: { titleId: "twoStepLogin" },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "billing",
|
|
||||||
component: OrganizationBillingComponent,
|
|
||||||
canActivate: [PermissionsGuard],
|
|
||||||
data: { titleId: "billing", permissions: [Permissions.ManageBilling] },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "subscription",
|
|
||||||
component: OrganizationSubscriptionComponent,
|
|
||||||
data: { titleId: "subscription" },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "members",
|
||||||
|
component: PeopleComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
titleId: "members",
|
||||||
|
permissions: NavigationPermissionsService.getPermissions("members"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "groups",
|
||||||
|
component: GroupsComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
titleId: "groups",
|
||||||
|
permissions: NavigationPermissionsService.getPermissions("groups"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "reporting",
|
||||||
|
loadChildren: () =>
|
||||||
|
import("./reporting/organization-reporting.module").then(
|
||||||
|
(m) => m.OrganizationReportingModule
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "billing",
|
||||||
|
loadChildren: () =>
|
||||||
|
import("./billing/organization-billing.module").then((m) => m.OrganizationBillingModule),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
|
import { Permissions } from "@bitwarden/common/enums/permissions";
|
||||||
|
|
||||||
|
import { PermissionsGuard } from "../guards/permissions.guard";
|
||||||
|
import { EventsComponent } from "../manage/events.component";
|
||||||
|
import { NavigationPermissionsService } from "../services/navigation-permissions.service";
|
||||||
|
import { ExposedPasswordsReportComponent } from "../tools/exposed-passwords-report.component";
|
||||||
|
import { InactiveTwoFactorReportComponent } from "../tools/inactive-two-factor-report.component";
|
||||||
|
import { ReusedPasswordsReportComponent } from "../tools/reused-passwords-report.component";
|
||||||
|
import { UnsecuredWebsitesReportComponent } from "../tools/unsecured-websites-report.component";
|
||||||
|
import { WeakPasswordsReportComponent } from "../tools/weak-passwords-report.component";
|
||||||
|
|
||||||
|
import { ReportingComponent } from "./reporting.component";
|
||||||
|
import { ReportsHomeComponent } from "./reports-home.component";
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: ReportingComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: { permissions: NavigationPermissionsService.getPermissions("reporting") },
|
||||||
|
children: [
|
||||||
|
{ path: "", pathMatch: "full", redirectTo: "reports" },
|
||||||
|
{
|
||||||
|
path: "reports",
|
||||||
|
component: ReportsHomeComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
titleId: "reports",
|
||||||
|
permissions: [Permissions.AccessReports],
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "exposed-passwords-report",
|
||||||
|
component: ExposedPasswordsReportComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
titleId: "exposedPasswordsReport",
|
||||||
|
permissions: [Permissions.AccessReports],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "inactive-two-factor-report",
|
||||||
|
component: InactiveTwoFactorReportComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
titleId: "inactive2faReport",
|
||||||
|
permissions: [Permissions.AccessReports],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "reused-passwords-report",
|
||||||
|
component: ReusedPasswordsReportComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
titleId: "reusedPasswordsReport",
|
||||||
|
permissions: [Permissions.AccessReports],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "unsecured-websites-report",
|
||||||
|
component: UnsecuredWebsitesReportComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
titleId: "unsecuredWebsitesReport",
|
||||||
|
permissions: [Permissions.AccessReports],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "weak-passwords-report",
|
||||||
|
component: WeakPasswordsReportComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
titleId: "weakPasswordsReport",
|
||||||
|
permissions: [Permissions.AccessReports],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "events",
|
||||||
|
component: EventsComponent,
|
||||||
|
canActivate: [PermissionsGuard],
|
||||||
|
data: {
|
||||||
|
titleId: "eventLogs",
|
||||||
|
permissions: [Permissions.AccessEventLogs],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class OrganizationReportingRoutingModule {}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { ReportsSharedModule } from "../../reports";
|
||||||
|
import { SharedModule } from "../../shared/shared.module";
|
||||||
|
|
||||||
|
import { OrganizationReportingRoutingModule } from "./organization-reporting-routing.module";
|
||||||
|
import { ReportingComponent } from "./reporting.component";
|
||||||
|
import { ReportsHomeComponent } from "./reports-home.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [SharedModule, ReportsSharedModule, OrganizationReportingRoutingModule],
|
||||||
|
declarations: [ReportsHomeComponent, ReportingComponent],
|
||||||
|
})
|
||||||
|
export class OrganizationReportingModule {}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<div class="container page-content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3" *ngIf="showLeftNav">
|
||||||
|
<div class="card" *ngIf="organization">
|
||||||
|
<div class="card-header">{{ "reporting" | i18n }}</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
<a
|
||||||
|
routerLink="reports"
|
||||||
|
class="list-group-item"
|
||||||
|
routerLinkActive="active"
|
||||||
|
*ngIf="organization.canAccessReports"
|
||||||
|
>
|
||||||
|
{{ "reports" | i18n }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
routerLink="events"
|
||||||
|
class="list-group-item"
|
||||||
|
routerLinkActive="active"
|
||||||
|
*ngIf="organization.canAccessEventLogs && accessEvents"
|
||||||
|
>
|
||||||
|
{{ "eventLogs" | i18n }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-9" [ngClass]="showLeftNav ? 'col-9' : 'col-12'">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
|
||||||
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-org-reporting",
|
||||||
|
templateUrl: "reporting.component.html",
|
||||||
|
})
|
||||||
|
export class ReportingComponent implements OnInit {
|
||||||
|
organization: Organization;
|
||||||
|
accessEvents = false;
|
||||||
|
showLeftNav = true;
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute, private organizationService: OrganizationService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.route.parent.params.subscribe(async (params) => {
|
||||||
|
this.organization = await this.organizationService.get(params.organizationId);
|
||||||
|
this.accessEvents = this.showLeftNav = this.organization.useEvents;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<ng-container *ngIf="homepage">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>{{ "reports" | i18n }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>{{ "orgsReportsDesc" | i18n }}</p>
|
||||||
|
|
||||||
|
<app-report-list [reports]="reports"></app-report-list>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col">
|
||||||
|
<a bitButton routerLink="./" *ngIf="!homepage">
|
||||||
|
<i class="bwi bwi-angle-left" aria-hidden="true"></i>
|
||||||
|
{{ "backToReports" | i18n }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { NavigationEnd, Router } from "@angular/router";
|
||||||
|
import { filter, Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||||
|
|
||||||
|
import { ReportVariant, reports, ReportType, ReportEntry } from "../../reports";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-org-reports-home",
|
||||||
|
templateUrl: "reports-home.component.html",
|
||||||
|
})
|
||||||
|
export class ReportsHomeComponent implements OnInit, OnDestroy {
|
||||||
|
reports: ReportEntry[];
|
||||||
|
|
||||||
|
homepage = true;
|
||||||
|
private destrory$: Subject<void> = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(private stateService: StateService, router: Router) {
|
||||||
|
router.events
|
||||||
|
.pipe(
|
||||||
|
takeUntil(this.destrory$),
|
||||||
|
filter((event) => event instanceof NavigationEnd)
|
||||||
|
)
|
||||||
|
.subscribe((event) => {
|
||||||
|
this.homepage = (event as NavigationEnd).urlAfterRedirects.endsWith("/reports");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
const userHasPremium = await this.stateService.getCanAccessPremium();
|
||||||
|
|
||||||
|
const reportRequiresPremium = userHasPremium
|
||||||
|
? ReportVariant.Enabled
|
||||||
|
: ReportVariant.RequiresPremium;
|
||||||
|
|
||||||
|
this.reports = [
|
||||||
|
{
|
||||||
|
...reports[ReportType.ExposedPasswords],
|
||||||
|
variant: reportRequiresPremium,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...reports[ReportType.ReusedPasswords],
|
||||||
|
variant: reportRequiresPremium,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...reports[ReportType.WeakPasswords],
|
||||||
|
variant: reportRequiresPremium,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...reports[ReportType.UnsecuredWebsites],
|
||||||
|
variant: reportRequiresPremium,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...reports[ReportType.Inactive2fa],
|
||||||
|
variant: reportRequiresPremium,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destrory$.next();
|
||||||
|
this.destrory$.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,8 +15,11 @@ const permissions = {
|
|||||||
Permissions.ManageSso,
|
Permissions.ManageSso,
|
||||||
Permissions.ManageScim,
|
Permissions.ManageScim,
|
||||||
],
|
],
|
||||||
tools: [Permissions.AccessImportExport, Permissions.AccessReports],
|
members: [Permissions.ManageUsers, Permissions.ManageUsersPassword],
|
||||||
settings: [Permissions.ManageOrganization],
|
groups: [Permissions.ManageGroups],
|
||||||
|
reporting: [Permissions.AccessReports, Permissions.AccessEventLogs],
|
||||||
|
billing: [Permissions.ManageBilling],
|
||||||
|
settings: [Permissions.ManageOrganization, Permissions.ManagePolicies, Permissions.ManageSso],
|
||||||
};
|
};
|
||||||
|
|
||||||
export class NavigationPermissionsService {
|
export class NavigationPermissionsService {
|
||||||
@@ -30,21 +33,30 @@ export class NavigationPermissionsService {
|
|||||||
|
|
||||||
static canAccessAdmin(organization: Organization): boolean {
|
static canAccessAdmin(organization: Organization): boolean {
|
||||||
return (
|
return (
|
||||||
this.canAccessTools(organization) ||
|
this.canAccessMembers(organization) ||
|
||||||
this.canAccessSettings(organization) ||
|
this.canAccessGroups(organization) ||
|
||||||
this.canAccessManage(organization)
|
this.canAccessReporting(organization) ||
|
||||||
|
this.canAccessBilling(organization)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static canAccessTools(organization: Organization): boolean {
|
static canAccessMembers(organization: Organization): boolean {
|
||||||
return organization.hasAnyPermission(NavigationPermissionsService.getPermissions("tools"));
|
return organization.hasAnyPermission(NavigationPermissionsService.getPermissions("members"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static canAccessGroups(organization: Organization): boolean {
|
||||||
|
return organization.hasAnyPermission(NavigationPermissionsService.getPermissions("groups"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static canAccessReporting(organization: Organization): boolean {
|
||||||
|
return organization.hasAnyPermission(NavigationPermissionsService.getPermissions("reporting"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static canAccessBilling(organization: Organization): boolean {
|
||||||
|
return organization.hasAnyPermission(NavigationPermissionsService.getPermissions("billing"));
|
||||||
}
|
}
|
||||||
|
|
||||||
static canAccessSettings(organization: Organization): boolean {
|
static canAccessSettings(organization: Organization): boolean {
|
||||||
return organization.hasAnyPermission(NavigationPermissionsService.getPermissions("settings"));
|
return organization.hasAnyPermission(NavigationPermissionsService.getPermissions("settings"));
|
||||||
}
|
}
|
||||||
|
|
||||||
static canAccessManage(organization: Organization): boolean {
|
|
||||||
return organization.hasAnyPermission(NavigationPermissionsService.getPermissions("manage"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>{{ "myOrganization" | i18n }}</h1>
|
<h1>{{ "organizationInfo" | i18n }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="loading">
|
<div *ngIf="loading">
|
||||||
<i
|
<i
|
||||||
@@ -87,31 +87,6 @@
|
|||||||
{{ "rotateApiKey" | i18n }}
|
{{ "rotateApiKey" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div class="secondary-header border-0 mb-0">
|
|
||||||
<h1>{{ "taxInformation" | i18n }}</h1>
|
|
||||||
</div>
|
|
||||||
<p>{{ "taxInformationDesc" | i18n }}</p>
|
|
||||||
<div *ngIf="!org || loading">
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</div>
|
|
||||||
<form
|
|
||||||
*ngIf="org && !loading"
|
|
||||||
#formTax
|
|
||||||
(ngSubmit)="submitTaxInfo()"
|
|
||||||
[appApiAction]="taxFormPromise"
|
|
||||||
ngNativeValidate
|
|
||||||
>
|
|
||||||
<app-tax-info></app-tax-info>
|
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="formTax.loading">
|
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
||||||
<span>{{ "save" | i18n }}</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<div class="secondary-header text-danger border-0 mb-0">
|
<div class="secondary-header text-danger border-0 mb-0">
|
||||||
<h1>{{ "dangerZone" | i18n }}</h1>
|
<h1>{{ "dangerZone" | i18n }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { OrganizationResponse } from "@bitwarden/common/models/response/organiza
|
|||||||
|
|
||||||
import { ApiKeyComponent } from "../../settings/api-key.component";
|
import { ApiKeyComponent } from "../../settings/api-key.component";
|
||||||
import { PurgeVaultComponent } from "../../settings/purge-vault.component";
|
import { PurgeVaultComponent } from "../../settings/purge-vault.component";
|
||||||
import { TaxInfoComponent } from "../../settings/tax-info.component";
|
|
||||||
|
|
||||||
import { DeleteOrganizationComponent } from "./delete-organization.component";
|
import { DeleteOrganizationComponent } from "./delete-organization.component";
|
||||||
|
|
||||||
@@ -32,7 +31,6 @@ export class AccountComponent {
|
|||||||
apiKeyModalRef: ViewContainerRef;
|
apiKeyModalRef: ViewContainerRef;
|
||||||
@ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||||
rotateApiKeyModalRef: ViewContainerRef;
|
rotateApiKeyModalRef: ViewContainerRef;
|
||||||
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
|
|
||||||
|
|
||||||
selfHosted = false;
|
selfHosted = false;
|
||||||
canManageBilling = true;
|
canManageBilling = true;
|
||||||
@@ -40,7 +38,6 @@ export class AccountComponent {
|
|||||||
canUseApi = false;
|
canUseApi = false;
|
||||||
org: OrganizationResponse;
|
org: OrganizationResponse;
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
taxFormPromise: Promise<any>;
|
|
||||||
|
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
|
|
||||||
@@ -104,12 +101,6 @@ export class AccountComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitTaxInfo() {
|
|
||||||
this.taxFormPromise = this.taxInfo.submitTaxInfo();
|
|
||||||
await this.taxFormPromise;
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("taxInfoUpdated"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteOrganization() {
|
async deleteOrganization() {
|
||||||
await this.modalService.openViewRef(
|
await this.modalService.openViewRef(
|
||||||
DeleteOrganizationComponent,
|
DeleteOrganizationComponent,
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
<div class="page-header d-flex">
|
|
||||||
<h1>
|
|
||||||
{{ "billing" | i18n }}
|
|
||||||
</h1>
|
|
||||||
<button
|
|
||||||
(click)="load()"
|
|
||||||
class="btn btn-sm btn-outline-primary ml-auto"
|
|
||||||
*ngIf="firstLoaded"
|
|
||||||
[disabled]="loading"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': loading }" aria-hidden="true"></i>
|
|
||||||
{{ "refresh" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="!firstLoaded && loading">
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="billing">
|
|
||||||
<h2>{{ (isCreditBalance ? "accountCredit" : "accountBalance") | i18n }}</h2>
|
|
||||||
<p class="text-lg">
|
|
||||||
<strong>{{ creditOrBalance | currency: "$" }}</strong>
|
|
||||||
</p>
|
|
||||||
<p>{{ "creditAppliedDesc" | i18n }}</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
(click)="addCredit()"
|
|
||||||
*ngIf="!showAddCredit"
|
|
||||||
>
|
|
||||||
{{ "addCredit" | i18n }}
|
|
||||||
</button>
|
|
||||||
<app-add-credit
|
|
||||||
[organizationId]="organizationId"
|
|
||||||
(onAdded)="closeAddCredit(true)"
|
|
||||||
(onCanceled)="closeAddCredit(false)"
|
|
||||||
*ngIf="showAddCredit"
|
|
||||||
>
|
|
||||||
</app-add-credit>
|
|
||||||
<h2 class="spaced-header">{{ "paymentMethod" | i18n }}</h2>
|
|
||||||
<p *ngIf="!paymentSource">{{ "noPaymentMethod" | i18n }}</p>
|
|
||||||
<ng-container *ngIf="paymentSource">
|
|
||||||
<app-callout
|
|
||||||
type="warning"
|
|
||||||
title="{{ 'verifyBankAccount' | i18n }}"
|
|
||||||
*ngIf="
|
|
||||||
paymentSource.type === paymentMethodType.BankAccount && paymentSource.needsVerification
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<p>{{ "verifyBankAccountDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}</p>
|
|
||||||
<form
|
|
||||||
#verifyForm
|
|
||||||
class="form-inline"
|
|
||||||
(ngSubmit)="verifyBank()"
|
|
||||||
[appApiAction]="verifyBankPromise"
|
|
||||||
ngNativeValidate
|
|
||||||
>
|
|
||||||
<label class="sr-only" for="verifyAmount1">{{ "amount" | i18n: "1" }}</label>
|
|
||||||
<div class="input-group mr-2">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<div class="input-group-text">$0.</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="form-control"
|
|
||||||
id="verifyAmount1"
|
|
||||||
placeholder="xx"
|
|
||||||
name="Amount1"
|
|
||||||
[(ngModel)]="verifyAmount1"
|
|
||||||
min="1"
|
|
||||||
max="99"
|
|
||||||
step="1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<label class="sr-only" for="verifyAmount2">{{ "amount" | i18n: "2" }}</label>
|
|
||||||
<div class="input-group mr-2">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<div class="input-group-text">$0.</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="form-control"
|
|
||||||
id="verifyAmount2"
|
|
||||||
placeholder="xx"
|
|
||||||
name="Amount2"
|
|
||||||
[(ngModel)]="verifyAmount2"
|
|
||||||
min="1"
|
|
||||||
max="99"
|
|
||||||
step="1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-outline-primary btn-submit"
|
|
||||||
[disabled]="verifyForm.loading"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
||||||
<span>{{ "verifyBankAccount" | i18n }}</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</app-callout>
|
|
||||||
<p>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-fw"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-credit-card': paymentSource.type === paymentMethodType.Card,
|
|
||||||
'bwi-bank': paymentSource.type === paymentMethodType.BankAccount,
|
|
||||||
'bwi-money': paymentSource.type === paymentMethodType.Check,
|
|
||||||
'bwi-paypal text-primary': paymentSource.type === paymentMethodType.PayPal,
|
|
||||||
'bwi-apple text-muted': paymentSource.type === paymentMethodType.AppleInApp,
|
|
||||||
'bwi-google text-muted': paymentSource.type === paymentMethodType.GoogleInApp
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
<span *ngIf="paymentSourceInApp">{{ "inAppPurchase" | i18n }}</span>
|
|
||||||
{{ paymentSource.description }}
|
|
||||||
</p>
|
|
||||||
</ng-container>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
(click)="changePayment()"
|
|
||||||
*ngIf="!showAdjustPayment"
|
|
||||||
>
|
|
||||||
{{ (paymentSource ? "changePaymentMethod" : "addPaymentMethod") | i18n }}
|
|
||||||
</button>
|
|
||||||
<app-adjust-payment
|
|
||||||
[currentType]="paymentSource != null ? paymentSource.type : null"
|
|
||||||
[organizationId]="organizationId"
|
|
||||||
(onAdjusted)="closePayment(true)"
|
|
||||||
(onCanceled)="closePayment(false)"
|
|
||||||
*ngIf="showAdjustPayment"
|
|
||||||
>
|
|
||||||
</app-adjust-payment>
|
|
||||||
<h2 class="spaced-header">{{ "invoices" | i18n }}</h2>
|
|
||||||
<p *ngIf="!invoices || !invoices.length">{{ "noInvoices" | i18n }}</p>
|
|
||||||
<table class="table mb-2" *ngIf="invoices && invoices.length">
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let i of invoices">
|
|
||||||
<td>{{ i.date | date: "mediumDate" }}</td>
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
href="{{ i.pdfUrl }}"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
class="mr-2"
|
|
||||||
appA11yTitle="{{ 'downloadInvoice' | i18n }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-file-pdf" aria-hidden="true"></i
|
|
||||||
></a>
|
|
||||||
<a href="{{ i.url }}" target="_blank" rel="noopener" title="{{ 'viewInvoice' | i18n }}">
|
|
||||||
{{ "invoiceNumber" | i18n: i.number }}</a
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
<td>{{ i.amount | currency: "$" }}</td>
|
|
||||||
<td>
|
|
||||||
<span *ngIf="i.paid">
|
|
||||||
<i class="bwi bwi-check text-success" aria-hidden="true"></i>
|
|
||||||
{{ "paid" | i18n }}
|
|
||||||
</span>
|
|
||||||
<span *ngIf="!i.paid">
|
|
||||||
<i class="bwi bwi-exclamation-circle text-muted" aria-hidden="true"></i>
|
|
||||||
{{ "unpaid" | i18n }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h2 class="spaced-header">{{ "transactions" | i18n }}</h2>
|
|
||||||
<p *ngIf="!transactions || !transactions.length">{{ "noTransactions" | i18n }}</p>
|
|
||||||
<table class="table mb-2" *ngIf="transactions && transactions.length">
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let t of transactions">
|
|
||||||
<td>{{ t.createdDate | date: "mediumDate" }}</td>
|
|
||||||
<td>
|
|
||||||
<span *ngIf="t.type === transactionType.Charge || t.type === transactionType.Credit">
|
|
||||||
{{ "chargeNoun" | i18n }}
|
|
||||||
</span>
|
|
||||||
<span *ngIf="t.type === transactionType.Refund">{{ "refundNoun" | i18n }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-fw"
|
|
||||||
*ngIf="t.paymentMethodType"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-credit-card': t.paymentMethodType === paymentMethodType.Card,
|
|
||||||
'bwi-bank':
|
|
||||||
t.paymentMethodType === paymentMethodType.BankAccount ||
|
|
||||||
t.paymentMethodType === paymentMethodType.WireTransfer,
|
|
||||||
'bwi-bitcoin text-warning': t.paymentMethodType === paymentMethodType.BitPay,
|
|
||||||
'bwi-paypal text-primary': t.paymentMethodType === paymentMethodType.PayPal
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
{{ t.details }}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
[ngClass]="{ 'text-strike': t.refunded }"
|
|
||||||
title="{{ (t.refunded ? 'refunded' : '') | i18n }}"
|
|
||||||
>
|
|
||||||
{{ t.amount | currency: "$" }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<small class="text-muted">* {{ "chargesStatement" | i18n: "BITWARDEN" }}</small>
|
|
||||||
</ng-container>
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
|
||||||
import { ActivatedRoute } from "@angular/router";
|
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
|
||||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
|
||||||
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
|
|
||||||
import { TransactionType } from "@bitwarden/common/enums/transactionType";
|
|
||||||
import { VerifyBankRequest } from "@bitwarden/common/models/request/verifyBankRequest";
|
|
||||||
import { BillingResponse } from "@bitwarden/common/models/response/billingResponse";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: "app-org-billing",
|
|
||||||
templateUrl: "./organization-billing.component.html",
|
|
||||||
})
|
|
||||||
export class OrganizationBillingComponent implements OnInit {
|
|
||||||
loading = false;
|
|
||||||
firstLoaded = false;
|
|
||||||
showAdjustPayment = false;
|
|
||||||
showAddCredit = false;
|
|
||||||
billing: BillingResponse;
|
|
||||||
paymentMethodType = PaymentMethodType;
|
|
||||||
transactionType = TransactionType;
|
|
||||||
organizationId: string;
|
|
||||||
verifyAmount1: number;
|
|
||||||
verifyAmount2: number;
|
|
||||||
|
|
||||||
verifyBankPromise: Promise<any>;
|
|
||||||
|
|
||||||
// TODO - Make sure to properly split out the billing/invoice and payment method/account during org admin refresh
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private apiService: ApiService,
|
|
||||||
private i18nService: I18nService,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private logService: LogService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
|
||||||
this.route.parent.parent.params.subscribe(async (params) => {
|
|
||||||
this.organizationId = params.organizationId;
|
|
||||||
await this.load();
|
|
||||||
this.firstLoaded = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
if (this.loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.loading = true;
|
|
||||||
if (this.organizationId != null) {
|
|
||||||
this.billing = await this.apiService.getOrganizationBilling(this.organizationId);
|
|
||||||
}
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyBank() {
|
|
||||||
if (this.loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const request = new VerifyBankRequest();
|
|
||||||
request.amount1 = this.verifyAmount1;
|
|
||||||
request.amount2 = this.verifyAmount2;
|
|
||||||
this.verifyBankPromise = this.apiService.postOrganizationVerifyBank(
|
|
||||||
this.organizationId,
|
|
||||||
request
|
|
||||||
);
|
|
||||||
await this.verifyBankPromise;
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("verifiedBankAccount")
|
|
||||||
);
|
|
||||||
this.load();
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addCredit() {
|
|
||||||
if (this.paymentSourceInApp) {
|
|
||||||
this.platformUtilsService.showDialog(
|
|
||||||
this.i18nService.t("cannotPerformInAppPurchase"),
|
|
||||||
this.i18nService.t("addCredit"),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
"warning"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.showAddCredit = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAddCredit(load: boolean) {
|
|
||||||
this.showAddCredit = false;
|
|
||||||
if (load) {
|
|
||||||
this.load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changePayment() {
|
|
||||||
if (this.paymentSourceInApp) {
|
|
||||||
this.platformUtilsService.showDialog(
|
|
||||||
this.i18nService.t("cannotPerformInAppPurchase"),
|
|
||||||
this.i18nService.t("changePaymentMethod"),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
"warning"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.showAdjustPayment = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
closePayment(load: boolean) {
|
|
||||||
this.showAdjustPayment = false;
|
|
||||||
if (load) {
|
|
||||||
this.load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get isCreditBalance() {
|
|
||||||
return this.billing == null || this.billing.balance <= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get creditOrBalance() {
|
|
||||||
return Math.abs(this.billing != null ? this.billing.balance : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
get paymentSource() {
|
|
||||||
return this.billing != null ? this.billing.paymentSource : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get paymentSourceInApp() {
|
|
||||||
return (
|
|
||||||
this.paymentSource != null &&
|
|
||||||
(this.paymentSource.type === PaymentMethodType.AppleInApp ||
|
|
||||||
this.paymentSource.type === PaymentMethodType.GoogleInApp)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get invoices() {
|
|
||||||
return this.billing != null ? this.billing.invoices : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get transactions() {
|
|
||||||
return this.billing != null ? this.billing.transactions : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,18 +5,7 @@
|
|||||||
<div class="card-header">{{ "settings" | i18n }}</div>
|
<div class="card-header">{{ "settings" | i18n }}</div>
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
<a routerLink="account" class="list-group-item" routerLinkActive="active">
|
<a routerLink="account" class="list-group-item" routerLinkActive="active">
|
||||||
{{ "myOrganization" | i18n }}
|
{{ "organizationInfo" | i18n }}
|
||||||
</a>
|
|
||||||
<a routerLink="subscription" class="list-group-item" routerLinkActive="active">
|
|
||||||
{{ "subscription" | i18n }}
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
routerLink="billing"
|
|
||||||
class="list-group-item"
|
|
||||||
routerLinkActive="active"
|
|
||||||
*ngIf="showBilling"
|
|
||||||
>
|
|
||||||
{{ "billing" | i18n }}
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
routerLink="two-factor"
|
routerLink="two-factor"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from "./reports.module";
|
export * from "./reports.module";
|
||||||
export * from "./models/report-entry";
|
export * from "./shared";
|
||||||
export * from "./models/report-variant";
|
export * from "./reports";
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
|
|
||||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||||
|
|
||||||
import { ReportEntry } from "../models/report-entry";
|
|
||||||
import { ReportVariant } from "../models/report-variant";
|
|
||||||
import { reports, ReportType } from "../reports";
|
import { reports, ReportType } from "../reports";
|
||||||
|
import { ReportEntry, ReportVariant } from "../shared";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-reports-home",
|
selector: "app-reports-home",
|
||||||
|
|||||||
@@ -10,19 +10,16 @@ import { ReportsHomeComponent } from "./pages/reports-home.component";
|
|||||||
import { ReusedPasswordsReportComponent } from "./pages/reused-passwords-report.component";
|
import { ReusedPasswordsReportComponent } from "./pages/reused-passwords-report.component";
|
||||||
import { UnsecuredWebsitesReportComponent } from "./pages/unsecured-websites-report.component";
|
import { UnsecuredWebsitesReportComponent } from "./pages/unsecured-websites-report.component";
|
||||||
import { WeakPasswordsReportComponent } from "./pages/weak-passwords-report.component";
|
import { WeakPasswordsReportComponent } from "./pages/weak-passwords-report.component";
|
||||||
import { ReportCardComponent } from "./report-card/report-card.component";
|
|
||||||
import { ReportListComponent } from "./report-list/report-list.component";
|
|
||||||
import { ReportsLayoutComponent } from "./reports-layout.component";
|
import { ReportsLayoutComponent } from "./reports-layout.component";
|
||||||
import { ReportsRoutingModule } from "./reports-routing.module";
|
import { ReportsRoutingModule } from "./reports-routing.module";
|
||||||
|
import { ReportsSharedModule } from "./shared";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, SharedModule, ReportsRoutingModule],
|
imports: [CommonModule, SharedModule, ReportsSharedModule, ReportsRoutingModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
BreachReportComponent,
|
BreachReportComponent,
|
||||||
ExposedPasswordsReportComponent,
|
ExposedPasswordsReportComponent,
|
||||||
InactiveTwoFactorReportComponent,
|
InactiveTwoFactorReportComponent,
|
||||||
ReportCardComponent,
|
|
||||||
ReportListComponent,
|
|
||||||
ReportsLayoutComponent,
|
ReportsLayoutComponent,
|
||||||
ReportsHomeComponent,
|
ReportsHomeComponent,
|
||||||
ReusedPasswordsReportComponent,
|
ReusedPasswordsReportComponent,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReportEntry } from "./models/report-entry";
|
import { ReportEntry } from "./shared";
|
||||||
|
|
||||||
export enum ReportType {
|
export enum ReportType {
|
||||||
ExposedPasswords = "exposedPasswords",
|
ExposedPasswords = "exposedPasswords",
|
||||||
|
|||||||
3
apps/web/src/app/reports/shared/index.ts
Normal file
3
apps/web/src/app/reports/shared/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./models/report-entry";
|
||||||
|
export * from "./models/report-variant";
|
||||||
|
export * from "./reports-shared.module";
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<a
|
<a
|
||||||
class="tw-block tw-h-full tw-w-72 tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-300 !tw-text-main tw-transition-all hover:tw-scale-105 hover:tw-no-underline focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2"
|
class="tw-block tw-h-full tw-max-w-72 tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-300 !tw-text-main tw-transition-all hover:tw-scale-105 hover:tw-no-underline focus:tw-outline-none focus:tw-ring focus:tw-ring-primary-700 focus:tw-ring-offset-2"
|
||||||
[routerLink]="route"
|
[routerLink]="route"
|
||||||
>
|
>
|
||||||
<div class="tw-relative">
|
<div class="tw-relative">
|
||||||
@@ -4,8 +4,8 @@ import { Meta, Story, moduleMetadata } from "@storybook/angular";
|
|||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { BadgeModule, IconModule } from "@bitwarden/components";
|
import { BadgeModule, IconModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { PremiumBadgeComponent } from "../../components/premium-badge.component";
|
import { PremiumBadgeComponent } from "../../../components/premium-badge.component";
|
||||||
import { PreloadedEnglishI18nModule } from "../../tests/preloaded-english-i18n.module";
|
import { PreloadedEnglishI18nModule } from "../../../tests/preloaded-english-i18n.module";
|
||||||
import { ReportVariant } from "../models/report-variant";
|
import { ReportVariant } from "../models/report-variant";
|
||||||
|
|
||||||
import { ReportCardComponent } from "./report-card.component";
|
import { ReportCardComponent } from "./report-card.component";
|
||||||
@@ -4,11 +4,11 @@ import { Meta, Story, moduleMetadata } from "@storybook/angular";
|
|||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { BadgeModule, IconModule } from "@bitwarden/components";
|
import { BadgeModule, IconModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { PremiumBadgeComponent } from "../../components/premium-badge.component";
|
import { PremiumBadgeComponent } from "../../../components/premium-badge.component";
|
||||||
import { PreloadedEnglishI18nModule } from "../../tests/preloaded-english-i18n.module";
|
import { PreloadedEnglishI18nModule } from "../../../tests/preloaded-english-i18n.module";
|
||||||
|
import { reports } from "../../reports";
|
||||||
import { ReportVariant } from "../models/report-variant";
|
import { ReportVariant } from "../models/report-variant";
|
||||||
import { ReportCardComponent } from "../report-card/report-card.component";
|
import { ReportCardComponent } from "../report-card/report-card.component";
|
||||||
import { reports } from "../reports";
|
|
||||||
|
|
||||||
import { ReportListComponent } from "./report-list.component";
|
import { ReportListComponent } from "./report-list.component";
|
||||||
|
|
||||||
14
apps/web/src/app/reports/shared/reports-shared.module.ts
Normal file
14
apps/web/src/app/reports/shared/reports-shared.module.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../shared/shared.module";
|
||||||
|
|
||||||
|
import { ReportCardComponent } from "./report-card/report-card.component";
|
||||||
|
import { ReportListComponent } from "./report-list/report-list.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [CommonModule, SharedModule],
|
||||||
|
declarations: [ReportCardComponent, ReportListComponent],
|
||||||
|
exports: [ReportCardComponent, ReportListComponent],
|
||||||
|
})
|
||||||
|
export class ReportsSharedModule {}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<div class="d-flex tabbed-header">
|
||||||
|
<h1>
|
||||||
|
{{ "billingHistory" | i18n }}
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="secondary"
|
||||||
|
(click)="load()"
|
||||||
|
class="tw-ml-auto"
|
||||||
|
*ngIf="firstLoaded"
|
||||||
|
[disabled]="loading"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': loading }" aria-hidden="true"></i>
|
||||||
|
{{ "refresh" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="!firstLoaded && loading">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="billing">
|
||||||
|
<app-billing-history [billing]="billing"></app-billing-history>
|
||||||
|
</ng-container>
|
||||||
@@ -2,33 +2,28 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
|
|
||||||
import { TransactionType } from "@bitwarden/common/enums/transactionType";
|
|
||||||
import { BillingHistoryResponse } from "@bitwarden/common/models/response/billingHistoryResponse";
|
import { BillingHistoryResponse } from "@bitwarden/common/models/response/billingHistoryResponse";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-user-billing",
|
selector: "app-billing-history-view",
|
||||||
templateUrl: "user-billing-history.component.html",
|
templateUrl: "billing-history-view.component.html",
|
||||||
})
|
})
|
||||||
export class UserBillingHistoryComponent implements OnInit {
|
export class BillingHistoryViewComponent implements OnInit {
|
||||||
loading = false;
|
loading = false;
|
||||||
firstLoaded = false;
|
firstLoaded = false;
|
||||||
billing: BillingHistoryResponse;
|
billing: BillingHistoryResponse;
|
||||||
paymentMethodType = PaymentMethodType;
|
|
||||||
transactionType = TransactionType;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected apiService: ApiService,
|
private apiService: ApiService,
|
||||||
protected i18nService: I18nService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
protected platformUtilsService: PlatformUtilsService,
|
|
||||||
private router: Router
|
private router: Router
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
if (this.platformUtilsService.isSelfHost()) {
|
if (this.platformUtilsService.isSelfHost()) {
|
||||||
this.router.navigate(["/settings/subscription"]);
|
this.router.navigate(["/settings/subscription"]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
await this.load();
|
await this.load();
|
||||||
this.firstLoaded = true;
|
this.firstLoaded = true;
|
||||||
@@ -42,12 +37,4 @@ export class UserBillingHistoryComponent implements OnInit {
|
|||||||
this.billing = await this.apiService.getUserBillingHistory();
|
this.billing = await this.apiService.getUserBillingHistory();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get invoices() {
|
|
||||||
return this.billing != null ? this.billing.invoices : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get transactions() {
|
|
||||||
return this.billing != null ? this.billing.transactions : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
65
apps/web/src/app/settings/billing-history.component.html
Normal file
65
apps/web/src/app/settings/billing-history.component.html
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<h2 class="mt-3">{{ "invoices" | i18n }}</h2>
|
||||||
|
<p *ngIf="!invoices || !invoices.length">{{ "noInvoices" | i18n }}</p>
|
||||||
|
<table class="table mb-2" *ngIf="invoices && invoices.length">
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let i of invoices">
|
||||||
|
<td>{{ i.date | date: "mediumDate" }}</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="{{ i.pdfUrl }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="mr-2"
|
||||||
|
appA11yTitle="{{ 'downloadInvoice' | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-file-pdf" aria-hidden="true"></i
|
||||||
|
></a>
|
||||||
|
<a href="{{ i.url }}" target="_blank" rel="noopener" title="{{ 'viewInvoice' | i18n }}">
|
||||||
|
{{ "invoiceNumber" | i18n: i.number }}</a
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>{{ i.amount | currency: "$" }}</td>
|
||||||
|
<td>
|
||||||
|
<span *ngIf="i.paid">
|
||||||
|
<i class="bwi bwi-check text-success" aria-hidden="true"></i>
|
||||||
|
{{ "paid" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="!i.paid">
|
||||||
|
<i class="bwi bwi-exclamation-circle text-muted" aria-hidden="true"></i>
|
||||||
|
{{ "unpaid" | i18n }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h2 class="spaced-header">{{ "transactions" | i18n }}</h2>
|
||||||
|
<p *ngIf="!transactions || !transactions.length">{{ "noTransactions" | i18n }}</p>
|
||||||
|
<table class="table mb-2" *ngIf="transactions && transactions.length">
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let t of transactions">
|
||||||
|
<td>{{ t.createdDate | date: "mediumDate" }}</td>
|
||||||
|
<td>
|
||||||
|
<span *ngIf="t.type === transactionType.Charge || t.type === transactionType.Credit">
|
||||||
|
{{ "chargeNoun" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="t.type === transactionType.Refund">{{ "refundNoun" | i18n }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<i
|
||||||
|
class="bwi bwi-fw"
|
||||||
|
*ngIf="t.paymentMethodType"
|
||||||
|
aria-hidden="true"
|
||||||
|
[ngClass]="paymentMethodClasses(t.paymentMethodType)"
|
||||||
|
></i>
|
||||||
|
{{ t.details }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
[ngClass]="{ 'text-strike': t.refunded }"
|
||||||
|
title="{{ (t.refunded ? 'refunded' : '') | i18n }}"
|
||||||
|
>
|
||||||
|
{{ t.amount | currency: "$" }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<small class="text-muted">* {{ "chargesStatement" | i18n: "BITWARDEN" }}</small>
|
||||||
41
apps/web/src/app/settings/billing-history.component.ts
Normal file
41
apps/web/src/app/settings/billing-history.component.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Component, Input } from "@angular/core";
|
||||||
|
|
||||||
|
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
|
||||||
|
import { TransactionType } from "@bitwarden/common/enums/transactionType";
|
||||||
|
import { BillingHistoryResponse } from "@bitwarden/common/models/response/billingHistoryResponse";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-billing-history",
|
||||||
|
templateUrl: "billing-history.component.html",
|
||||||
|
})
|
||||||
|
export class BillingHistoryComponent {
|
||||||
|
@Input()
|
||||||
|
billing: BillingHistoryResponse;
|
||||||
|
|
||||||
|
paymentMethodType = PaymentMethodType;
|
||||||
|
transactionType = TransactionType;
|
||||||
|
|
||||||
|
get invoices() {
|
||||||
|
return this.billing != null ? this.billing.invoices : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get transactions() {
|
||||||
|
return this.billing != null ? this.billing.transactions : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentMethodClasses(type: PaymentMethodType) {
|
||||||
|
switch (type) {
|
||||||
|
case PaymentMethodType.Card:
|
||||||
|
return ["bwi-credit-card"];
|
||||||
|
case PaymentMethodType.BankAccount:
|
||||||
|
case PaymentMethodType.WireTransfer:
|
||||||
|
return ["bwi-bank"];
|
||||||
|
case PaymentMethodType.BitPay:
|
||||||
|
return ["bwi-bitcoin text-warning"];
|
||||||
|
case PaymentMethodType.PayPal:
|
||||||
|
return ["bwi-paypal text-primary"];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="tabbed-header d-flex">
|
<div class="d-flex" [ngClass]="headerClass">
|
||||||
<h1>
|
<h1>
|
||||||
{{ "paymentMethod" | i18n }}
|
{{ "paymentMethod" | i18n }}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -40,18 +40,48 @@
|
|||||||
<h2 class="spaced-header">{{ "paymentMethod" | i18n }}</h2>
|
<h2 class="spaced-header">{{ "paymentMethod" | i18n }}</h2>
|
||||||
<p *ngIf="!paymentSource">{{ "noPaymentMethod" | i18n }}</p>
|
<p *ngIf="!paymentSource">{{ "noPaymentMethod" | i18n }}</p>
|
||||||
<ng-container *ngIf="paymentSource">
|
<ng-container *ngIf="paymentSource">
|
||||||
|
<app-callout
|
||||||
|
type="warning"
|
||||||
|
title="{{ 'verifyBankAccount' | i18n }}"
|
||||||
|
*ngIf="
|
||||||
|
forOrganization &&
|
||||||
|
paymentSource.type === paymentMethodType.BankAccount &&
|
||||||
|
paymentSource.needsVerification
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p>{{ "verifyBankAccountDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}</p>
|
||||||
|
<form
|
||||||
|
#verifyForm
|
||||||
|
class="form-inline"
|
||||||
|
(ngSubmit)="verifyBank()"
|
||||||
|
[formGroup]="verifyBankForm"
|
||||||
|
[appApiAction]="verifyBankPromise"
|
||||||
|
ngNativeValidate
|
||||||
|
>
|
||||||
|
<bit-form-field class="tw-mr-2 tw-w-40">
|
||||||
|
<bit-label>{{ "amountX" | i18n: "1" }}</bit-label>
|
||||||
|
<input bitInput type="number" step="1" placeholder="xx" formControlName="amount1" />
|
||||||
|
<span bitPrefix>$0.</span>
|
||||||
|
</bit-form-field>
|
||||||
|
<bit-form-field class="tw-mr-2 tw-w-40">
|
||||||
|
<bit-label>{{ "amountX" | i18n: "2" }}</bit-label>
|
||||||
|
<input bitInput type="number" step="1" placeholder="xx" formControlName="amount2" />
|
||||||
|
<span bitPrefix>$0.</span>
|
||||||
|
</bit-form-field>
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
class="btn-submit"
|
||||||
|
[disabled]="verifyForm.loading"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||||
|
<span>{{ "verifyBankAccount" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</app-callout>
|
||||||
<p>
|
<p>
|
||||||
<i
|
<i class="bwi bwi-fw" [ngClass]="paymentSourceClasses"></i>
|
||||||
class="bwi bwi-fw"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-credit-card': paymentSource.type === paymentMethodType.Card,
|
|
||||||
'bwi-bank': paymentSource.type === paymentMethodType.BankAccount,
|
|
||||||
'bwi-money': paymentSource.type === paymentMethodType.Check,
|
|
||||||
'bwi-paypal text-primary': paymentSource.type === paymentMethodType.PayPal,
|
|
||||||
'bwi-apple text-muted': paymentSource.type === paymentMethodType.AppleInApp,
|
|
||||||
'bwi-google text-muted': paymentSource.type === paymentMethodType.GoogleInApp
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
<span *ngIf="paymentSourceInApp">{{ "inAppPurchase" | i18n }}</span>
|
<span *ngIf="paymentSourceInApp">{{ "inAppPurchase" | i18n }}</span>
|
||||||
{{ paymentSource.description }}
|
{{ paymentSource.description }}
|
||||||
</p>
|
</p>
|
||||||
@@ -66,4 +96,35 @@
|
|||||||
*ngIf="showAdjustPayment"
|
*ngIf="showAdjustPayment"
|
||||||
>
|
>
|
||||||
</app-adjust-payment>
|
</app-adjust-payment>
|
||||||
|
<ng-container *ngIf="forOrganization">
|
||||||
|
<h2 class="spaced-header">{{ "taxInformation" | i18n }}</h2>
|
||||||
|
<p>{{ "taxInformationDesc" | i18n }}</p>
|
||||||
|
<div *ngIf="!org || loading">
|
||||||
|
<i
|
||||||
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
|
title="{{ 'loading' | i18n }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
*ngIf="org && !loading"
|
||||||
|
#formTax
|
||||||
|
(ngSubmit)="submitTaxInfo()"
|
||||||
|
[appApiAction]="taxFormPromise"
|
||||||
|
ngNativeValidate
|
||||||
|
>
|
||||||
|
<app-tax-info></app-tax-info>
|
||||||
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
class="btn-submit"
|
||||||
|
[disabled]="formTax.loading"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||||
|
<span>{{ "save" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -1,37 +1,72 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
import { FormBuilder, FormControl, Validators } from "@angular/forms";
|
||||||
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
|
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
|
||||||
|
import { VerifyBankRequest } from "@bitwarden/common/models/request/verifyBankRequest";
|
||||||
import { BillingPaymentResponse } from "@bitwarden/common/models/response/billingPaymentResponse";
|
import { BillingPaymentResponse } from "@bitwarden/common/models/response/billingPaymentResponse";
|
||||||
|
import { OrganizationResponse } from "@bitwarden/common/models/response/organizationResponse";
|
||||||
|
|
||||||
|
import { TaxInfoComponent } from "./tax-info.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-payment-method",
|
selector: "app-payment-method",
|
||||||
templateUrl: "payment-method.component.html",
|
templateUrl: "payment-method.component.html",
|
||||||
})
|
})
|
||||||
export class PaymentMethodComponent implements OnInit {
|
export class PaymentMethodComponent implements OnInit {
|
||||||
|
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
firstLoaded = false;
|
firstLoaded = false;
|
||||||
showAdjustPayment = false;
|
showAdjustPayment = false;
|
||||||
showAddCredit = false;
|
showAddCredit = false;
|
||||||
billing: BillingPaymentResponse;
|
billing: BillingPaymentResponse;
|
||||||
|
org: OrganizationResponse;
|
||||||
paymentMethodType = PaymentMethodType;
|
paymentMethodType = PaymentMethodType;
|
||||||
|
organizationId: string;
|
||||||
|
|
||||||
|
verifyBankPromise: Promise<any>;
|
||||||
|
taxFormPromise: Promise<any>;
|
||||||
|
|
||||||
|
verifyBankForm = this.formBuilder.group({
|
||||||
|
amount1: new FormControl<number>(null, [
|
||||||
|
Validators.required,
|
||||||
|
Validators.max(99),
|
||||||
|
Validators.min(0),
|
||||||
|
]),
|
||||||
|
amount2: new FormControl<number>(null, [
|
||||||
|
Validators.required,
|
||||||
|
Validators.max(99),
|
||||||
|
Validators.min(0),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
protected platformUtilsService: PlatformUtilsService,
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
private router: Router
|
private router: Router,
|
||||||
|
private logService: LogService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private formBuilder: FormBuilder
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
if (this.platformUtilsService.isSelfHost()) {
|
this.route.params.subscribe(async (params) => {
|
||||||
this.router.navigate(["/settings/subscription"]);
|
if (params.organizationId) {
|
||||||
}
|
this.organizationId = params.organizationId;
|
||||||
await this.load();
|
} else if (this.platformUtilsService.isSelfHost()) {
|
||||||
this.firstLoaded = true;
|
this.router.navigate(["/settings/subscription"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.load();
|
||||||
|
this.firstLoaded = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@@ -39,7 +74,16 @@ export class PaymentMethodComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.billing = await this.apiService.getUserBillingPayment();
|
|
||||||
|
if (this.forOrganization) {
|
||||||
|
const billingPromise = this.apiService.getOrganizationBilling(this.organizationId);
|
||||||
|
const orgPromise = this.apiService.getOrganization(this.organizationId);
|
||||||
|
|
||||||
|
[this.billing, this.org] = await Promise.all([billingPromise, orgPromise]);
|
||||||
|
} else {
|
||||||
|
this.billing = await this.apiService.getUserBillingPayment();
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +129,37 @@ export class PaymentMethodComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async verifyBank() {
|
||||||
|
if (this.loading || !this.forOrganization) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new VerifyBankRequest();
|
||||||
|
request.amount1 = this.verifyBankForm.value.amount1;
|
||||||
|
request.amount2 = this.verifyBankForm.value.amount2;
|
||||||
|
this.verifyBankPromise = this.apiService.postOrganizationVerifyBank(
|
||||||
|
this.organizationId,
|
||||||
|
request
|
||||||
|
);
|
||||||
|
await this.verifyBankPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("verifiedBankAccount")
|
||||||
|
);
|
||||||
|
this.load();
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitTaxInfo() {
|
||||||
|
this.taxFormPromise = this.taxInfo.submitTaxInfo();
|
||||||
|
await this.taxFormPromise;
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("taxInfoUpdated"));
|
||||||
|
}
|
||||||
|
|
||||||
get isCreditBalance() {
|
get isCreditBalance() {
|
||||||
return this.billing == null || this.billing.balance <= 0;
|
return this.billing == null || this.billing.balance <= 0;
|
||||||
}
|
}
|
||||||
@@ -97,6 +172,36 @@ export class PaymentMethodComponent implements OnInit {
|
|||||||
return this.billing != null ? this.billing.paymentSource : null;
|
return this.billing != null ? this.billing.paymentSource : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get forOrganization() {
|
||||||
|
return this.organizationId != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get headerClass() {
|
||||||
|
return this.forOrganization ? ["page-header"] : ["tabbed-header"];
|
||||||
|
}
|
||||||
|
|
||||||
|
get paymentSourceClasses() {
|
||||||
|
if (this.paymentSource == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
switch (this.paymentSource.type) {
|
||||||
|
case PaymentMethodType.Card:
|
||||||
|
return ["bwi-credit-card"];
|
||||||
|
case PaymentMethodType.BankAccount:
|
||||||
|
return ["bwi-bank"];
|
||||||
|
case PaymentMethodType.Check:
|
||||||
|
return ["bwi-money"];
|
||||||
|
case PaymentMethodType.AppleInApp:
|
||||||
|
return ["bwi-apple text-muted"];
|
||||||
|
case PaymentMethodType.GoogleInApp:
|
||||||
|
return ["bwi-google text-muted"];
|
||||||
|
case PaymentMethodType.PayPal:
|
||||||
|
return ["bwi-paypal text-primary"];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get paymentSourceInApp() {
|
get paymentSourceInApp() {
|
||||||
return (
|
return (
|
||||||
this.paymentSource != null &&
|
this.paymentSource != null &&
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
|
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||||
import { PaymentMethodComponent } from "./payment-method.component";
|
import { PaymentMethodComponent } from "./payment-method.component";
|
||||||
import { PremiumComponent } from "./premium.component";
|
import { PremiumComponent } from "./premium.component";
|
||||||
import { SubscriptionComponent } from "./subscription.component";
|
import { SubscriptionComponent } from "./subscription.component";
|
||||||
import { UserBillingHistoryComponent } from "./user-billing-history.component";
|
|
||||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -31,7 +31,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "billing-history",
|
path: "billing-history",
|
||||||
component: UserBillingHistoryComponent,
|
component: BillingHistoryViewComponent,
|
||||||
data: { titleId: "billingHistory" },
|
data: { titleId: "billingHistory" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
<div class="tabbed-header d-flex">
|
|
||||||
<h1>
|
|
||||||
{{ "billingHistory" | i18n }}
|
|
||||||
</h1>
|
|
||||||
<button
|
|
||||||
bitButton
|
|
||||||
buttonType="secondary"
|
|
||||||
(click)="load()"
|
|
||||||
class="tw-ml-auto"
|
|
||||||
*ngIf="firstLoaded"
|
|
||||||
[disabled]="loading"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-refresh bwi-fw" [ngClass]="{ 'bwi-spin': loading }" aria-hidden="true"></i>
|
|
||||||
{{ "refresh" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="!firstLoaded && loading">
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="billing">
|
|
||||||
<h2>{{ "invoices" | i18n }}</h2>
|
|
||||||
<p *ngIf="!invoices || !invoices.length">{{ "noInvoices" | i18n }}</p>
|
|
||||||
<table class="table mb-2" *ngIf="invoices && invoices.length">
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let i of invoices">
|
|
||||||
<td>{{ i.date | date: "mediumDate" }}</td>
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
href="{{ i.pdfUrl }}"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
class="mr-2"
|
|
||||||
appA11yTitle="{{ 'downloadInvoice' | i18n }}"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-file-pdf" aria-hidden="true"></i
|
|
||||||
></a>
|
|
||||||
<a href="{{ i.url }}" target="_blank" rel="noopener" title="{{ 'viewInvoice' | i18n }}">
|
|
||||||
{{ "invoiceNumber" | i18n: i.number }}</a
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
<td>{{ i.amount | currency: "$" }}</td>
|
|
||||||
<td>
|
|
||||||
<span *ngIf="i.paid">
|
|
||||||
<i class="bwi bwi-check text-success" aria-hidden="true"></i>
|
|
||||||
{{ "paid" | i18n }}
|
|
||||||
</span>
|
|
||||||
<span *ngIf="!i.paid">
|
|
||||||
<i class="bwi bwi-exclamation-circle text-muted" aria-hidden="true"></i>
|
|
||||||
{{ "unpaid" | i18n }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h2 class="spaced-header">{{ "transactions" | i18n }}</h2>
|
|
||||||
<p *ngIf="!transactions || !transactions.length">{{ "noTransactions" | i18n }}</p>
|
|
||||||
<table class="table mb-2" *ngIf="transactions && transactions.length">
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let t of transactions">
|
|
||||||
<td>{{ t.createdDate | date: "mediumDate" }}</td>
|
|
||||||
<td>
|
|
||||||
<span *ngIf="t.type === transactionType.Charge || t.type === transactionType.Credit">
|
|
||||||
{{ "chargeNoun" | i18n }}
|
|
||||||
</span>
|
|
||||||
<span *ngIf="t.type === transactionType.Refund">{{ "refundNoun" | i18n }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-fw"
|
|
||||||
*ngIf="t.paymentMethodType"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="{
|
|
||||||
'bwi-credit-card': t.paymentMethodType === paymentMethodType.Card,
|
|
||||||
'bwi-bank':
|
|
||||||
t.paymentMethodType === paymentMethodType.BankAccount ||
|
|
||||||
t.paymentMethodType === paymentMethodType.WireTransfer,
|
|
||||||
'bwi-bitcoin text-warning': t.paymentMethodType === paymentMethodType.BitPay,
|
|
||||||
'bwi-paypal text-primary': t.paymentMethodType === paymentMethodType.PayPal
|
|
||||||
}"
|
|
||||||
></i>
|
|
||||||
{{ t.details }}
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
[ngClass]="{ 'text-strike': t.refunded }"
|
|
||||||
title="{{ (t.refunded ? 'refunded' : '') | i18n }}"
|
|
||||||
>
|
|
||||||
{{ t.amount | currency: "$" }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<small class="text-muted">* {{ "chargesStatement" | i18n: "BITWARDEN" }}</small>
|
|
||||||
</ng-container>
|
|
||||||
@@ -59,13 +59,10 @@ import { SingleOrgPolicyComponent } from "../organizations/policies/single-org.c
|
|||||||
import { TwoFactorAuthenticationPolicyComponent } from "../organizations/policies/two-factor-authentication.component";
|
import { TwoFactorAuthenticationPolicyComponent } from "../organizations/policies/two-factor-authentication.component";
|
||||||
import { AccountComponent as OrgAccountComponent } from "../organizations/settings/account.component";
|
import { AccountComponent as OrgAccountComponent } from "../organizations/settings/account.component";
|
||||||
import { AdjustSubscription } from "../organizations/settings/adjust-subscription.component";
|
import { AdjustSubscription } from "../organizations/settings/adjust-subscription.component";
|
||||||
import { BillingSyncApiKeyComponent } from "../organizations/settings/billing-sync-api-key.component";
|
|
||||||
import { ChangePlanComponent } from "../organizations/settings/change-plan.component";
|
import { ChangePlanComponent } from "../organizations/settings/change-plan.component";
|
||||||
import { DeleteOrganizationComponent } from "../organizations/settings/delete-organization.component";
|
import { DeleteOrganizationComponent } from "../organizations/settings/delete-organization.component";
|
||||||
import { DownloadLicenseComponent } from "../organizations/settings/download-license.component";
|
import { DownloadLicenseComponent } from "../organizations/settings/download-license.component";
|
||||||
import { ImageSubscriptionHiddenComponent as OrgSubscriptionHiddenComponent } from "../organizations/settings/image-subscription-hidden.component";
|
import { ImageSubscriptionHiddenComponent as OrgSubscriptionHiddenComponent } from "../organizations/settings/image-subscription-hidden.component";
|
||||||
import { OrganizationBillingComponent } from "../organizations/settings/organization-billing.component";
|
|
||||||
import { OrganizationSubscriptionComponent } from "../organizations/settings/organization-subscription.component";
|
|
||||||
import { SettingsComponent as OrgSettingComponent } from "../organizations/settings/settings.component";
|
import { SettingsComponent as OrgSettingComponent } from "../organizations/settings/settings.component";
|
||||||
import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent } from "../organizations/settings/two-factor-setup.component";
|
import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent } from "../organizations/settings/two-factor-setup.component";
|
||||||
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
|
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
|
||||||
@@ -89,6 +86,8 @@ import { AddCreditComponent } from "../settings/add-credit.component";
|
|||||||
import { AdjustPaymentComponent } from "../settings/adjust-payment.component";
|
import { AdjustPaymentComponent } from "../settings/adjust-payment.component";
|
||||||
import { AdjustStorageComponent } from "../settings/adjust-storage.component";
|
import { AdjustStorageComponent } from "../settings/adjust-storage.component";
|
||||||
import { ApiKeyComponent } from "../settings/api-key.component";
|
import { ApiKeyComponent } from "../settings/api-key.component";
|
||||||
|
import { BillingHistoryViewComponent } from "../settings/billing-history-view.component";
|
||||||
|
import { BillingHistoryComponent } from "../settings/billing-history.component";
|
||||||
import { BillingSyncKeyComponent } from "../settings/billing-sync-key.component";
|
import { BillingSyncKeyComponent } from "../settings/billing-sync-key.component";
|
||||||
import { ChangeEmailComponent } from "../settings/change-email.component";
|
import { ChangeEmailComponent } from "../settings/change-email.component";
|
||||||
import { ChangeKdfComponent } from "../settings/change-kdf.component";
|
import { ChangeKdfComponent } from "../settings/change-kdf.component";
|
||||||
@@ -128,7 +127,6 @@ import { TwoFactorWebAuthnComponent } from "../settings/two-factor-webauthn.comp
|
|||||||
import { TwoFactorYubiKeyComponent } from "../settings/two-factor-yubikey.component";
|
import { TwoFactorYubiKeyComponent } from "../settings/two-factor-yubikey.component";
|
||||||
import { UpdateKeyComponent } from "../settings/update-key.component";
|
import { UpdateKeyComponent } from "../settings/update-key.component";
|
||||||
import { UpdateLicenseComponent } from "../settings/update-license.component";
|
import { UpdateLicenseComponent } from "../settings/update-license.component";
|
||||||
import { UserBillingHistoryComponent } from "../settings/user-billing-history.component";
|
|
||||||
import { UserSubscriptionComponent } from "../settings/user-subscription.component";
|
import { UserSubscriptionComponent } from "../settings/user-subscription.component";
|
||||||
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
||||||
import { VerifyEmailComponent } from "../settings/verify-email.component";
|
import { VerifyEmailComponent } from "../settings/verify-email.component";
|
||||||
@@ -177,7 +175,6 @@ import { SharedModule } from ".";
|
|||||||
AdjustSubscription,
|
AdjustSubscription,
|
||||||
ApiKeyComponent,
|
ApiKeyComponent,
|
||||||
AttachmentsComponent,
|
AttachmentsComponent,
|
||||||
BillingSyncApiKeyComponent,
|
|
||||||
BillingSyncKeyComponent,
|
BillingSyncKeyComponent,
|
||||||
BulkActionsComponent,
|
BulkActionsComponent,
|
||||||
BulkDeleteComponent,
|
BulkDeleteComponent,
|
||||||
@@ -216,10 +213,8 @@ import { SharedModule } from ".";
|
|||||||
OrganizationSwitcherComponent,
|
OrganizationSwitcherComponent,
|
||||||
OrgAccountComponent,
|
OrgAccountComponent,
|
||||||
OrgAddEditComponent,
|
OrgAddEditComponent,
|
||||||
OrganizationBillingComponent,
|
|
||||||
OrganizationLayoutComponent,
|
OrganizationLayoutComponent,
|
||||||
OrganizationPlansComponent,
|
OrganizationPlansComponent,
|
||||||
OrganizationSubscriptionComponent,
|
|
||||||
OrgAttachmentsComponent,
|
OrgAttachmentsComponent,
|
||||||
OrgBulkConfirmComponent,
|
OrgBulkConfirmComponent,
|
||||||
OrgBulkRestoreRevokeComponent,
|
OrgBulkRestoreRevokeComponent,
|
||||||
@@ -299,7 +294,8 @@ import { SharedModule } from ".";
|
|||||||
UpdateLicenseComponent,
|
UpdateLicenseComponent,
|
||||||
UpdatePasswordComponent,
|
UpdatePasswordComponent,
|
||||||
UpdateTempPasswordComponent,
|
UpdateTempPasswordComponent,
|
||||||
UserBillingHistoryComponent,
|
BillingHistoryComponent,
|
||||||
|
BillingHistoryViewComponent,
|
||||||
UserLayoutComponent,
|
UserLayoutComponent,
|
||||||
UserSubscriptionComponent,
|
UserSubscriptionComponent,
|
||||||
UserVerificationComponent,
|
UserVerificationComponent,
|
||||||
@@ -360,10 +356,8 @@ import { SharedModule } from ".";
|
|||||||
OrganizationSwitcherComponent,
|
OrganizationSwitcherComponent,
|
||||||
OrgAccountComponent,
|
OrgAccountComponent,
|
||||||
OrgAddEditComponent,
|
OrgAddEditComponent,
|
||||||
OrganizationBillingComponent,
|
|
||||||
OrganizationLayoutComponent,
|
OrganizationLayoutComponent,
|
||||||
OrganizationPlansComponent,
|
OrganizationPlansComponent,
|
||||||
OrganizationSubscriptionComponent,
|
|
||||||
OrgAttachmentsComponent,
|
OrgAttachmentsComponent,
|
||||||
OrgBulkConfirmComponent,
|
OrgBulkConfirmComponent,
|
||||||
OrgBulkRestoreRevokeComponent,
|
OrgBulkRestoreRevokeComponent,
|
||||||
@@ -442,7 +436,8 @@ import { SharedModule } from ".";
|
|||||||
UpdateLicenseComponent,
|
UpdateLicenseComponent,
|
||||||
UpdatePasswordComponent,
|
UpdatePasswordComponent,
|
||||||
UpdateTempPasswordComponent,
|
UpdateTempPasswordComponent,
|
||||||
UserBillingHistoryComponent,
|
BillingHistoryComponent,
|
||||||
|
BillingHistoryViewComponent,
|
||||||
UserLayoutComponent,
|
UserLayoutComponent,
|
||||||
UserSubscriptionComponent,
|
UserSubscriptionComponent,
|
||||||
UserVerificationComponent,
|
UserVerificationComponent,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
SubmitButtonModule,
|
SubmitButtonModule,
|
||||||
MenuModule,
|
MenuModule,
|
||||||
|
TabsModule,
|
||||||
IconModule,
|
IconModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ import "./locales";
|
|||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
SubmitButtonModule,
|
SubmitButtonModule,
|
||||||
IconModule,
|
IconModule,
|
||||||
|
TabsModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -65,6 +67,7 @@ import "./locales";
|
|||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
SubmitButtonModule,
|
SubmitButtonModule,
|
||||||
IconModule,
|
IconModule,
|
||||||
|
TabsModule,
|
||||||
],
|
],
|
||||||
providers: [DatePipe],
|
providers: [DatePipe],
|
||||||
bootstrap: [],
|
bootstrap: [],
|
||||||
|
|||||||
@@ -1481,6 +1481,10 @@
|
|||||||
"message": "Identify and close security gaps in your online accounts by clicking the reports below.",
|
"message": "Identify and close security gaps in your online accounts by clicking the reports below.",
|
||||||
"description": "Vault Health Reports can be used to evaluate the security of your Bitwarden Personal or Organization Vault."
|
"description": "Vault Health Reports can be used to evaluate the security of your Bitwarden Personal or Organization Vault."
|
||||||
},
|
},
|
||||||
|
"orgsReportsDesc": {
|
||||||
|
"message": "Identify and close security gaps in your organization's accounts by clicking the reports below.",
|
||||||
|
"description": "Vault Health Reports can be used to evaluate the security of your Bitwarden Personal or Organization Vault."
|
||||||
|
},
|
||||||
"unsecuredWebsitesReport": {
|
"unsecuredWebsitesReport": {
|
||||||
"message": "Unsecure Websites"
|
"message": "Unsecure Websites"
|
||||||
},
|
},
|
||||||
@@ -2925,6 +2929,9 @@
|
|||||||
"myOrganization": {
|
"myOrganization": {
|
||||||
"message": "My Organization"
|
"message": "My Organization"
|
||||||
},
|
},
|
||||||
|
"organizationInfo": {
|
||||||
|
"message": "Organization Info"
|
||||||
|
},
|
||||||
"deleteOrganization": {
|
"deleteOrganization": {
|
||||||
"message": "Delete Organization"
|
"message": "Delete Organization"
|
||||||
},
|
},
|
||||||
@@ -5304,6 +5311,12 @@
|
|||||||
"on": {
|
"on": {
|
||||||
"message": "On"
|
"message": "On"
|
||||||
},
|
},
|
||||||
|
"members": {
|
||||||
|
"message": "Members"
|
||||||
|
},
|
||||||
|
"reporting": {
|
||||||
|
"message": "Reporting"
|
||||||
|
},
|
||||||
"cardBrandMir": {
|
"cardBrandMir": {
|
||||||
"message": "Mir"
|
"message": "Mir"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export class BillingResponse extends BaseResponse {
|
|||||||
this.invoices = invoices.map((i: any) => new BillingInvoiceResponse(i));
|
this.invoices = invoices.map((i: any) => new BillingInvoiceResponse(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasNoHistory() {
|
||||||
|
return this.invoices.length == 0 && this.transactions.length == 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BillingSourceResponse extends BaseResponse {
|
export class BillingSourceResponse extends BaseResponse {
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ module.exports = {
|
|||||||
"50vw": "50vw",
|
"50vw": "50vw",
|
||||||
"75vw": "75vw",
|
"75vw": "75vw",
|
||||||
},
|
},
|
||||||
|
maxWidth: ({ theme }) => ({
|
||||||
|
...theme("width"),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user