mirror of
https://github.com/bitwarden/browser
synced 2025-12-23 19:53:43 +00:00
[EC-7] Org Admin Vault Refresh Client V1 (#3925)
* [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> * [EC-451] Org Admin Refresh Permissions Refactor (#3320) * [EC-451] Update new org permissions for new tabs * [EC-451] Remove redudant route guards * [EC-451] Remove canAccessManageTab() * [EC-451] Use canAccess* callbacks in org routing module * Fix org api service refactor and linting after pulling in master * Fix broken org people and group pages after merge * [EC-18] Reporting side nav direction (#3420) * [EC-18] Re-order side nav for org reports according to Figma * [EC-18] Fix rxjs linter errors and redundant org flag * [EC-526] Default to Event Logs page for Reporting Tab (#3470) * [EC-526] Default to the Events Logs page when navigating to the Reporting tab * [EC-526] Undo default routing redirect when the child path is missing. Avoids defaulting to "/events" in case a user/org doesn't have access to event logs. * [EC-19] Update Organization Settings Page (#3251) * [EC-19] Refactor existing organization settings components to its own module * [EC-19] Move SSO page to settings tab * [EC-19] Move Policies page to Settings tab Refactor Policy components into its own module * [EC-19] Move ImageSubscriptionHiddenComponent * [EC-19] Lazy load org settings module * [EC-19] Add SSO Id to SSO config view * [EC-19] Remove SSO identfier from org info page * [EC-19] Update org settings/policies to follow ADR-0011 * [EC-19] Update two-step login setup description * [EC-19] Revert nested policy components folder * [EC-19] Revert nested org setting components folder * [EC-19] Remove left over image component * [EC-19] Prettier * [EC-19] Fix missing i18n * [EC-19] Update SSO form to use CL * [EC-19] Remove unused SSO input components * [EC-19] Fix bad SSO locale identifier * [EC-19] Fix import order linting * [EC-19] Add explicit whitespace check for launch click directive * [EC-19] Add restricted import paths to eslint config * [EC-19] Tag deprecated field with Jira issue to cleanup in future release * [EC-19] Remove out of date comment * [EC-19] Move policy components to policies module * [EC-19] Remove dityRequired validator * [EC-19] Use explicit type for SSO config form * [EC-19] Fix rxjs linter errors * [EC-19] Fix RxJS eslint comments in org settings component * [EC-19] Use explicit ControlsOf<T> helper for nested SSO form groups. * [EC-19] Attribute source of ControlsOf<T> helper * [EC-19] Fix missing settings side nav links * [EC-19] Fix member/user language for policy modals * [EC-551] Update Event Logs Client Column (#3572) * [EC-551] Fix RxJS warnings * [EC-551] Update page to use CL components and Tailwind classes * [EC-551] Update Client column to use text instead of icon. Update language and i18n. * [EC-14] Refactor vault filter (#3440) * [EC-14] initial refactoring of vault filter * [EC-14] return observable trees for all filters with head node * [EC-14] Remove bindings on callbacks * [EC-14] fix formatting on disabled orgs * [EC-14] hide MyVault if personal org policy * [EC-14] add check for single org policy * [EC-14] add policies to org and change node constructor * [EC-14] don't show options if personal vault policy * [EC-14] default to all vaults * [EC-14] add default selection to filters * [EC-14] finish filter model callbacks * [EC-14] finish filter functionality and begin cleaning up * [EC-14] clean up old components and start on org vault * [EC-14] loop through filters for presentation * [EC-14] refactor VaultFilterService and put filter presentation data back into Vault Filter component. Remove VaultService * [EC-14] begin refactoring org vault * [EC-14] Refactor Vault Filter Service to use observables * [EC-14] finish org vault filter * [EC-14] fix vault model tests * [EC-14] fix org service calls * [EC-14] pull refactor out of shared code * [EC-14] include head node for collections even if collections aren't loaded yet * [EC-14] fix url params for vaults * [EC-14] remove comments * [EC-14] Remove unnecesary getter for org on vault filter * [EC-14] fix linter * [EC-14] fix prettier * [EC-14] add deprecated methods to collection service for desktop and browser * [EC-14] simplify cipher type node check * [EC-14] add getters to vault filter model * [EC-14] refactor how we build the filter list into methods * [EC-14] add getters to build filter method * [EC-14] remove param ids if false * [EC-14] fix collapsing nodes * [EC-14] add specific type to search placeholder * [EC-14] remove extra constructor and comment from org vault filter * [EC-14] extract subscription callback to methods * [EC-14] Remove unecessary await * [EC-14] Remove ternary operators while building org filter * [EC-14] remove unnecessary deps array in vault filter service declaration * [EC-14] consolidate new models into one file * [EC-14] initialize nested observable inside of service Signed-off-by: Jacob Fink <jfink@bitwarden.com> * [EC-14] change how we load orgs into the vault filter and select the default filter * [EC-14] remove get from getters name * [EC-14] remove eslint-disable comment * [EC-14] move vault filter service abstraction to angular folder and separate * [EC-14] rename filter types and delete VaultFilterLabel * [EC-14] remove changes to workspace file * [EC-14] remove deprecated service from jslib module * [EC-14] remove any remaining files from common code * [EC-14] consolidate vault filter components into components folder * [EC-14] simplify method call * [EC-14] refactor the vault filter service - orgs now have observable property - BehaviorSubjects have been migrated to ReplaySubjects if they don't need starting value - added unit tests - fix small error when selecting org badge of personal vault - renamed some properties * [EC-14] replace mergeMap with switchMap in vault filter service * [EC-14] early return to prevent nesting * [EC-14] clean up filterCollections method * [EC-14] use isDeleted helper in html * [EC-14] add jsdoc comments to ServiceUtils * [EC-14] fix linter * [EC-14] use array.slice instead of setting length * Update apps/web/src/app/vault/vault-filter/services/vault-filter.service.ts Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * [EC-14] add missing high level jsdoc description * [EC-14] fix storybook absolute imports * [EC-14] delete vault-shared.module * [EC-14] change search placeholder text to getter and add missing strings * [EC-14] remove two way binding from search text in vault filter * [EC-14] removed all binding from search text and just use input event * [EC-14] remove async from apply vault filter * [EC-14] remove circular observable calls in vault filter service Co-authored-by: Thomas Rittson <eliykat@users.noreply.github.com> * [EC-14] move collapsed nodes to vault filter section * [EC-14] deconstruct filter section inside component * [EC-14] fix merge conflicts and introduce refactored organization service to vault filter service * [EC-14] remove mutation from filter builders * [EC-14] fix styling on buildFolderTree * [EC-14] remove leftover folder-filters reference and use ternary for collapse icon * [EC-14] remove unecessary checks * [EC-14] stop rebuilding filters when the organization changes * [EC-14] Move subscription out of setter in vault filter section * [EC-14] remove extra policy service methods from vault filter service * [EC-14] remove new methods from old vault-filter.service * [EC-14] Use vault filter service in vault components * [EC-14] reload collections from vault now that we have vault filter service * [EC-14] remove currentFilterCollections in vault filter component * [EC-14] change VaultFilterType to more specific OrganizationFilter in organization-options * [EC-14] include org check in isNodeSelected * [EC-14] add getters to filter function, fix storybook, and add test for All Collections * [EC-14] show org options even if there's a personal vault policy * [EC-14] use !"AllCollections" instead of just !null * [EC-14] Remove extra org Subject in vault filter service * [EC-14] remove null check from vault search text * [EC-14] replace store/build names with set/get. Remove extra call to setOrganizationFilter * [EC-14] add take(1) to subscribe in test * [EC-14] move init logic in org vault filter component to ngOnInit * [EC-14] Fix linter * [EC-14] revert change to vault filter model * [EC-14] be specific about ignoring All Collections * [EC-14] move observable init logic to beforeEach in test * [EC-14] make buildAllFilters return something to reduce side effects Signed-off-by: Jacob Fink <jfink@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Thomas Rittson <eliykat@users.noreply.github.com> * [EC-97] Organization Billing Language / RxJS Warnings (#3688) * [EC-97] Update copy to use the word members in a few places * [EC-97] Cleanup RxJS warnings and unused properties in org billing components * [EC-599] Access Selector Component (#3717) * Add Access Selector Component and Stories * Cherry pick FormSelectionList * Fix some problems caused from cherry-pick * Fix some Web module problems caused from cherry-pick * Move AccessSelector out of the root components directory. Move UserType pipe to AccessSelectorModule * Fix broken member access selector story * Add organization feature module * Undo changes to messages.json * Fix messages.json * Remove redundant CommonModule * [EC-599] Fix avatar/icon sizing * [EC-599] Remove padding in permission column * [EC-599] Make FormSelectionList operations immutable * [EC-599] Integrate the multi-select component * [EC-599] Handle readonly/access all edge cases * [EC-599] Add initial unit tests Also cleans up public interface for the AccessSelectorComponent. Fixes a bug found during unit test creation. * [EC-599] Include item name in control labels * [EC-599] Cleanup member email display * [EC-599] Review suggestions - Change PermissionMode to Enum - Rename permControl to permissionControl to be more clear - Rename FormSelectionList file to kebab case. - Move permission row boolean logic to named function for readability * [EC-599] Cleanup AccessSelectorComponent tests - Clarify test states - Add tests for column rendering - Add tests for permission mode - Add id to column headers for testing - Fix small permissionControl bug found during testing * [EC-599] Add FormSelectionList unit tests * [EC-599] Fix unit test and linter * [EC-599] Update Enums to Pascal case * [EC-599] Undo change to Enum values * [EC-7] fix: broken build * [EC-646] Org Admin Vault Refresh November Release Prep (#3913) * [EC-646] Remove links from Manage component These links are no longer necessary as they are now located in the new OAVR tabs. * [EC-646] Re-introduce the canAccessManageTab helper * [EC-646] Re-introduce /manage route in Organization routing module - Add the parent /manage route - Add child routes for collections, people, and groups * [EC-646] Adjust Org admin tabs Re-introduce the Manage tab and remove Groups and Members tabs. * [EC-646] Change Members title back to People * [EC-646] Move missing billing components Some billing components were in the org settings module and needed to be moved the org billing module * [EC-646] Fix import file upload button -Update to use click event handler and tailwind class to hide input. Avoids inline styles/js blocked by CSP - Fix broken async pipe * [EC-646] Fix groups and people page overflow Remove the container and page-content wrapper as the pages are no longer on their own tab * [EC-646] Change People to Members Change the text regarding managing members from People to Members to more closely follow changes coming later in the OAVR. Also update the URL to use /manage/members * [EC-646] Cherry-pickae39afeto fix tab text color * [EC-646] Fix org routing permissions helpers - Add canAccessVaultTab helper - Update canAccessOrgAdmin include check for vault tab access - Simplify canManageCollections * [EC-646] Fix Manage tab conditional logic - Add *ngIf condition for rendering Manage tab - Re-introduce dynamic route for Manage tab * Revert "[EC-14] Refactor vault filter (#3440)" (#3926) This reverts commit4d83b81d82. * Remove old reference to bit-submit-button that no longer exists (#3927) * [EC-593] Top align event logs row content (#3813) * [EC-593] Top align event log row contents * [EC-593] Prevent event log timestamp from wrapping * [EC-593] Add alignContent input to bitRow directive * [EC-593] Remove ineffective inline styles (CSP) * [EC-593] Remove templated tailwind classes Tailwind minimizes the bundled stylesheet by removing classes that aren't used in code. Using a string template for the classes causes those classes to be ignored. * [EC-593] Introduce alignContent input to table story * [EC-657] Hide Billing History and Payment Method for selfhosted orgs (#3935) Signed-off-by: Jacob Fink <jfink@bitwarden.com> Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com> Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com> Co-authored-by: Jake Fink <jfink@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Thomas Rittson <eliykat@users.noreply.github.com>
This commit is contained in:
@@ -27,15 +27,17 @@ import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.ab
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
|
||||
|
||||
import { PolicyListService, RouterService } from "./core";
|
||||
import { DisableSendPolicy } from "./organizations/policies/disable-send.component";
|
||||
import { MasterPasswordPolicy } from "./organizations/policies/master-password.component";
|
||||
import { PasswordGeneratorPolicy } from "./organizations/policies/password-generator.component";
|
||||
import { PersonalOwnershipPolicy } from "./organizations/policies/personal-ownership.component";
|
||||
import { RequireSsoPolicy } from "./organizations/policies/require-sso.component";
|
||||
import { ResetPasswordPolicy } from "./organizations/policies/reset-password.component";
|
||||
import { SendOptionsPolicy } from "./organizations/policies/send-options.component";
|
||||
import { SingleOrgPolicy } from "./organizations/policies/single-org.component";
|
||||
import { TwoFactorAuthenticationPolicy } from "./organizations/policies/two-factor-authentication.component";
|
||||
import {
|
||||
DisableSendPolicy,
|
||||
MasterPasswordPolicy,
|
||||
PasswordGeneratorPolicy,
|
||||
PersonalOwnershipPolicy,
|
||||
RequireSsoPolicy,
|
||||
ResetPasswordPolicy,
|
||||
SendOptionsPolicy,
|
||||
SingleOrgPolicy,
|
||||
TwoFactorAuthenticationPolicy,
|
||||
} from "./organizations/policies";
|
||||
|
||||
const BroadcasterSubscriptionId = "AppComponent";
|
||||
const IdleTimeout = 60000 * 10; // 10 minutes
|
||||
|
||||
@@ -479,16 +479,14 @@ export class EventService implements OnInit, OnDestroy {
|
||||
private formatGroupId(ev: EventResponse) {
|
||||
const shortId = this.getShortId(ev.groupId);
|
||||
const a = this.makeAnchor(shortId);
|
||||
a.setAttribute(
|
||||
"href",
|
||||
"#/organizations/" + ev.organizationId + "/manage/groups?search=" + shortId
|
||||
);
|
||||
a.setAttribute("href", "#/organizations/" + ev.organizationId + "/groups?search=" + shortId);
|
||||
return a.outerHTML;
|
||||
}
|
||||
|
||||
private formatCollectionId(ev: EventResponse) {
|
||||
const shortId = this.getShortId(ev.collectionId);
|
||||
const a = this.makeAnchor(shortId);
|
||||
// TODO: Update view/edit collection link after EC-14 is completed
|
||||
a.setAttribute(
|
||||
"href",
|
||||
"#/organizations/" + ev.organizationId + "/manage/collections?search=" + shortId
|
||||
@@ -503,7 +501,7 @@ export class EventService implements OnInit, OnDestroy {
|
||||
"href",
|
||||
"#/organizations/" +
|
||||
ev.organizationId +
|
||||
"/manage/people?search=" +
|
||||
"/members?search=" +
|
||||
shortId +
|
||||
"&viewEvents=" +
|
||||
ev.organizationUserId
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BasePolicy } from "../organizations/policies/base-policy.component";
|
||||
import { BasePolicy } from "../organizations/policies";
|
||||
|
||||
export class PolicyListService {
|
||||
private policies: BasePolicy[] = [];
|
||||
|
||||
@@ -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,51 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { concatMap, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
|
||||
import { BillingHistoryResponse } from "@bitwarden/common/models/response/billing-history.response";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-billing-history-view",
|
||||
templateUrl: "organization-billing-history-view.component.html",
|
||||
})
|
||||
export class OrgBillingHistoryViewComponent implements OnInit, OnDestroy {
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
billing: BillingHistoryResponse;
|
||||
organizationId: string;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.params
|
||||
.pipe(
|
||||
concatMap(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
this.firstLoaded = true;
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.billing = await this.organizationApiService.getBilling(this.organizationId);
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessBillingTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
|
||||
import { PaymentMethodComponent } from "../../settings/payment-method.component";
|
||||
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
|
||||
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: [OrganizationPermissionsGuard],
|
||||
data: { organizationPermissions: canAccessBillingTab },
|
||||
children: [
|
||||
{ path: "", pathMatch: "full", redirectTo: "subscription" },
|
||||
{
|
||||
path: "subscription",
|
||||
component: OrganizationSubscriptionComponent,
|
||||
data: { titleId: "subscription" },
|
||||
},
|
||||
{
|
||||
path: "payment-method",
|
||||
component: PaymentMethodComponent,
|
||||
data: {
|
||||
titleId: "paymentMethod",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "history",
|
||||
component: OrgBillingHistoryViewComponent,
|
||||
data: {
|
||||
titleId: "billingHistory",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class OrganizationBillingRoutingModule {}
|
||||
@@ -0,0 +1,33 @@
|
||||
<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
|
||||
*ngIf="showPaymentAndHistory"
|
||||
routerLink="payment-method"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
{{ "paymentMethod" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
*ngIf="showPaymentAndHistory"
|
||||
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,14 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-billing-tab",
|
||||
templateUrl: "organization-billing-tab.component.html",
|
||||
})
|
||||
export class OrganizationBillingTabComponent {
|
||||
showPaymentAndHistory: boolean;
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {
|
||||
this.showPaymentAndHistory = !this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { LooseComponentsModule, SharedModule } from "../../shared";
|
||||
|
||||
import { AdjustSubscription } from "./adjust-subscription.component";
|
||||
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
||||
import { ChangePlanComponent } from "./change-plan.component";
|
||||
import { DownloadLicenseComponent } from "./download-license.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: [
|
||||
AdjustSubscription,
|
||||
BillingSyncApiKeyComponent,
|
||||
ChangePlanComponent,
|
||||
DownloadLicenseComponent,
|
||||
OrganizationBillingTabComponent,
|
||||
OrganizationSubscriptionComponent,
|
||||
OrgBillingHistoryViewComponent,
|
||||
],
|
||||
})
|
||||
export class OrganizationBillingModule {}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { concatMap, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
@@ -27,17 +28,13 @@ import { SubscriptionHiddenIcon } from "./subscription-hidden.icon";
|
||||
selector: "app-org-subscription",
|
||||
templateUrl: "organization-subscription.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class OrganizationSubscriptionComponent implements OnInit {
|
||||
export class OrganizationSubscriptionComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("setupBillingSyncTemplate", { read: ViewContainerRef, static: true })
|
||||
setupBillingSyncModalRef: ViewContainerRef;
|
||||
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
organizationId: string;
|
||||
adjustSeatsAdd = true;
|
||||
showAdjustSeats = false;
|
||||
showAdjustSeatAutoscale = false;
|
||||
adjustStorageAdd = true;
|
||||
showAdjustStorage = false;
|
||||
showUpdateLicense = false;
|
||||
@@ -61,6 +58,8 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
|
||||
subscriptionHiddenIcon = SubscriptionHiddenIcon;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@@ -76,19 +75,27 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
this.firstLoaded = true;
|
||||
});
|
||||
this.route.params
|
||||
.pipe(
|
||||
concatMap(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
this.firstLoaded = true;
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.userOrg = this.organizationService.get(this.organizationId);
|
||||
if (this.userOrg.canManageBilling) {
|
||||
@@ -175,7 +182,7 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
this.showChangePlan = !this.showChangePlan;
|
||||
}
|
||||
|
||||
closeChangePlan(changed: boolean) {
|
||||
closeChangePlan() {
|
||||
this.showChangePlan = false;
|
||||
}
|
||||
|
||||
@@ -192,10 +199,14 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
comp.hasBillingToken = this.hasBillingSyncToken;
|
||||
}
|
||||
);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
ref.onClosed.subscribe(async () => {
|
||||
await this.load();
|
||||
});
|
||||
ref.onClosed
|
||||
.pipe(
|
||||
concatMap(async () => {
|
||||
await this.load();
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
closeDownloadLicense() {
|
||||
@@ -0,0 +1,136 @@
|
||||
<div class="tw-flex">
|
||||
<bit-form-field *ngIf="permissionMode == 'edit'">
|
||||
<bit-label>{{ "permission" | i18n }}</bit-label>
|
||||
<select
|
||||
bitInput
|
||||
[disabled]="disabled"
|
||||
[(ngModel)]="initialPermission"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
(blur)="handleBlur()"
|
||||
>
|
||||
<option *ngFor="let p of permissionList" [value]="p.perm">
|
||||
{{ p.labelId | i18n }}
|
||||
</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field class="tw-ml-3 tw-flex-grow">
|
||||
<bit-label>{{ selectorLabelText }}</bit-label>
|
||||
<bit-multi-select
|
||||
class="tw-w-full"
|
||||
[baseItems]="selectionList.deselectedItems"
|
||||
[removeSelectedItems]="true"
|
||||
[disabled]="disabled"
|
||||
(onItemsConfirmed)="selectItems($event)"
|
||||
(blur)="handleBlur()"
|
||||
></bit-multi-select>
|
||||
<bit-hint *ngIf="selectorHelpText">{{ selectorHelpText }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<bit-table [formGroup]="formGroup">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ columnHeader }}</th>
|
||||
<th bitCell id="permissionColHeading" *ngIf="permissionMode != 'hidden'">
|
||||
{{ "permission" | i18n }}
|
||||
</th>
|
||||
<th bitCell id="roleColHeading" *ngIf="showMemberRoles">{{ "role" | i18n }}</th>
|
||||
<th bitCell id="groupColHeading" *ngIf="showGroupColumn">{{ "group" | i18n }}</th>
|
||||
<th bitCell style="width: 50px"></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container body formArrayName="items">
|
||||
<tr
|
||||
bitRow
|
||||
*ngFor="let item of selectionList.selectedItems; let i = index"
|
||||
[formGroupName]="i"
|
||||
[ngClass]="{ 'tw-text-muted': item.readonly }"
|
||||
>
|
||||
<td bitCell [ngSwitch]="item.type">
|
||||
<div class="tw-flex tw-items-center" *ngSwitchCase="itemType.Member">
|
||||
<bit-avatar size="small" class="tw-mr-3" text="{{ item.labelName }}"></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<div class="tw-text-sm">
|
||||
{{ item.labelName }}
|
||||
<span *ngIf="item.status == 0" bitBadge badgeType="secondary">
|
||||
{{ "invited" | i18n }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tw-text-xs tw-text-muted" *ngIf="item.status != 0">{{ item.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-items-center tw-text-sm" *ngSwitchDefault>
|
||||
<i
|
||||
class="bwi tw-mr-3 tw-px-0.5 tw-text-2xl"
|
||||
[ngClass]="item.icon || itemIcon(item)"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{{ item.labelName }}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td bitCell *ngIf="permissionMode != 'hidden'">
|
||||
<ng-container *ngIf="canEditItemPermission(item); else readOnlyPerm">
|
||||
<label class="sr-only" [for]="'permission' + i"
|
||||
>{{ item.labelName }} {{ "permission" | i18n }}</label
|
||||
>
|
||||
<select
|
||||
bitInput
|
||||
class="-tw-ml-1 tw-max-w-36 tw-overflow-ellipsis !tw-rounded tw-border-0 !tw-bg-transparent tw-pl-0 tw-font-bold"
|
||||
formControlName="permission"
|
||||
[id]="'permission' + i"
|
||||
(blur)="handleBlur()"
|
||||
>
|
||||
<option *ngFor="let p of permissionList" [value]="p.perm">
|
||||
{{ p.labelId | i18n }}
|
||||
</option>
|
||||
</select>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #readOnlyPerm>
|
||||
<div
|
||||
*ngIf="item.accessAllItems"
|
||||
class="tw-max-w-36 tw-overflow-hidden tw-overflow-ellipsis tw-whitespace-nowrap tw-font-bold tw-text-muted"
|
||||
[appA11yTitle]="accessAllLabelId(item) | i18n"
|
||||
>
|
||||
{{ "canEdit" | i18n }}
|
||||
<i class="bwi bwi-filter tw-ml-1" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="item.readonly"
|
||||
class="tw-max-w-36 tw-overflow-hidden tw-overflow-ellipsis tw-whitespace-nowrap tw-font-bold tw-text-muted"
|
||||
[title]="permissionLabelId(item.readonlyPermission) | i18n"
|
||||
>
|
||||
{{ permissionLabelId(item.readonlyPermission) | i18n }}
|
||||
</div>
|
||||
</ng-template>
|
||||
</td>
|
||||
|
||||
<td bitCell *ngIf="showMemberRoles">
|
||||
{{ item.role | userType: "-" }}
|
||||
</td>
|
||||
|
||||
<td bitCell *ngIf="showGroupColumn">
|
||||
{{ item.viaGroupName ?? "-" }}
|
||||
</td>
|
||||
|
||||
<td bitCell>
|
||||
<button
|
||||
*ngIf="!item.readonly"
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
buttonType="muted"
|
||||
appA11yTitle="{{ 'remove' | i18n }} {{ item.labelName }}"
|
||||
[disabled]="disabled"
|
||||
(click)="selectionList.deselectItem(item.id); handleBlur()"
|
||||
></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="selectionList.selectedItems.length == 0">
|
||||
<td bitCell>{{ emptySelectionText }}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</bit-table>
|
||||
@@ -0,0 +1,250 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
|
||||
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
||||
import {
|
||||
AvatarModule,
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
TableModule,
|
||||
TabsModule,
|
||||
} from "@bitwarden/components";
|
||||
import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../tests/preloaded-english-i18n.module";
|
||||
|
||||
import { AccessSelectorComponent, PermissionMode } from "./access-selector.component";
|
||||
import { AccessItemType, CollectionPermission } from "./access-selector.models";
|
||||
import { UserTypePipe } from "./user-type.pipe";
|
||||
|
||||
/**
|
||||
* Helper class that makes it easier to test the AccessSelectorComponent by
|
||||
* exposing some protected methods/properties
|
||||
*/
|
||||
class TestableAccessSelectorComponent extends AccessSelectorComponent {
|
||||
selectItems(items: SelectItemView[]) {
|
||||
super.selectItems(items);
|
||||
}
|
||||
deselectItem(id: string) {
|
||||
this.selectionList.deselectItem(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper used to simulate a user selecting a new permission for a table row
|
||||
* @param index - "Row" index
|
||||
* @param perm - The new permission value
|
||||
*/
|
||||
changeSelectedItemPerm(index: number, perm: CollectionPermission) {
|
||||
this.selectionList.formArray.at(index).patchValue({
|
||||
permission: perm,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe("AccessSelectorComponent", () => {
|
||||
let component: TestableAccessSelectorComponent;
|
||||
let fixture: ComponentFixture<TestableAccessSelectorComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
AvatarModule,
|
||||
BadgeModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
TabsModule,
|
||||
TableModule,
|
||||
PreloadedEnglishI18nModule,
|
||||
JslibModule,
|
||||
IconButtonModule,
|
||||
],
|
||||
declarations: [TestableAccessSelectorComponent, UserTypePipe],
|
||||
providers: [],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TestableAccessSelectorComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
component.emptySelectionText = "Nothing selected";
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("item selection", () => {
|
||||
beforeEach(() => {
|
||||
component.items = [
|
||||
{
|
||||
id: "123",
|
||||
type: AccessItemType.Group,
|
||||
labelName: "Group 1",
|
||||
listName: "Group 1",
|
||||
},
|
||||
];
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should show the empty row when nothing is selected", () => {
|
||||
const emptyTableCell = fixture.nativeElement.querySelector("tbody tr td");
|
||||
expect(emptyTableCell?.textContent).toEqual("Nothing selected");
|
||||
});
|
||||
|
||||
it("should show one row when one value is selected", () => {
|
||||
component.selectItems([{ id: "123" } as any]);
|
||||
fixture.detectChanges();
|
||||
const firstColSpan = fixture.nativeElement.querySelector("tbody tr td span");
|
||||
expect(firstColSpan.textContent).toEqual("Group 1");
|
||||
});
|
||||
|
||||
it("should emit value change when a value is selected", () => {
|
||||
// Arrange
|
||||
const mockChange = jest.fn();
|
||||
component.registerOnChange(mockChange);
|
||||
component.permissionMode = PermissionMode.Edit;
|
||||
|
||||
// Act
|
||||
component.selectItems([{ id: "123" } as any]);
|
||||
|
||||
// Assert
|
||||
expect(mockChange.mock.calls.length).toEqual(1);
|
||||
expect(mockChange.mock.lastCall[0]).toHaveProperty("[0].id", "123");
|
||||
});
|
||||
|
||||
it("should emit value change when a row is modified", () => {
|
||||
// Arrange
|
||||
const mockChange = jest.fn();
|
||||
component.permissionMode = PermissionMode.Edit;
|
||||
component.selectItems([{ id: "123" } as any]);
|
||||
component.registerOnChange(mockChange); // Register change listener after setup
|
||||
|
||||
// Act
|
||||
component.changeSelectedItemPerm(0, CollectionPermission.Edit);
|
||||
|
||||
// Assert
|
||||
expect(mockChange.mock.calls.length).toEqual(1);
|
||||
expect(mockChange.mock.lastCall[0]).toHaveProperty("[0].id", "123");
|
||||
expect(mockChange.mock.lastCall[0]).toHaveProperty(
|
||||
"[0].permission",
|
||||
CollectionPermission.Edit
|
||||
);
|
||||
});
|
||||
|
||||
it("should emit value change when a row is removed", () => {
|
||||
// Arrange
|
||||
const mockChange = jest.fn();
|
||||
component.permissionMode = PermissionMode.Edit;
|
||||
component.selectItems([{ id: "123" } as any]);
|
||||
component.registerOnChange(mockChange); // Register change listener after setup
|
||||
|
||||
// Act
|
||||
component.deselectItem("123");
|
||||
|
||||
// Assert
|
||||
expect(mockChange.mock.calls.length).toEqual(1);
|
||||
expect(mockChange.mock.lastCall[0].length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should emit permission values when in edit mode", () => {
|
||||
// Arrange
|
||||
const mockChange = jest.fn();
|
||||
component.registerOnChange(mockChange);
|
||||
component.permissionMode = PermissionMode.Edit;
|
||||
|
||||
// Act
|
||||
component.selectItems([{ id: "123" } as any]);
|
||||
|
||||
// Assert
|
||||
expect(mockChange.mock.calls.length).toEqual(1);
|
||||
expect(mockChange.mock.lastCall[0]).toHaveProperty("[0].id", "123");
|
||||
expect(mockChange.mock.lastCall[0]).toHaveProperty("[0].permission");
|
||||
});
|
||||
|
||||
it("should not emit permission values when not in edit mode", () => {
|
||||
// Arrange
|
||||
const mockChange = jest.fn();
|
||||
component.registerOnChange(mockChange);
|
||||
component.permissionMode = PermissionMode.Hidden;
|
||||
|
||||
// Act
|
||||
component.selectItems([{ id: "123" } as any]);
|
||||
|
||||
// Assert
|
||||
expect(mockChange.mock.calls.length).toEqual(1);
|
||||
expect(mockChange.mock.lastCall[0]).toHaveProperty("[0].id", "123");
|
||||
expect(mockChange.mock.lastCall[0]).not.toHaveProperty("[0].permission");
|
||||
});
|
||||
});
|
||||
|
||||
describe("column rendering", () => {
|
||||
beforeEach(() => {
|
||||
component.items = [
|
||||
{
|
||||
id: "g1",
|
||||
type: AccessItemType.Group,
|
||||
labelName: "Group 1",
|
||||
listName: "Group 1",
|
||||
},
|
||||
{
|
||||
id: "m1",
|
||||
type: AccessItemType.Member,
|
||||
labelName: "Member 1",
|
||||
listName: "Member 1 (member1@email.com)",
|
||||
email: "member1@email.com",
|
||||
role: OrganizationUserType.Manager,
|
||||
status: OrganizationUserStatusType.Confirmed,
|
||||
},
|
||||
];
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
test.each([true, false])("should show the role column when enabled", (columnEnabled) => {
|
||||
// Act
|
||||
component.showMemberRoles = columnEnabled;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const colHeading = fixture.nativeElement.querySelector("#roleColHeading");
|
||||
expect(!!colHeading).toEqual(columnEnabled);
|
||||
});
|
||||
|
||||
test.each([true, false])("should show the group column when enabled", (columnEnabled) => {
|
||||
// Act
|
||||
component.showGroupColumn = columnEnabled;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const colHeading = fixture.nativeElement.querySelector("#groupColHeading");
|
||||
expect(!!colHeading).toEqual(columnEnabled);
|
||||
});
|
||||
|
||||
const permissionColumnCases = [
|
||||
[PermissionMode.Hidden, false],
|
||||
[PermissionMode.Edit, true],
|
||||
[PermissionMode.Readonly, true],
|
||||
];
|
||||
|
||||
test.each(permissionColumnCases)(
|
||||
"should show the permission column when enabled",
|
||||
(mode: PermissionMode, shouldShowColumn) => {
|
||||
// Act
|
||||
component.permissionMode = mode;
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const colHeading = fixture.nativeElement.querySelector("#permissionColHeading");
|
||||
expect(!!colHeading).toEqual(shouldShowColumn);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,290 @@
|
||||
import { Component, forwardRef, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { FormSelectionList } from "@bitwarden/angular/utils/form-selection-list";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
|
||||
|
||||
import {
|
||||
AccessItemType,
|
||||
AccessItemValue,
|
||||
AccessItemView,
|
||||
CollectionPermission,
|
||||
} from "./access-selector.models";
|
||||
|
||||
export enum PermissionMode {
|
||||
/**
|
||||
* No permission controls or column present. No permission values are emitted.
|
||||
*/
|
||||
Hidden = "hidden",
|
||||
|
||||
/**
|
||||
* No permission controls. Column rendered an if available on an item. No permission values are emitted
|
||||
*/
|
||||
Readonly = "readonly",
|
||||
|
||||
/**
|
||||
* Permission Controls and column present. Permission values are emitted.
|
||||
*/
|
||||
Edit = "edit",
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "bit-access-selector",
|
||||
templateUrl: "access-selector.component.html",
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => AccessSelectorComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AccessSelectorComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
private notifyOnChange: (v: unknown) => void;
|
||||
private notifyOnTouch: () => void;
|
||||
private pauseChangeNotification: boolean;
|
||||
|
||||
/**
|
||||
* The internal selection list that tracks the value of this form control / component.
|
||||
* It's responsible for keeping items sorted and synced with the rendered form controls
|
||||
* @protected
|
||||
*/
|
||||
protected selectionList = new FormSelectionList<AccessItemView, AccessItemValue>((item) => {
|
||||
const permissionControl = this.formBuilder.control(this.initialPermission);
|
||||
|
||||
const fg = this.formBuilder.group({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
permission: permissionControl,
|
||||
});
|
||||
|
||||
// Disable entire row form group if readonly
|
||||
if (item.readonly) {
|
||||
fg.disable();
|
||||
}
|
||||
|
||||
// Disable permission control if accessAllItems is enabled
|
||||
if (item.accessAllItems || this.permissionMode != PermissionMode.Edit) {
|
||||
permissionControl.disable();
|
||||
}
|
||||
|
||||
return fg;
|
||||
}, this._itemComparator.bind(this));
|
||||
|
||||
/**
|
||||
* Internal form group for this component.
|
||||
* @protected
|
||||
*/
|
||||
protected formGroup = this.formBuilder.group({
|
||||
items: this.selectionList.formArray,
|
||||
});
|
||||
|
||||
protected itemType = AccessItemType;
|
||||
protected permissionList = [
|
||||
{ perm: CollectionPermission.View, labelId: "canView" },
|
||||
{ perm: CollectionPermission.ViewExceptPass, labelId: "canViewExceptPass" },
|
||||
{ perm: CollectionPermission.Edit, labelId: "canEdit" },
|
||||
{ perm: CollectionPermission.EditExceptPass, labelId: "canEditExceptPass" },
|
||||
];
|
||||
protected initialPermission = CollectionPermission.View;
|
||||
|
||||
disabled: boolean;
|
||||
|
||||
/**
|
||||
* List of all selectable items that. Sorted internally.
|
||||
*/
|
||||
@Input()
|
||||
get items(): AccessItemView[] {
|
||||
return this.selectionList.allItems;
|
||||
}
|
||||
|
||||
set items(val: AccessItemView[]) {
|
||||
const selected = (this.selectionList.formArray.getRawValue() ?? []).concat(
|
||||
val.filter((m) => m.readonly)
|
||||
);
|
||||
this.selectionList.populateItems(
|
||||
val.map((m) => {
|
||||
m.icon = m.icon ?? this.itemIcon(m); // Ensure an icon is set
|
||||
return m;
|
||||
}),
|
||||
selected
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission mode that controls if the permission form controls and column should be present.
|
||||
*/
|
||||
@Input()
|
||||
get permissionMode(): PermissionMode {
|
||||
return this._permissionMode;
|
||||
}
|
||||
|
||||
set permissionMode(value: PermissionMode) {
|
||||
this._permissionMode = value;
|
||||
// Toggle any internal permission controls
|
||||
for (const control of this.selectionList.formArray.controls) {
|
||||
if (value == PermissionMode.Edit) {
|
||||
control.get("permission").enable();
|
||||
} else {
|
||||
control.get("permission").disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
private _permissionMode: PermissionMode = PermissionMode.Hidden;
|
||||
|
||||
/**
|
||||
* Column header for the selected items table
|
||||
*/
|
||||
@Input() columnHeader: string;
|
||||
|
||||
/**
|
||||
* Label used for the ng selector
|
||||
*/
|
||||
@Input() selectorLabelText: string;
|
||||
|
||||
/**
|
||||
* Helper text displayed under the ng selector
|
||||
*/
|
||||
@Input() selectorHelpText: string;
|
||||
|
||||
/**
|
||||
* Text that is shown in the table when no items are selected
|
||||
*/
|
||||
@Input() emptySelectionText: string;
|
||||
|
||||
/**
|
||||
* Flag for if the member roles column should be present
|
||||
*/
|
||||
@Input() showMemberRoles: boolean;
|
||||
|
||||
/**
|
||||
* Flag for if the group column should be present
|
||||
*/
|
||||
@Input() showGroupColumn: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly formBuilder: FormBuilder,
|
||||
private readonly i18nService: I18nService
|
||||
) {}
|
||||
|
||||
/** Required for NG_VALUE_ACCESSOR */
|
||||
registerOnChange(fn: any): void {
|
||||
this.notifyOnChange = fn;
|
||||
}
|
||||
|
||||
/** Required for NG_VALUE_ACCESSOR */
|
||||
registerOnTouched(fn: any): void {
|
||||
this.notifyOnTouch = fn;
|
||||
}
|
||||
|
||||
/** Required for NG_VALUE_ACCESSOR */
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
|
||||
// Keep the internal FormGroup in sync
|
||||
if (this.disabled) {
|
||||
this.formGroup.disable();
|
||||
} else {
|
||||
this.formGroup.enable();
|
||||
}
|
||||
}
|
||||
|
||||
/** Required for NG_VALUE_ACCESSOR */
|
||||
writeValue(selectedItems: AccessItemValue[]): void {
|
||||
// Modifying the selection list, mistakenly fires valueChanges in the
|
||||
// internal form array, so we need to know to pause external notification
|
||||
this.pauseChangeNotification = true;
|
||||
|
||||
// Always clear the internal selection list on a new value
|
||||
this.selectionList.deselectAll();
|
||||
|
||||
// We need to also select any read only items to appear in the table
|
||||
this.selectionList.selectItems(this.items.filter((m) => m.readonly).map((m) => m.id));
|
||||
|
||||
// If the new value is null, then we're done
|
||||
if (selectedItems == null) {
|
||||
this.pauseChangeNotification = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Unable to handle other value types, throw
|
||||
if (!Array.isArray(selectedItems)) {
|
||||
throw new Error("The access selector component only supports Array form values!");
|
||||
}
|
||||
|
||||
// Iterate and internally select each item
|
||||
for (const value of selectedItems) {
|
||||
this.selectionList.selectItem(value.id, value);
|
||||
}
|
||||
|
||||
this.pauseChangeNotification = false;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Watch the internal formArray for changes and propagate them
|
||||
this.selectionList.formArray.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => {
|
||||
if (!this.notifyOnChange || this.pauseChangeNotification) {
|
||||
return;
|
||||
}
|
||||
this.notifyOnChange(v);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
protected handleBlur() {
|
||||
if (!this.notifyOnTouch) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifyOnTouch();
|
||||
}
|
||||
|
||||
protected selectItems(items: SelectItemView[]) {
|
||||
this.pauseChangeNotification = true;
|
||||
this.selectionList.selectItems(items.map((i) => i.id));
|
||||
this.pauseChangeNotification = false;
|
||||
if (this.notifyOnChange != undefined) {
|
||||
this.notifyOnChange(this.selectionList.formArray.value);
|
||||
}
|
||||
}
|
||||
|
||||
protected itemIcon(item: AccessItemView) {
|
||||
switch (item.type) {
|
||||
case AccessItemType.Collection:
|
||||
return "bwi-collection";
|
||||
case AccessItemType.Group:
|
||||
return "bwi-users";
|
||||
case AccessItemType.Member:
|
||||
return "bwi-user";
|
||||
}
|
||||
}
|
||||
|
||||
protected permissionLabelId(perm: CollectionPermission) {
|
||||
return this.permissionList.find((p) => p.perm == perm)?.labelId;
|
||||
}
|
||||
|
||||
protected accessAllLabelId(item: AccessItemView) {
|
||||
return item.type == AccessItemType.Group ? "groupAccessAll" : "memberAccessAll";
|
||||
}
|
||||
|
||||
protected canEditItemPermission(item: AccessItemView) {
|
||||
return this.permissionMode == PermissionMode.Edit && !item.readonly && !item.accessAllItems;
|
||||
}
|
||||
|
||||
private _itemComparator(a: AccessItemView, b: AccessItemView) {
|
||||
if (a.type != b.type) {
|
||||
return a.type - b.type;
|
||||
}
|
||||
return this.i18nService.collator.compare(
|
||||
a.listName + a.labelName + a.readonly,
|
||||
b.listName + b.labelName + b.readonly
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
|
||||
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request";
|
||||
import { SelectionReadOnlyResponse } from "@bitwarden/common/models/response/selection-read-only.response";
|
||||
import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
|
||||
|
||||
/**
|
||||
* Permission options that replace/correspond with readOnly and hidePassword server fields.
|
||||
*/
|
||||
export enum CollectionPermission {
|
||||
View = "view",
|
||||
ViewExceptPass = "viewExceptPass",
|
||||
Edit = "edit",
|
||||
EditExceptPass = "editExceptPass",
|
||||
}
|
||||
|
||||
export enum AccessItemType {
|
||||
Collection,
|
||||
Group,
|
||||
Member,
|
||||
}
|
||||
|
||||
/**
|
||||
* A "generic" type that describes an item that can be selected from a
|
||||
* ng-select list and have its collection permission modified.
|
||||
*
|
||||
* Currently, it supports Collections, Groups, and Members. Members require some additional
|
||||
* details to render in the AccessSelectorComponent so their type is defined separately
|
||||
* and then joined back with the base type.
|
||||
*
|
||||
*/
|
||||
export type AccessItemView =
|
||||
| SelectItemView & {
|
||||
/**
|
||||
* Flag that this group/member can access all items.
|
||||
* This will disable the permission editor for this item.
|
||||
*/
|
||||
accessAllItems?: boolean;
|
||||
|
||||
/**
|
||||
* Flag that this item cannot be modified.
|
||||
* This will disable the permission editor and will keep
|
||||
* the item always selected.
|
||||
*/
|
||||
readonly?: boolean;
|
||||
|
||||
/**
|
||||
* Optional permission that will be rendered for this
|
||||
* item if it set to readonly.
|
||||
*/
|
||||
readonlyPermission?: CollectionPermission;
|
||||
} & (
|
||||
| {
|
||||
type: AccessItemType.Collection;
|
||||
viaGroupName?: string;
|
||||
}
|
||||
| {
|
||||
type: AccessItemType.Group;
|
||||
}
|
||||
| {
|
||||
type: AccessItemType.Member; // Members have a few extra details required to display, so they're added here
|
||||
email: string;
|
||||
role: OrganizationUserType;
|
||||
status: OrganizationUserStatusType;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* A type that is emitted as a value for the ngControl
|
||||
*/
|
||||
export type AccessItemValue = {
|
||||
id: string;
|
||||
permission?: CollectionPermission;
|
||||
type: AccessItemType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the older SelectionReadOnly interface to one of the new CollectionPermission values
|
||||
* for the dropdown in the AccessSelectorComponent
|
||||
* @param value
|
||||
*/
|
||||
export const convertToPermission = (value: SelectionReadOnlyResponse) => {
|
||||
if (value.readOnly) {
|
||||
return value.hidePasswords ? CollectionPermission.ViewExceptPass : CollectionPermission.View;
|
||||
} else {
|
||||
return value.hidePasswords ? CollectionPermission.EditExceptPass : CollectionPermission.Edit;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an AccessItemValue back into a SelectionReadOnly class using the CollectionPermission
|
||||
* to determine the values for `readOnly` and `hidePassword`
|
||||
* @param value
|
||||
*/
|
||||
export const convertToSelectionReadOnly = (value: AccessItemValue) => {
|
||||
return new SelectionReadOnlyRequest(
|
||||
value.id,
|
||||
readOnly(value.permission),
|
||||
hidePassword(value.permission)
|
||||
);
|
||||
};
|
||||
|
||||
const readOnly = (perm: CollectionPermission) =>
|
||||
[CollectionPermission.View, CollectionPermission.ViewExceptPass].includes(perm);
|
||||
|
||||
const hidePassword = (perm: CollectionPermission) =>
|
||||
[CollectionPermission.ViewExceptPass, CollectionPermission.EditExceptPass].includes(perm);
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
import { AccessSelectorComponent } from "./access-selector.component";
|
||||
import { UserTypePipe } from "./user-type.pipe";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [AccessSelectorComponent, UserTypePipe],
|
||||
exports: [AccessSelectorComponent],
|
||||
})
|
||||
export class AccessSelectorModule {}
|
||||
@@ -0,0 +1,302 @@
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
|
||||
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
||||
import {
|
||||
AvatarModule,
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
TableModule,
|
||||
TabsModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../tests/preloaded-english-i18n.module";
|
||||
|
||||
import { AccessSelectorComponent } from "./access-selector.component";
|
||||
import { AccessItemType, AccessItemView, CollectionPermission } from "./access-selector.models";
|
||||
import { UserTypePipe } from "./user-type.pipe";
|
||||
|
||||
export default {
|
||||
title: "Web/Organizations/Access Selector",
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
declarations: [AccessSelectorComponent, UserTypePipe],
|
||||
imports: [
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
AvatarModule,
|
||||
BadgeModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
TabsModule,
|
||||
TableModule,
|
||||
PreloadedEnglishI18nModule,
|
||||
JslibModule,
|
||||
IconButtonModule,
|
||||
],
|
||||
providers: [],
|
||||
}),
|
||||
],
|
||||
parameters: {},
|
||||
argTypes: {
|
||||
formObj: { table: { disable: true } },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const actionsData = {
|
||||
onValueChanged: action("onValueChanged"),
|
||||
onSubmit: action("onSubmit"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory to help build semi-realistic looking items
|
||||
* @param n - The number of items to build
|
||||
* @param type - Which type to build
|
||||
*/
|
||||
const itemsFactory = (n: number, type: AccessItemType) => {
|
||||
return [...Array(n)].map((_: unknown, id: number) => {
|
||||
const item: AccessItemView = {
|
||||
id: id.toString(),
|
||||
type: type,
|
||||
} as AccessItemView;
|
||||
|
||||
switch (item.type) {
|
||||
case AccessItemType.Collection:
|
||||
item.labelName = item.listName = `Collection ${id}`;
|
||||
item.id = item.id + "c";
|
||||
item.parentGrouping = "Collection Parent Group " + ((id % 2) + 1);
|
||||
break;
|
||||
case AccessItemType.Group:
|
||||
item.labelName = item.listName = `Group ${id}`;
|
||||
item.id = item.id + "g";
|
||||
break;
|
||||
case AccessItemType.Member:
|
||||
item.id = item.id + "m";
|
||||
item.email = `member${id}@email.com`;
|
||||
item.status = id % 3 == 0 ? 0 : 2;
|
||||
item.labelName = item.status == 2 ? `Member ${id}` : item.email;
|
||||
item.listName = item.status == 2 ? `${item.labelName} (${item.email})` : item.email;
|
||||
item.role = id % 5;
|
||||
break;
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
const sampleMembers = itemsFactory(10, AccessItemType.Member);
|
||||
const sampleGroups = itemsFactory(6, AccessItemType.Group);
|
||||
|
||||
const StandaloneAccessSelectorTemplate: Story<AccessSelectorComponent> = (
|
||||
args: AccessSelectorComponent
|
||||
) => ({
|
||||
props: {
|
||||
items: [],
|
||||
valueChanged: actionsData.onValueChanged,
|
||||
initialValue: [],
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<bit-access-selector
|
||||
(ngModelChange)="valueChanged($event)"
|
||||
[ngModel]="initialValue"
|
||||
[items]="items"
|
||||
[disabled]="disabled"
|
||||
[columnHeader]="columnHeader"
|
||||
[showGroupColumn]="showGroupColumn"
|
||||
[selectorLabelText]="selectorLabelText"
|
||||
[selectorHelpText]="selectorHelpText"
|
||||
[emptySelectionText]="emptySelectionText"
|
||||
[permissionMode]="permissionMode"
|
||||
[showMemberRoles]="showMemberRoles"
|
||||
></bit-access-selector>
|
||||
`,
|
||||
});
|
||||
|
||||
const memberCollectionAccessItems = itemsFactory(3, AccessItemType.Collection).concat([
|
||||
{
|
||||
id: "c1-group1",
|
||||
type: AccessItemType.Collection,
|
||||
labelName: "Collection 1",
|
||||
listName: "Collection 1",
|
||||
viaGroupName: "Group 1",
|
||||
readonlyPermission: CollectionPermission.View,
|
||||
readonly: true,
|
||||
},
|
||||
{
|
||||
id: "c1-group2",
|
||||
type: AccessItemType.Collection,
|
||||
labelName: "Collection 1",
|
||||
listName: "Collection 1",
|
||||
viaGroupName: "Group 2",
|
||||
readonlyPermission: CollectionPermission.ViewExceptPass,
|
||||
readonly: true,
|
||||
},
|
||||
]);
|
||||
|
||||
export const MemberCollectionAccess = StandaloneAccessSelectorTemplate.bind({});
|
||||
MemberCollectionAccess.args = {
|
||||
permissionMode: "edit",
|
||||
showMemberRoles: false,
|
||||
showGroupColumn: true,
|
||||
columnHeader: "Collection",
|
||||
selectorLabelText: "Select Collections",
|
||||
selectorHelpText: "Some helper text describing what this does",
|
||||
emptySelectionText: "No collections added",
|
||||
disabled: false,
|
||||
initialValue: [],
|
||||
items: memberCollectionAccessItems,
|
||||
};
|
||||
MemberCollectionAccess.story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
storyDescription: `
|
||||
Example of an access selector for modifying the collections a member has access to.
|
||||
Includes examples of a readonly group and member that cannot be edited.
|
||||
`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MemberGroupAccess = StandaloneAccessSelectorTemplate.bind({});
|
||||
MemberGroupAccess.args = {
|
||||
permissionMode: "readonly",
|
||||
showMemberRoles: false,
|
||||
columnHeader: "Groups",
|
||||
selectorLabelText: "Select Groups",
|
||||
selectorHelpText: "Some helper text describing what this does",
|
||||
emptySelectionText: "No groups added",
|
||||
disabled: false,
|
||||
initialValue: [{ id: "3g" }, { id: "0g" }],
|
||||
items: itemsFactory(4, AccessItemType.Group).concat([
|
||||
{
|
||||
id: "admin",
|
||||
type: AccessItemType.Group,
|
||||
listName: "Admin Group",
|
||||
labelName: "Admin Group",
|
||||
accessAllItems: true,
|
||||
},
|
||||
]),
|
||||
};
|
||||
MemberGroupAccess.story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
storyDescription: `
|
||||
Example of an access selector for selecting which groups an individual member belongs too.
|
||||
`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const GroupMembersAccess = StandaloneAccessSelectorTemplate.bind({});
|
||||
GroupMembersAccess.args = {
|
||||
permissionMode: "hidden",
|
||||
showMemberRoles: true,
|
||||
columnHeader: "Members",
|
||||
selectorLabelText: "Select Members",
|
||||
selectorHelpText: "Some helper text describing what this does",
|
||||
emptySelectionText: "No members added",
|
||||
disabled: false,
|
||||
initialValue: [{ id: "2m" }, { id: "0m" }],
|
||||
items: sampleMembers,
|
||||
};
|
||||
GroupMembersAccess.story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
storyDescription: `
|
||||
Example of an access selector for selecting which members belong to an specific group.
|
||||
`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CollectionAccess = StandaloneAccessSelectorTemplate.bind({});
|
||||
CollectionAccess.args = {
|
||||
permissionMode: "edit",
|
||||
showMemberRoles: false,
|
||||
columnHeader: "Groups/Members",
|
||||
selectorLabelText: "Select groups and members",
|
||||
selectorHelpText:
|
||||
"Permissions set for a member will replace permissions set by that member's group",
|
||||
emptySelectionText: "No members or groups added",
|
||||
disabled: false,
|
||||
initialValue: [
|
||||
{ id: "3g", permission: CollectionPermission.EditExceptPass },
|
||||
{ id: "0m", permission: CollectionPermission.View },
|
||||
],
|
||||
items: sampleGroups.concat(sampleMembers).concat([
|
||||
{
|
||||
id: "admin-group",
|
||||
type: AccessItemType.Group,
|
||||
listName: "Admin Group",
|
||||
labelName: "Admin Group",
|
||||
accessAllItems: true,
|
||||
readonly: true,
|
||||
},
|
||||
{
|
||||
id: "admin-member",
|
||||
type: AccessItemType.Member,
|
||||
listName: "Admin Member (admin@email.com)",
|
||||
labelName: "Admin Member",
|
||||
status: OrganizationUserStatusType.Confirmed,
|
||||
role: OrganizationUserType.Admin,
|
||||
email: "admin@email.com",
|
||||
accessAllItems: true,
|
||||
readonly: true,
|
||||
},
|
||||
]),
|
||||
};
|
||||
GroupMembersAccess.story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
storyDescription: `
|
||||
Example of an access selector for selecting which members/groups have access to a specific collection.
|
||||
`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const fb = new FormBuilder();
|
||||
|
||||
const ReactiveFormAccessSelectorTemplate: Story<AccessSelectorComponent> = (
|
||||
args: AccessSelectorComponent
|
||||
) => ({
|
||||
props: {
|
||||
items: [],
|
||||
onSubmit: actionsData.onSubmit,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<form [formGroup]="formObj" (ngSubmit)="onSubmit(formObj.controls.formItems.value)">
|
||||
<bit-access-selector
|
||||
formControlName="formItems"
|
||||
[items]="items"
|
||||
[columnHeader]="columnHeader"
|
||||
[selectorLabelText]="selectorLabelText"
|
||||
[selectorHelpText]="selectorHelpText"
|
||||
[emptySelectionText]="emptySelectionText"
|
||||
[permissionMode]="permissionMode"
|
||||
[showMemberRoles]="showMemberRoles"
|
||||
></bit-access-selector>
|
||||
<button type="submit" bitButton buttonType="primary" class="tw-mt-5">Submit</button>
|
||||
</form>
|
||||
`,
|
||||
});
|
||||
|
||||
export const ReactiveForm = ReactiveFormAccessSelectorTemplate.bind({});
|
||||
ReactiveForm.args = {
|
||||
formObj: fb.group({ formItems: [[{ id: "1g" }]] }),
|
||||
permissionMode: "edit",
|
||||
showMemberRoles: false,
|
||||
columnHeader: "Groups/Members",
|
||||
selectorLabelText: "Select groups and members",
|
||||
selectorHelpText:
|
||||
"Permissions set for a member will replace permissions set by that member's group",
|
||||
emptySelectionText: "No members or groups added",
|
||||
items: sampleGroups.concat(sampleMembers),
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./access-selector.component";
|
||||
export * from "./access-selector.module";
|
||||
export * from "./access-selector.models";
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
||||
|
||||
@Pipe({
|
||||
name: "userType",
|
||||
})
|
||||
export class UserTypePipe implements PipeTransform {
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
transform(value?: OrganizationUserType, unknownText?: string): string {
|
||||
if (value == null) {
|
||||
return unknownText ?? this.i18nService.t("unknown");
|
||||
}
|
||||
switch (value) {
|
||||
case OrganizationUserType.Owner:
|
||||
return this.i18nService.t("owner");
|
||||
case OrganizationUserType.Admin:
|
||||
return this.i18nService.t("admin");
|
||||
case OrganizationUserType.User:
|
||||
return this.i18nService.t("user");
|
||||
case OrganizationUserType.Manager:
|
||||
return this.i18nService.t("manager");
|
||||
case OrganizationUserType.Custom:
|
||||
return this.i18nService.t("custom");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,32 @@
|
||||
<app-navbar></app-navbar>
|
||||
<ng-container *ngIf="organization$ | async as organization">
|
||||
<div class="org-nav" *ngIf="organization">
|
||||
<div class="container d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
<app-organization-switcher
|
||||
class="my-auto pl-1"
|
||||
[activeOrganization]="organization"
|
||||
></app-organization-switcher>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="vault" routerLinkActive="active">
|
||||
<i class="bwi bwi-lock" aria-hidden="true"></i>
|
||||
{{ "vault" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="canShowManageTab(organization)">
|
||||
<a
|
||||
class="nav-link"
|
||||
[routerLink]="getManageRoute(organization)"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<i class="bwi bwi-sliders" aria-hidden="true"></i>
|
||||
{{ "manage" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="canShowToolsTab(organization)">
|
||||
<a
|
||||
class="nav-link"
|
||||
[routerLink]="getToolsRoute(organization)"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<i class="bwi bwi-wrench" aria-hidden="true"></i>
|
||||
{{ "tools" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="canShowSettingsTab(organization)">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="bwi bwi-cogs" aria-hidden="true"></i>
|
||||
{{ "settings" | i18n }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="org-nav !tw-h-32" *ngIf="organization$ | async as organization">
|
||||
<div class="container d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
<app-organization-switcher
|
||||
class="my-auto pl-1"
|
||||
[activeOrganization]="organization"
|
||||
></app-organization-switcher>
|
||||
<bit-tab-nav-bar class="-tw-mb-px">
|
||||
<bit-tab-link route="vault">{{ "vault" | i18n }}</bit-tab-link>
|
||||
<bit-tab-link *ngIf="canShowManageTab(organization)" [route]="getManageRoute(organization)">
|
||||
{{ "manage" | i18n }}
|
||||
</bit-tab-link>
|
||||
<bit-tab-link
|
||||
*ngIf="canShowReportsTab(organization)"
|
||||
[route]="getReportRoute(organization)"
|
||||
>
|
||||
{{ getReportTabLabel(organization) | i18n }}
|
||||
</bit-tab-link>
|
||||
<bit-tab-link *ngIf="canShowBillingTab(organization)" route="billing">{{
|
||||
"billing" | i18n
|
||||
}}</bit-tab-link>
|
||||
<bit-tab-link *ngIf="canShowSettingsTab(organization)" route="settings">{{
|
||||
"settings" | i18n
|
||||
}}</bit-tab-link>
|
||||
</bit-tab-nav-bar>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
<app-footer></app-footer>
|
||||
|
||||
@@ -3,11 +3,14 @@ import { ActivatedRoute } from "@angular/router";
|
||||
import { map, mergeMap, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationService,
|
||||
getOrganizationById,
|
||||
canAccessBillingTab,
|
||||
canAccessGroupsTab,
|
||||
canAccessManageTab,
|
||||
canAccessMembersTab,
|
||||
canAccessReportingTab,
|
||||
canAccessSettingsTab,
|
||||
canAccessToolsTab,
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
|
||||
@@ -17,7 +20,6 @@ import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
})
|
||||
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
organization$: Observable<Organization>;
|
||||
businessTokenPromise: Promise<void>;
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
@@ -43,27 +45,43 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
this._destroy.complete();
|
||||
}
|
||||
|
||||
canShowManageTab(organization: Organization): boolean {
|
||||
return canAccessManageTab(organization);
|
||||
}
|
||||
|
||||
canShowToolsTab(organization: Organization): boolean {
|
||||
return canAccessToolsTab(organization);
|
||||
}
|
||||
|
||||
canShowSettingsTab(organization: Organization): boolean {
|
||||
return canAccessSettingsTab(organization);
|
||||
}
|
||||
|
||||
getToolsRoute(organization: Organization): string {
|
||||
return organization.canAccessImportExport ? "tools/import" : "tools/exposed-passwords-report";
|
||||
canShowManageTab(organization: Organization): boolean {
|
||||
return canAccessManageTab(organization);
|
||||
}
|
||||
|
||||
canShowMembersTab(organization: Organization): boolean {
|
||||
return canAccessMembersTab(organization);
|
||||
}
|
||||
|
||||
canShowGroupsTab(organization: Organization): boolean {
|
||||
return canAccessGroupsTab(organization);
|
||||
}
|
||||
|
||||
canShowReportsTab(organization: Organization): boolean {
|
||||
return canAccessReportingTab(organization);
|
||||
}
|
||||
|
||||
canShowBillingTab(organization: Organization): boolean {
|
||||
return canAccessBillingTab(organization);
|
||||
}
|
||||
|
||||
getReportTabLabel(organization: Organization): string {
|
||||
return organization.useEvents ? "reporting" : "reports";
|
||||
}
|
||||
|
||||
getReportRoute(organization: Organization): string {
|
||||
return organization.useEvents ? "reporting/events" : "reporting/reports";
|
||||
}
|
||||
|
||||
getManageRoute(organization: Organization): string {
|
||||
let route: string;
|
||||
switch (true) {
|
||||
case organization.canManageUsers:
|
||||
route = "manage/people";
|
||||
route = "manage/members";
|
||||
break;
|
||||
case organization.canViewAssignedCollections || organization.canViewAllCollections:
|
||||
route = "manage/collections";
|
||||
@@ -71,18 +89,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
case organization.canManageGroups:
|
||||
route = "manage/groups";
|
||||
break;
|
||||
case organization.canManagePolicies:
|
||||
route = "manage/policies";
|
||||
break;
|
||||
case organization.canManageSso:
|
||||
route = "manage/sso";
|
||||
break;
|
||||
case organization.canManageScim:
|
||||
route = "manage/scim";
|
||||
break;
|
||||
case organization.canAccessEventLogs:
|
||||
route = "manage/events";
|
||||
break;
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
@@ -1,54 +1,57 @@
|
||||
<div class="page-header d-flex">
|
||||
<div class="tw-mb-4">
|
||||
<h1>{{ "eventLogs" | i18n }}</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="form-inline">
|
||||
<label class="sr-only" for="start">{{ "startDate" | i18n }}</label>
|
||||
<div class="tw-mt-4 tw-flex tw-items-center">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "from" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="datetime-local"
|
||||
class="form-control form-control-sm"
|
||||
id="start"
|
||||
placeholder="{{ 'startDate' | i18n }}"
|
||||
[(ngModel)]="start"
|
||||
placeholder="YYYY-MM-DDTHH:MM"
|
||||
(change)="dirtyDates = true"
|
||||
/>
|
||||
<span class="mx-2">-</span>
|
||||
<label class="sr-only" for="end">{{ "endDate" | i18n }}</label>
|
||||
</bit-form-field>
|
||||
<span class="tw-mx-2">-</span>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "to" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="datetime-local"
|
||||
class="form-control form-control-sm"
|
||||
id="end"
|
||||
placeholder="{{ 'endDate' | i18n }}"
|
||||
[(ngModel)]="end"
|
||||
placeholder="YYYY-MM-DDTHH:MM"
|
||||
(change)="dirtyDates = true"
|
||||
/>
|
||||
</div>
|
||||
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
|
||||
</bit-form-field>
|
||||
<form #refreshForm [appApiAction]="refreshPromise">
|
||||
<button
|
||||
class="tw-mx-3 tw-mt-1"
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary ml-3"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="loadEvents(true)"
|
||||
[disabled]="loaded && refreshForm.loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-refresh bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-spin': loaded && refreshForm.loading }"
|
||||
></i>
|
||||
{{ "refresh" | i18n }}
|
||||
{{ "update" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
|
||||
<form #exportForm [appApiAction]="exportPromise">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
|
||||
class="tw-mt-1"
|
||||
bitButton
|
||||
[ngClass]="{ loading: exportForm.loading }"
|
||||
(click)="exportEvents()"
|
||||
[disabled]="(loaded && exportForm.loading) || dirtyDates"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i>
|
||||
<span>{{ "export" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-fw"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{
|
||||
'bwi-sign-in': !exportForm.loading,
|
||||
'bwi-spinner bwi-spin': exportForm.loading
|
||||
}"
|
||||
></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -63,45 +66,44 @@
|
||||
</ng-container>
|
||||
<ng-container *ngIf="loaded">
|
||||
<p *ngIf="!events || !events.length">{{ "noEventsInList" | i18n }}</p>
|
||||
<table class="table table-hover" *ngIf="events && events.length">
|
||||
<thead>
|
||||
<bit-table *ngIf="events && events.length">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th class="border-top-0" width="210">{{ "timestamp" | i18n }}</th>
|
||||
<th class="border-top-0" width="40">
|
||||
<span class="sr-only">{{ "device" | i18n }}</span>
|
||||
</th>
|
||||
<th class="border-top-0" width="150">{{ "user" | i18n }}</th>
|
||||
<th class="border-top-0">{{ "event" | i18n }}</th>
|
||||
<th bitCell>{{ "timestamp" | i18n }}</th>
|
||||
<th bitCell>{{ "client" | i18n }}</th>
|
||||
<th bitCell>{{ "member" | i18n }}</th>
|
||||
<th bitCell>{{ "event" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let e of events">
|
||||
<td>{{ e.date | date: "medium" }}</td>
|
||||
<td>
|
||||
<i
|
||||
class="text-muted bwi bwi-lg {{ e.appIcon }}"
|
||||
title="{{ e.appName }}, {{ e.ip }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ e.appName }}, {{ e.ip }}</span>
|
||||
</ng-container>
|
||||
<ng-container body>
|
||||
<tr bitRow *ngFor="let e of events" alignContent="top">
|
||||
<td bitCell class="tw-whitespace-nowrap">{{ e.date | date: "medium" }}</td>
|
||||
<td bitCell>
|
||||
<span title="{{ e.appName }}, {{ e.ip }}">{{ e.appName }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<td bitCell>
|
||||
<span title="{{ e.userEmail }}">{{ e.userName }}</span>
|
||||
</td>
|
||||
<td [innerHTML]="e.message"></td>
|
||||
<td bitCell [innerHTML]="e.message"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</bit-table>
|
||||
<button
|
||||
#moreBtn
|
||||
[appApiAction]="morePromise"
|
||||
type="button"
|
||||
class="btn btn-block btn-link btn-submit"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(click)="loadEvents(false)"
|
||||
[disabled]="loaded && moreBtn.loading"
|
||||
*ngIf="continuationToken"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
*ngIf="moreBtn.loading"
|
||||
></i>
|
||||
<span>{{ "loadMore" | i18n }}</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { concatMap, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -20,13 +21,13 @@ import { EventService } from "../../core";
|
||||
selector: "app-org-events",
|
||||
templateUrl: "events.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class EventsComponent extends BaseEventsComponent implements OnInit {
|
||||
export class EventsComponent extends BaseEventsComponent implements OnInit, OnDestroy {
|
||||
exportFileName = "org-events";
|
||||
organizationId: string;
|
||||
organization: Organization;
|
||||
|
||||
private orgUsersUserIdMap = new Map<string, any>();
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@@ -53,17 +54,20 @@ export class EventsComponent extends BaseEventsComponent implements OnInit {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
this.organization = await this.organizationService.get(this.organizationId);
|
||||
if (this.organization == null || !this.organization.useEvents) {
|
||||
this.router.navigate(["/organizations", this.organizationId]);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.load();
|
||||
});
|
||||
this.route.params
|
||||
.pipe(
|
||||
concatMap(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
this.organization = await this.organizationService.get(this.organizationId);
|
||||
if (this.organization == null || !this.organization.useEvents) {
|
||||
await this.router.navigate(["/organizations", this.organizationId]);
|
||||
return;
|
||||
}
|
||||
await this.load();
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async load() {
|
||||
@@ -126,4 +130,9 @@ export class EventsComponent extends BaseEventsComponent implements OnInit {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export class GroupsComponent implements OnInit {
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
<div class="card-header">{{ "manage" | i18n }}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a
|
||||
routerLink="people"
|
||||
routerLink="members"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="organization.canManageUsers"
|
||||
>
|
||||
{{ "people" | i18n }}
|
||||
{{ "members" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="collections"
|
||||
@@ -28,38 +28,6 @@
|
||||
>
|
||||
{{ "groups" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="policies"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="organization.canManagePolicies"
|
||||
>
|
||||
{{ "policies" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="sso"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="organization.canManageSso"
|
||||
>
|
||||
{{ "singleSignOn" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="scim"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="organization.canManageScim"
|
||||
>
|
||||
{{ "scim" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="events"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="organization.canAccessEventLogs"
|
||||
>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="page-header">
|
||||
<h1>{{ "people" | i18n }}</h1>
|
||||
<div class="mt-2 d-flex">
|
||||
<div class="page-header d-flex">
|
||||
<h1>{{ "members" | i18n }}</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -3,32 +3,19 @@ import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
|
||||
import {
|
||||
canAccessOrgAdmin,
|
||||
canAccessGroupsTab,
|
||||
canAccessManageTab,
|
||||
canAccessSettingsTab,
|
||||
canAccessToolsTab,
|
||||
canAccessMembersTab,
|
||||
canAccessOrgAdmin,
|
||||
canManageCollections,
|
||||
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
|
||||
import { OrganizationPermissionsGuard } from "./guards/org-permissions.guard";
|
||||
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 { ManageComponent } from "./manage/manage.component";
|
||||
import { PeopleComponent } from "./manage/people.component";
|
||||
import { PoliciesComponent } from "./manage/policies.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 { 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";
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -46,71 +33,8 @@ const routes: Routes = [
|
||||
loadChildren: () => VaultModule,
|
||||
},
|
||||
{
|
||||
path: "tools",
|
||||
component: ToolsComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
organizationPermissions: canAccessToolsTab,
|
||||
},
|
||||
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: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "exposedPasswordsReport",
|
||||
organizationPermissions: (org: Organization) => org.canAccessReports,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "inactive-two-factor-report",
|
||||
component: InactiveTwoFactorReportComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "inactive2faReport",
|
||||
organizationPermissions: (org: Organization) => org.canAccessReports,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "reused-passwords-report",
|
||||
component: ReusedPasswordsReportComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "reusedPasswordsReport",
|
||||
organizationPermissions: (org: Organization) => org.canAccessReports,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "unsecured-websites-report",
|
||||
component: UnsecuredWebsitesReportComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "unsecuredWebsitesReport",
|
||||
organizationPermissions: (org: Organization) => org.canAccessReports,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "weak-passwords-report",
|
||||
component: WeakPasswordsReportComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "weakPasswordsReport",
|
||||
organizationPermissions: (org: Organization) => org.canAccessReports,
|
||||
},
|
||||
},
|
||||
],
|
||||
path: "settings",
|
||||
loadChildren: () => import("./settings").then((m) => m.OrganizationSettingsModule),
|
||||
},
|
||||
{
|
||||
path: "manage",
|
||||
@@ -123,7 +47,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
pathMatch: "full",
|
||||
redirectTo: "people",
|
||||
redirectTo: "members",
|
||||
},
|
||||
{
|
||||
path: "collections",
|
||||
@@ -131,21 +55,7 @@ const routes: Routes = [
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "collections",
|
||||
organizationPermissions: (org: Organization) =>
|
||||
org.canCreateNewCollections ||
|
||||
org.canEditAnyCollection ||
|
||||
org.canDeleteAnyCollection ||
|
||||
org.canEditAssignedCollections ||
|
||||
org.canDeleteAssignedCollections,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "events",
|
||||
component: EventsComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "eventLogs",
|
||||
organizationPermissions: (org: Organization) => org.canAccessEventLogs,
|
||||
organizationPermissions: canManageCollections,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -154,58 +64,31 @@ const routes: Routes = [
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "groups",
|
||||
organizationPermissions: (org: Organization) => org.canManageGroups,
|
||||
organizationPermissions: canAccessGroupsTab,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "people",
|
||||
path: "members",
|
||||
component: PeopleComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "people",
|
||||
organizationPermissions: (org: Organization) =>
|
||||
org.canManageUsers || org.canManageUsersPassword,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "policies",
|
||||
component: PoliciesComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "policies",
|
||||
organizationPermissions: (org: Organization) => org.canManagePolicies,
|
||||
titleId: "members",
|
||||
organizationPermissions: canAccessMembersTab,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
component: SettingsComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: { organizationPermissions: canAccessSettingsTab },
|
||||
children: [
|
||||
{ path: "", pathMatch: "full", redirectTo: "account" },
|
||||
{ path: "account", component: AccountComponent, data: { titleId: "myOrganization" } },
|
||||
{
|
||||
path: "two-factor",
|
||||
component: TwoFactorSetupComponent,
|
||||
data: { titleId: "twoStepLogin" },
|
||||
},
|
||||
{
|
||||
path: "billing",
|
||||
component: OrganizationBillingComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "billing",
|
||||
organizationPermissions: (org: Organization) => org.canManageBilling,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "subscription",
|
||||
component: OrganizationSubscriptionComponent,
|
||||
data: { titleId: "subscription" },
|
||||
},
|
||||
],
|
||||
path: "reporting",
|
||||
loadChildren: () =>
|
||||
import("./reporting/organization-reporting.module").then(
|
||||
(m) => m.OrganizationReportingModule
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "billing",
|
||||
loadChildren: () =>
|
||||
import("./billing/organization-billing.module").then((m) => m.OrganizationBillingModule),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
11
apps/web/src/app/organizations/organization.module.ts
Normal file
11
apps/web/src/app/organizations/organization.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
import { AccessSelectorModule } from "./components/access-selector";
|
||||
import { OrganizationsRoutingModule } from "./organization-routing.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, AccessSelectorModule, OrganizationsRoutingModule],
|
||||
})
|
||||
export class OrganizationModule {}
|
||||
12
apps/web/src/app/organizations/policies/index.ts
Normal file
12
apps/web/src/app/organizations/policies/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from "./policies.module";
|
||||
export { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||
export { DisableSendPolicy } from "./disable-send.component";
|
||||
export { MasterPasswordPolicy } from "./master-password.component";
|
||||
export { PasswordGeneratorPolicy } from "./password-generator.component";
|
||||
export { PersonalOwnershipPolicy } from "./personal-ownership.component";
|
||||
export { RequireSsoPolicy } from "./require-sso.component";
|
||||
export { ResetPasswordPolicy } from "./reset-password.component";
|
||||
export { SendOptionsPolicy } from "./send-options.component";
|
||||
export { SingleOrgPolicy } from "./single-org.component";
|
||||
export { TwoFactorAuthenticationPolicy } from "./two-factor-authentication.component";
|
||||
export { PoliciesComponent } from "./policies.component";
|
||||
@@ -10,7 +10,7 @@ import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { PolicyResponse } from "@bitwarden/common/models/response/policy.response";
|
||||
|
||||
import { PolicyListService } from "../../core";
|
||||
import { BasePolicy } from "../policies/base-policy.component";
|
||||
import { BasePolicy } from "../policies";
|
||||
|
||||
import { PolicyEditComponent } from "./policy-edit.component";
|
||||
|
||||
46
apps/web/src/app/organizations/policies/policies.module.ts
Normal file
46
apps/web/src/app/organizations/policies/policies.module.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { LooseComponentsModule, SharedModule } from "../../shared";
|
||||
|
||||
import { DisableSendPolicyComponent } from "./disable-send.component";
|
||||
import { MasterPasswordPolicyComponent } from "./master-password.component";
|
||||
import { PasswordGeneratorPolicyComponent } from "./password-generator.component";
|
||||
import { PersonalOwnershipPolicyComponent } from "./personal-ownership.component";
|
||||
import { PoliciesComponent } from "./policies.component";
|
||||
import { PolicyEditComponent } from "./policy-edit.component";
|
||||
import { RequireSsoPolicyComponent } from "./require-sso.component";
|
||||
import { ResetPasswordPolicyComponent } from "./reset-password.component";
|
||||
import { SendOptionsPolicyComponent } from "./send-options.component";
|
||||
import { SingleOrgPolicyComponent } from "./single-org.component";
|
||||
import { TwoFactorAuthenticationPolicyComponent } from "./two-factor-authentication.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, LooseComponentsModule],
|
||||
declarations: [
|
||||
DisableSendPolicyComponent,
|
||||
MasterPasswordPolicyComponent,
|
||||
PasswordGeneratorPolicyComponent,
|
||||
PersonalOwnershipPolicyComponent,
|
||||
RequireSsoPolicyComponent,
|
||||
ResetPasswordPolicyComponent,
|
||||
SendOptionsPolicyComponent,
|
||||
SingleOrgPolicyComponent,
|
||||
TwoFactorAuthenticationPolicyComponent,
|
||||
PoliciesComponent,
|
||||
PolicyEditComponent,
|
||||
],
|
||||
exports: [
|
||||
DisableSendPolicyComponent,
|
||||
MasterPasswordPolicyComponent,
|
||||
PasswordGeneratorPolicyComponent,
|
||||
PersonalOwnershipPolicyComponent,
|
||||
RequireSsoPolicyComponent,
|
||||
ResetPasswordPolicyComponent,
|
||||
SendOptionsPolicyComponent,
|
||||
SingleOrgPolicyComponent,
|
||||
TwoFactorAuthenticationPolicyComponent,
|
||||
PoliciesComponent,
|
||||
PolicyEditComponent,
|
||||
],
|
||||
})
|
||||
export class PoliciesModule {}
|
||||
@@ -17,7 +17,7 @@ import { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||
import { PolicyRequest } from "@bitwarden/common/models/request/policy.request";
|
||||
import { PolicyResponse } from "@bitwarden/common/models/response/policy.response";
|
||||
|
||||
import { BasePolicy, BasePolicyComponent } from "../policies/base-policy.component";
|
||||
import { BasePolicy, BasePolicyComponent } from "../policies";
|
||||
|
||||
@Component({
|
||||
selector: "app-policy-edit",
|
||||
@@ -0,0 +1,87 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessReportingTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
|
||||
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
import { EventsComponent } from "../manage/events.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 { 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: [OrganizationPermissionsGuard],
|
||||
data: { organizationPermissions: canAccessReportingTab },
|
||||
children: [
|
||||
{ path: "", pathMatch: "full", redirectTo: "reports" },
|
||||
{
|
||||
path: "reports",
|
||||
component: ReportsHomeComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "reports",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "exposed-passwords-report",
|
||||
component: ExposedPasswordsReportComponent,
|
||||
data: {
|
||||
titleId: "exposedPasswordsReport",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "inactive-two-factor-report",
|
||||
component: InactiveTwoFactorReportComponent,
|
||||
data: {
|
||||
titleId: "inactive2faReport",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "reused-passwords-report",
|
||||
component: ReusedPasswordsReportComponent,
|
||||
data: {
|
||||
titleId: "reusedPasswordsReport",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "unsecured-websites-report",
|
||||
component: UnsecuredWebsitesReportComponent,
|
||||
data: {
|
||||
titleId: "unsecuredWebsitesReport",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "weak-passwords-report",
|
||||
component: WeakPasswordsReportComponent,
|
||||
data: {
|
||||
titleId: "weakPasswordsReport",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "events",
|
||||
component: EventsComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "eventLogs",
|
||||
organizationPermissions: (org: Organization) => org.canAccessEventLogs,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@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="events"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="organization.canAccessEventLogs"
|
||||
>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="reports"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="organization.canAccessReports"
|
||||
>
|
||||
{{ "reports" | 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,36 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { concatMap, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-reporting",
|
||||
templateUrl: "reporting.component.html",
|
||||
})
|
||||
export class ReportingComponent implements OnInit, OnDestroy {
|
||||
organization: Organization;
|
||||
showLeftNav = true;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(private route: ActivatedRoute, private organizationService: OrganizationService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.params
|
||||
.pipe(
|
||||
concatMap(async (params) => {
|
||||
this.organization = await this.organizationService.get(params.organizationId);
|
||||
this.showLeftNav = this.organization.canAccessEventLogs;
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
takeUntil(this.destrory$)
|
||||
)
|
||||
.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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="page-header">
|
||||
<h1>{{ "myOrganization" | i18n }}</h1>
|
||||
<h1>{{ "organizationInfo" | i18n }}</h1>
|
||||
</div>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
@@ -51,16 +51,6 @@
|
||||
[disabled]="selfHosted || !canManageBilling"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="identifier">{{ "identifier" | i18n }}</label>
|
||||
<input
|
||||
id="identifier"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Identifier"
|
||||
[(ngModel)]="org.identifier"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<bit-avatar [text]="org.name" [id]="org.id" size="large"></bit-avatar>
|
||||
@@ -87,31 +77,6 @@
|
||||
{{ "rotateApiKey" | i18n }}
|
||||
</button>
|
||||
</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">
|
||||
<h1>{{ "dangerZone" | i18n }}</h1>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,6 @@ import { OrganizationResponse } from "@bitwarden/common/models/response/organiza
|
||||
|
||||
import { ApiKeyComponent } from "../../settings/api-key.component";
|
||||
import { PurgeVaultComponent } from "../../settings/purge-vault.component";
|
||||
import { TaxInfoComponent } from "../../settings/tax-info.component";
|
||||
|
||||
import { DeleteOrganizationComponent } from "./delete-organization.component";
|
||||
|
||||
@@ -32,7 +31,6 @@ export class AccountComponent {
|
||||
apiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
rotateApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
|
||||
|
||||
selfHosted = false;
|
||||
canManageBilling = true;
|
||||
@@ -100,12 +98,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() {
|
||||
await this.modalService.openViewRef(
|
||||
DeleteOrganizationComponent,
|
||||
|
||||
2
apps/web/src/app/organizations/settings/index.ts
Normal file
2
apps/web/src/app/organizations/settings/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./organization-settings.module";
|
||||
export { DeleteOrganizationComponent } from "./delete-organization.component";
|
||||
@@ -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,153 +0,0 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
|
||||
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/verify-bank.request";
|
||||
import { BillingResponse } from "@bitwarden/common/models/response/billing.response";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-billing",
|
||||
templateUrl: "./organization-billing.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
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<void>;
|
||||
|
||||
// TODO - Make sure to properly split out the billing/invoice and payment method/account during org admin refresh
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private route: ActivatedRoute,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
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.organizationApiService.getBilling(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.organizationApiService.verifyBank(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessSettingsTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
|
||||
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
import { PoliciesComponent } from "../policies";
|
||||
|
||||
import { AccountComponent } from "./account.component";
|
||||
import { SettingsComponent } from "./settings.component";
|
||||
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: SettingsComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: { organizationPermissions: canAccessSettingsTab },
|
||||
children: [
|
||||
{ path: "", pathMatch: "full", redirectTo: "account" },
|
||||
{ path: "account", component: AccountComponent, data: { titleId: "organizationInfo" } },
|
||||
{
|
||||
path: "two-factor",
|
||||
component: TwoFactorSetupComponent,
|
||||
data: { titleId: "twoStepLogin" },
|
||||
},
|
||||
{
|
||||
path: "policies",
|
||||
component: PoliciesComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
organizationPermissions: (org: Organization) => org.canManagePolicies,
|
||||
titleId: "policies",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "tools",
|
||||
loadChildren: () =>
|
||||
import("../tools/import-export/org-import-export.module").then(
|
||||
(m) => m.OrganizationImportExportModule
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class OrganizationSettingsRoutingModule {}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { LooseComponentsModule, SharedModule } from "../../shared";
|
||||
import { PoliciesModule } from "../policies";
|
||||
|
||||
import { AccountComponent } from "./account.component";
|
||||
import { DeleteOrganizationComponent } from "./delete-organization.component";
|
||||
import { OrganizationSettingsRoutingModule } from "./organization-settings-routing.module";
|
||||
import { SettingsComponent } from "./settings.component";
|
||||
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, LooseComponentsModule, PoliciesModule, OrganizationSettingsRoutingModule],
|
||||
declarations: [
|
||||
SettingsComponent,
|
||||
AccountComponent,
|
||||
DeleteOrganizationComponent,
|
||||
TwoFactorSetupComponent,
|
||||
],
|
||||
})
|
||||
export class OrganizationSettingsModule {}
|
||||
@@ -5,27 +5,56 @@
|
||||
<div class="card-header">{{ "settings" | i18n }}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a routerLink="account" class="list-group-item" routerLinkActive="active">
|
||||
{{ "myOrganization" | i18n }}
|
||||
</a>
|
||||
<a routerLink="subscription" class="list-group-item" routerLinkActive="active">
|
||||
{{ "subscription" | i18n }}
|
||||
{{ "organizationInfo" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="billing"
|
||||
routerLink="policies"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="showBilling"
|
||||
*ngIf="organization?.canManagePolicies"
|
||||
>
|
||||
{{ "billing" | i18n }}
|
||||
{{ "policies" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="two-factor"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="access2fa"
|
||||
*ngIf="organization?.use2fa"
|
||||
>
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="tools/import"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="organization?.canAccessImportExport"
|
||||
>
|
||||
{{ "importData" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="tools/export"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="organization?.canAccessImportExport"
|
||||
>
|
||||
{{ "exportVault" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="sso"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="organization?.canManageSso"
|
||||
>
|
||||
{{ "singleSignOn" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
routerLink="scim"
|
||||
class="list-group-item"
|
||||
routerLinkActive="active"
|
||||
*ngIf="organization?.canManageScim"
|
||||
>
|
||||
{{ "scim" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-settings",
|
||||
templateUrl: "settings.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class SettingsComponent {
|
||||
access2fa = false;
|
||||
showBilling: boolean;
|
||||
export class SettingsComponent implements OnInit, OnDestroy {
|
||||
organization: Organization;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private platformUtilsService: PlatformUtilsService
|
||||
) {}
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(private route: ActivatedRoute, private organizationService: OrganizationService) {}
|
||||
|
||||
ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
const organization = await this.organizationService.get(params.organizationId);
|
||||
this.showBilling = !this.platformUtilsService.isSelfHost() && organization.canManageBilling;
|
||||
this.access2fa = organization.use2fa;
|
||||
});
|
||||
this.route.params
|
||||
.pipe(
|
||||
switchMap(async (params) => await this.organizationService.get(params.organizationId)),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe((organization) => {
|
||||
this.organization = organization;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import { ProductType } from "@bitwarden/common/enums/productType";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { OrganizationSponsorshipRedeemRequest } from "@bitwarden/common/models/request/organization/organization-sponsorship-redeem.request";
|
||||
|
||||
import { DeleteOrganizationComponent } from "../../organizations/settings";
|
||||
import { OrganizationPlansComponent } from "../../settings/organization-plans.component";
|
||||
import { DeleteOrganizationComponent } from "../settings/delete-organization.component";
|
||||
|
||||
@Component({
|
||||
selector: "families-for-enterprise-setup",
|
||||
|
||||
@@ -28,7 +28,7 @@ import { VerifyRecoverDeleteComponent } from "./accounts/verify-recover-delete.c
|
||||
import { HomeGuard } from "./guards/home.guard";
|
||||
import { FrontendLayoutComponent } from "./layouts/frontend-layout.component";
|
||||
import { UserLayoutComponent } from "./layouts/user-layout.component";
|
||||
import { OrganizationsRoutingModule } from "./organizations/organization-routing.module";
|
||||
import { OrganizationModule } from "./organizations/organization.module";
|
||||
import { AcceptFamilySponsorshipComponent } from "./organizations/sponsorships/accept-family-sponsorship.component";
|
||||
import { FamiliesForEnterpriseSetupComponent } from "./organizations/sponsorships/families-for-enterprise-setup.component";
|
||||
import { ReportsModule } from "./reports";
|
||||
@@ -251,7 +251,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "organizations",
|
||||
loadChildren: () => OrganizationsRoutingModule,
|
||||
loadChildren: () => OrganizationModule,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./reports.module";
|
||||
export * from "./models/report-entry";
|
||||
export * from "./models/report-variant";
|
||||
export * from "./shared";
|
||||
export * from "./reports";
|
||||
|
||||
@@ -2,9 +2,8 @@ import { Component, OnInit } from "@angular/core";
|
||||
|
||||
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 { ReportEntry, ReportVariant } from "../shared";
|
||||
|
||||
@Component({
|
||||
selector: "app-reports-home",
|
||||
|
||||
@@ -10,19 +10,16 @@ import { ReportsHomeComponent } from "./pages/reports-home.component";
|
||||
import { ReusedPasswordsReportComponent } from "./pages/reused-passwords-report.component";
|
||||
import { UnsecuredWebsitesReportComponent } from "./pages/unsecured-websites-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 { ReportsRoutingModule } from "./reports-routing.module";
|
||||
import { ReportsSharedModule } from "./shared";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, SharedModule, ReportsRoutingModule],
|
||||
imports: [CommonModule, SharedModule, ReportsSharedModule, ReportsRoutingModule],
|
||||
declarations: [
|
||||
BreachReportComponent,
|
||||
ExposedPasswordsReportComponent,
|
||||
InactiveTwoFactorReportComponent,
|
||||
ReportCardComponent,
|
||||
ReportListComponent,
|
||||
ReportsLayoutComponent,
|
||||
ReportsHomeComponent,
|
||||
ReusedPasswordsReportComponent,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ReportInactiveTwoFactor } from "./icons/report-inactive-two-factor.icon
|
||||
import { ReportReusedPasswords } from "./icons/report-reused-passwords.icon";
|
||||
import { ReportUnsecuredWebsites } from "./icons/report-unsecured-websites.icon";
|
||||
import { ReportWeakPasswords } from "./icons/report-weak-passwords.icon";
|
||||
import { ReportEntry } from "./models/report-entry";
|
||||
import { ReportEntry } from "./shared";
|
||||
|
||||
export enum ReportType {
|
||||
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
|
||||
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"
|
||||
>
|
||||
<div class="tw-relative">
|
||||
@@ -4,8 +4,8 @@ import { Meta, Story, moduleMetadata } from "@storybook/angular";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BadgeModule, IconModule } from "@bitwarden/components";
|
||||
|
||||
import { PremiumBadgeComponent } from "../../components/premium-badge.component";
|
||||
import { PreloadedEnglishI18nModule } from "../../tests/preloaded-english-i18n.module";
|
||||
import { PremiumBadgeComponent } from "../../../components/premium-badge.component";
|
||||
import { PreloadedEnglishI18nModule } from "../../../tests/preloaded-english-i18n.module";
|
||||
import { ReportVariant } from "../models/report-variant";
|
||||
|
||||
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 { BadgeModule, IconModule } from "@bitwarden/components";
|
||||
|
||||
import { PremiumBadgeComponent } from "../../components/premium-badge.component";
|
||||
import { PreloadedEnglishI18nModule } from "../../tests/preloaded-english-i18n.module";
|
||||
import { PremiumBadgeComponent } from "../../../components/premium-badge.component";
|
||||
import { PreloadedEnglishI18nModule } from "../../../tests/preloaded-english-i18n.module";
|
||||
import { reports } from "../../reports";
|
||||
import { ReportVariant } from "../models/report-variant";
|
||||
import { ReportCardComponent } from "../report-card/report-card.component";
|
||||
import { reports } from "../reports";
|
||||
|
||||
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 { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.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/billing-history.response";
|
||||
|
||||
@Component({
|
||||
selector: "app-user-billing",
|
||||
templateUrl: "user-billing-history.component.html",
|
||||
selector: "app-billing-history-view",
|
||||
templateUrl: "billing-history-view.component.html",
|
||||
})
|
||||
export class UserBillingHistoryComponent implements OnInit {
|
||||
export class BillingHistoryViewComponent implements OnInit {
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
billing: BillingHistoryResponse;
|
||||
paymentMethodType = PaymentMethodType;
|
||||
transactionType = TransactionType;
|
||||
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
if (this.platformUtilsService.isSelfHost()) {
|
||||
this.router.navigate(["/settings/subscription"]);
|
||||
return;
|
||||
}
|
||||
await this.load();
|
||||
this.firstLoaded = true;
|
||||
@@ -42,12 +37,4 @@ export class UserBillingHistoryComponent implements OnInit {
|
||||
this.billing = await this.apiService.getUserBillingHistory();
|
||||
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/billing-history.response";
|
||||
|
||||
@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>
|
||||
{{ "paymentMethod" | i18n }}
|
||||
</h1>
|
||||
@@ -40,18 +40,48 @@
|
||||
<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="
|
||||
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>
|
||||
<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>
|
||||
<i class="bwi bwi-fw" [ngClass]="paymentSourceClasses"></i>
|
||||
<span *ngIf="paymentSourceInApp">{{ "inAppPurchase" | i18n }}</span>
|
||||
{{ paymentSource.description }}
|
||||
</p>
|
||||
@@ -66,4 +96,35 @@
|
||||
*ngIf="showAdjustPayment"
|
||||
>
|
||||
</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>
|
||||
|
||||
@@ -1,37 +1,76 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, FormControl, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } 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 { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType";
|
||||
import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank.request";
|
||||
import { BillingPaymentResponse } from "@bitwarden/common/models/response/billing-payment.response";
|
||||
import { OrganizationResponse } from "@bitwarden/common/models/response/organization.response";
|
||||
|
||||
import { TaxInfoComponent } from "./tax-info.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-payment-method",
|
||||
templateUrl: "payment-method.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class PaymentMethodComponent implements OnInit {
|
||||
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
|
||||
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
showAdjustPayment = false;
|
||||
showAddCredit = false;
|
||||
billing: BillingPaymentResponse;
|
||||
org: OrganizationResponse;
|
||||
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(
|
||||
protected apiService: ApiService,
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
private router: Router
|
||||
private router: Router,
|
||||
private logService: LogService,
|
||||
private route: ActivatedRoute,
|
||||
private formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
if (this.platformUtilsService.isSelfHost()) {
|
||||
this.router.navigate(["/settings/subscription"]);
|
||||
}
|
||||
await this.load();
|
||||
this.firstLoaded = true;
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.params.subscribe(async (params) => {
|
||||
if (params.organizationId) {
|
||||
this.organizationId = params.organizationId;
|
||||
} else if (this.platformUtilsService.isSelfHost()) {
|
||||
this.router.navigate(["/settings/subscription"]);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.load();
|
||||
this.firstLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
@@ -39,7 +78,16 @@ export class PaymentMethodComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.billing = await this.apiService.getUserBillingPayment();
|
||||
|
||||
if (this.forOrganization) {
|
||||
const billingPromise = this.organizationApiService.getBilling(this.organizationId);
|
||||
const orgPromise = this.organizationApiService.get(this.organizationId);
|
||||
|
||||
[this.billing, this.org] = await Promise.all([billingPromise, orgPromise]);
|
||||
} else {
|
||||
this.billing = await this.apiService.getUserBillingPayment();
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@@ -85,6 +133,34 @@ 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.organizationApiService.verifyBank(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() {
|
||||
return this.billing == null || this.billing.balance <= 0;
|
||||
}
|
||||
@@ -97,6 +173,36 @@ export class PaymentMethodComponent implements OnInit {
|
||||
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() {
|
||||
return (
|
||||
this.paymentSource != null &&
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||
import { PaymentMethodComponent } from "./payment-method.component";
|
||||
import { PremiumComponent } from "./premium.component";
|
||||
import { SubscriptionComponent } from "./subscription.component";
|
||||
import { UserBillingHistoryComponent } from "./user-billing-history.component";
|
||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -31,7 +31,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "billing-history",
|
||||
component: UserBillingHistoryComponent,
|
||||
component: BillingHistoryViewComponent,
|
||||
data: { titleId: "billingHistory" },
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
<div class="tabbed-header">
|
||||
<h1>{{ "twoStepLogin" | i18n }}</h1>
|
||||
<h1 *ngIf="!organizationId">{{ "twoStepLogin" | i18n }}</h1>
|
||||
<h1 *ngIf="organizationId">{{ "twoStepLoginEnforcement" | i18n }}</h1>
|
||||
</div>
|
||||
<p *ngIf="!organizationId">{{ "twoStepLoginDesc" | i18n }}</p>
|
||||
<p *ngIf="organizationId">{{ "twoStepLoginOrganizationDesc" | i18n }}</p>
|
||||
<ng-container *ngIf="organizationId">
|
||||
<p>
|
||||
{{ "twoStepLoginOrganizationDescStart" | i18n }}
|
||||
<a routerLink="../policies">{{ "twoStepLoginPolicy" | i18n }}.</a>
|
||||
<br />
|
||||
{{ "twoStepLoginOrganizationDuoDesc" | i18n }}
|
||||
</p>
|
||||
<p>{{ "twoStepLoginOrganizationSsoDesc" | i18n }}</p>
|
||||
</ng-container>
|
||||
<bit-callout type="warning" *ngIf="!organizationId">
|
||||
<p>{{ "twoStepLoginRecoveryWarning" | i18n }}</p>
|
||||
<button bitButton buttonType="secondary" (click)="recoveryCode()">
|
||||
|
||||
@@ -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>
|
||||
@@ -42,31 +42,10 @@ import { GroupAddEditComponent as OrgGroupAddEditComponent } from "../organizati
|
||||
import { GroupsComponent as OrgGroupsComponent } from "../organizations/manage/groups.component";
|
||||
import { ManageComponent as OrgManageComponent } from "../organizations/manage/manage.component";
|
||||
import { PeopleComponent as OrgPeopleComponent } from "../organizations/manage/people.component";
|
||||
import { PoliciesComponent as OrgPoliciesComponent } from "../organizations/manage/policies.component";
|
||||
import { PolicyEditComponent as OrgPolicyEditComponent } from "../organizations/manage/policy-edit.component";
|
||||
import { ResetPasswordComponent as OrgResetPasswordComponent } from "../organizations/manage/reset-password.component";
|
||||
import { UserAddEditComponent as OrgUserAddEditComponent } from "../organizations/manage/user-add-edit.component";
|
||||
import { UserConfirmComponent as OrgUserConfirmComponent } from "../organizations/manage/user-confirm.component";
|
||||
import { UserGroupsComponent as OrgUserGroupsComponent } from "../organizations/manage/user-groups.component";
|
||||
import { DisableSendPolicyComponent } from "../organizations/policies/disable-send.component";
|
||||
import { MasterPasswordPolicyComponent } from "../organizations/policies/master-password.component";
|
||||
import { PasswordGeneratorPolicyComponent } from "../organizations/policies/password-generator.component";
|
||||
import { PersonalOwnershipPolicyComponent } from "../organizations/policies/personal-ownership.component";
|
||||
import { RequireSsoPolicyComponent } from "../organizations/policies/require-sso.component";
|
||||
import { ResetPasswordPolicyComponent } from "../organizations/policies/reset-password.component";
|
||||
import { SendOptionsPolicyComponent } from "../organizations/policies/send-options.component";
|
||||
import { SingleOrgPolicyComponent } from "../organizations/policies/single-org.component";
|
||||
import { TwoFactorAuthenticationPolicyComponent } from "../organizations/policies/two-factor-authentication.component";
|
||||
import { AccountComponent as OrgAccountComponent } from "../organizations/settings/account.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 { DeleteOrganizationComponent } from "../organizations/settings/delete-organization.component";
|
||||
import { DownloadLicenseComponent } from "../organizations/settings/download-license.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 { TwoFactorSetupComponent as OrgTwoFactorSetupComponent } from "../organizations/settings/two-factor-setup.component";
|
||||
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
|
||||
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
|
||||
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../organizations/tools/exposed-passwords-report.component";
|
||||
@@ -88,6 +67,8 @@ import { AddCreditComponent } from "../settings/add-credit.component";
|
||||
import { AdjustPaymentComponent } from "../settings/adjust-payment.component";
|
||||
import { AdjustStorageComponent } from "../settings/adjust-storage.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 { ChangeEmailComponent } from "../settings/change-email.component";
|
||||
import { ChangeKdfComponent } from "../settings/change-kdf.component";
|
||||
@@ -127,7 +108,6 @@ import { TwoFactorWebAuthnComponent } from "../settings/two-factor-webauthn.comp
|
||||
import { TwoFactorYubiKeyComponent } from "../settings/two-factor-yubikey.component";
|
||||
import { UpdateKeyComponent } from "../settings/update-key.component";
|
||||
import { UpdateLicenseComponent } from "../settings/update-license.component";
|
||||
import { UserBillingHistoryComponent } from "../settings/user-billing-history.component";
|
||||
import { UserSubscriptionComponent } from "../settings/user-subscription.component";
|
||||
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
||||
import { VerifyEmailComponent } from "../settings/verify-email.component";
|
||||
@@ -173,10 +153,8 @@ import { SharedModule } from ".";
|
||||
AddEditCustomFieldsComponent,
|
||||
AdjustPaymentComponent,
|
||||
AdjustStorageComponent,
|
||||
AdjustSubscription,
|
||||
ApiKeyComponent,
|
||||
AttachmentsComponent,
|
||||
BillingSyncApiKeyComponent,
|
||||
BillingSyncKeyComponent,
|
||||
BulkActionsComponent,
|
||||
BulkDeleteComponent,
|
||||
@@ -186,15 +164,11 @@ import { SharedModule } from ".";
|
||||
ChangeEmailComponent,
|
||||
ChangeKdfComponent,
|
||||
ChangePasswordComponent,
|
||||
ChangePlanComponent,
|
||||
CollectionsComponent,
|
||||
CreateOrganizationComponent,
|
||||
DeauthorizeSessionsComponent,
|
||||
DeleteAccountComponent,
|
||||
DeleteOrganizationComponent,
|
||||
DisableSendPolicyComponent,
|
||||
DomainRulesComponent,
|
||||
DownloadLicenseComponent,
|
||||
EmergencyAccessAddEditComponent,
|
||||
EmergencyAccessAttachmentsComponent,
|
||||
EmergencyAccessComponent,
|
||||
@@ -208,16 +182,12 @@ import { SharedModule } from ".";
|
||||
FrontendLayoutComponent,
|
||||
HintComponent,
|
||||
LockComponent,
|
||||
MasterPasswordPolicyComponent,
|
||||
NavbarComponent,
|
||||
NestedCheckboxComponent,
|
||||
OrganizationSwitcherComponent,
|
||||
OrgAccountComponent,
|
||||
OrgAddEditComponent,
|
||||
OrganizationBillingComponent,
|
||||
OrganizationLayoutComponent,
|
||||
OrganizationPlansComponent,
|
||||
OrganizationSubscriptionComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgBulkConfirmComponent,
|
||||
OrgBulkRestoreRevokeComponent,
|
||||
@@ -234,13 +204,9 @@ import { SharedModule } from ".";
|
||||
OrgManageCollectionsComponent,
|
||||
OrgManageComponent,
|
||||
OrgPeopleComponent,
|
||||
OrgPoliciesComponent,
|
||||
OrgPolicyEditComponent,
|
||||
OrgResetPasswordComponent,
|
||||
OrgReusedPasswordsReportComponent,
|
||||
OrgSettingComponent,
|
||||
OrgToolsComponent,
|
||||
OrgTwoFactorSetupComponent,
|
||||
OrgUnsecuredWebsitesReportComponent,
|
||||
OrgUserAddEditComponent,
|
||||
OrgUserConfirmComponent,
|
||||
@@ -248,12 +214,10 @@ import { SharedModule } from ".";
|
||||
OrgWeakPasswordsReportComponent,
|
||||
GeneratorComponent,
|
||||
PasswordGeneratorHistoryComponent,
|
||||
PasswordGeneratorPolicyComponent,
|
||||
PasswordRepromptComponent,
|
||||
UserVerificationPromptComponent,
|
||||
PaymentComponent,
|
||||
PaymentMethodComponent,
|
||||
PersonalOwnershipPolicyComponent,
|
||||
PreferencesComponent,
|
||||
PremiumBadgeComponent,
|
||||
PremiumComponent,
|
||||
@@ -264,25 +228,20 @@ import { SharedModule } from ".";
|
||||
RecoverTwoFactorComponent,
|
||||
RegisterComponent,
|
||||
RemovePasswordComponent,
|
||||
RequireSsoPolicyComponent,
|
||||
ResetPasswordPolicyComponent,
|
||||
SecurityComponent,
|
||||
SecurityKeysComponent,
|
||||
SendAddEditComponent,
|
||||
SendComponent,
|
||||
SendEffluxDatesComponent,
|
||||
SendOptionsPolicyComponent,
|
||||
SetPasswordComponent,
|
||||
SettingsComponent,
|
||||
ShareComponent,
|
||||
SingleOrgPolicyComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
SsoComponent,
|
||||
SubscriptionComponent,
|
||||
TaxInfoComponent,
|
||||
ToolsComponent,
|
||||
TwoFactorAuthenticationPolicyComponent,
|
||||
TwoFactorAuthenticatorComponent,
|
||||
TwoFactorComponent,
|
||||
TwoFactorDuoComponent,
|
||||
@@ -297,7 +256,8 @@ import { SharedModule } from ".";
|
||||
UpdateLicenseComponent,
|
||||
UpdatePasswordComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
UserBillingHistoryComponent,
|
||||
BillingHistoryComponent,
|
||||
BillingHistoryViewComponent,
|
||||
UserLayoutComponent,
|
||||
UserSubscriptionComponent,
|
||||
UserVerificationComponent,
|
||||
@@ -318,7 +278,6 @@ import { SharedModule } from ".";
|
||||
AddEditCustomFieldsComponent,
|
||||
AdjustPaymentComponent,
|
||||
AdjustStorageComponent,
|
||||
AdjustSubscription,
|
||||
ApiKeyComponent,
|
||||
AttachmentsComponent,
|
||||
BulkActionsComponent,
|
||||
@@ -329,15 +288,11 @@ import { SharedModule } from ".";
|
||||
ChangeEmailComponent,
|
||||
ChangeKdfComponent,
|
||||
ChangePasswordComponent,
|
||||
ChangePlanComponent,
|
||||
CollectionsComponent,
|
||||
CreateOrganizationComponent,
|
||||
DeauthorizeSessionsComponent,
|
||||
DeleteAccountComponent,
|
||||
DeleteOrganizationComponent,
|
||||
DisableSendPolicyComponent,
|
||||
DomainRulesComponent,
|
||||
DownloadLicenseComponent,
|
||||
EmergencyAccessAddEditComponent,
|
||||
EmergencyAccessAttachmentsComponent,
|
||||
EmergencyAccessComponent,
|
||||
@@ -351,16 +306,12 @@ import { SharedModule } from ".";
|
||||
FrontendLayoutComponent,
|
||||
HintComponent,
|
||||
LockComponent,
|
||||
MasterPasswordPolicyComponent,
|
||||
NavbarComponent,
|
||||
NestedCheckboxComponent,
|
||||
OrganizationSwitcherComponent,
|
||||
OrgAccountComponent,
|
||||
OrgAddEditComponent,
|
||||
OrganizationBillingComponent,
|
||||
OrganizationLayoutComponent,
|
||||
OrganizationPlansComponent,
|
||||
OrganizationSubscriptionComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgBulkConfirmComponent,
|
||||
OrgBulkRestoreRevokeComponent,
|
||||
@@ -377,13 +328,9 @@ import { SharedModule } from ".";
|
||||
OrgManageCollectionsComponent,
|
||||
OrgManageComponent,
|
||||
OrgPeopleComponent,
|
||||
OrgPoliciesComponent,
|
||||
OrgPolicyEditComponent,
|
||||
OrgResetPasswordComponent,
|
||||
OrgReusedPasswordsReportComponent,
|
||||
OrgSettingComponent,
|
||||
OrgToolsComponent,
|
||||
OrgTwoFactorSetupComponent,
|
||||
OrgUnsecuredWebsitesReportComponent,
|
||||
OrgUserAddEditComponent,
|
||||
OrgUserConfirmComponent,
|
||||
@@ -391,11 +338,9 @@ import { SharedModule } from ".";
|
||||
OrgWeakPasswordsReportComponent,
|
||||
GeneratorComponent,
|
||||
PasswordGeneratorHistoryComponent,
|
||||
PasswordGeneratorPolicyComponent,
|
||||
PasswordRepromptComponent,
|
||||
PaymentComponent,
|
||||
PaymentMethodComponent,
|
||||
PersonalOwnershipPolicyComponent,
|
||||
PreferencesComponent,
|
||||
PremiumBadgeComponent,
|
||||
PremiumComponent,
|
||||
@@ -406,25 +351,20 @@ import { SharedModule } from ".";
|
||||
RecoverTwoFactorComponent,
|
||||
RegisterComponent,
|
||||
RemovePasswordComponent,
|
||||
RequireSsoPolicyComponent,
|
||||
ResetPasswordPolicyComponent,
|
||||
SecurityComponent,
|
||||
SecurityKeysComponent,
|
||||
SendAddEditComponent,
|
||||
SendComponent,
|
||||
SendEffluxDatesComponent,
|
||||
SendOptionsPolicyComponent,
|
||||
SetPasswordComponent,
|
||||
SettingsComponent,
|
||||
ShareComponent,
|
||||
SingleOrgPolicyComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
SsoComponent,
|
||||
SubscriptionComponent,
|
||||
TaxInfoComponent,
|
||||
ToolsComponent,
|
||||
TwoFactorAuthenticationPolicyComponent,
|
||||
TwoFactorAuthenticatorComponent,
|
||||
TwoFactorComponent,
|
||||
TwoFactorDuoComponent,
|
||||
@@ -439,7 +379,8 @@ import { SharedModule } from ".";
|
||||
UpdateLicenseComponent,
|
||||
UpdatePasswordComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
UserBillingHistoryComponent,
|
||||
BillingHistoryComponent,
|
||||
BillingHistoryViewComponent,
|
||||
UserLayoutComponent,
|
||||
UserSubscriptionComponent,
|
||||
UserVerificationComponent,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DragDropModule } from "@angular/cdk/drag-drop";
|
||||
import { DatePipe, CommonModule } from "@angular/common";
|
||||
import { CommonModule, DatePipe } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { RouterModule } from "@angular/router";
|
||||
@@ -14,8 +14,11 @@ import {
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
MenuModule,
|
||||
TableModule,
|
||||
TabsModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// Register the locales for the application
|
||||
@@ -46,7 +49,10 @@ import "./locales";
|
||||
MenuModule,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
TabsModule,
|
||||
TableModule,
|
||||
AvatarModule,
|
||||
IconButtonModule,
|
||||
],
|
||||
exports: [
|
||||
CommonModule,
|
||||
@@ -66,7 +72,10 @@ import "./locales";
|
||||
MenuModule,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
TabsModule,
|
||||
TableModule,
|
||||
AvatarModule,
|
||||
IconButtonModule,
|
||||
],
|
||||
providers: [DatePipe],
|
||||
bootstrap: [],
|
||||
|
||||
@@ -314,7 +314,7 @@
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
[disabled]="loading || importBlockedByPolicy$ | async"
|
||||
[disabled]="loading || (importBlockedByPolicy$ | async)"
|
||||
[ngClass]="{ manual: importBlockedByPolicy$ | async }"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
|
||||
@@ -97,14 +97,6 @@
|
||||
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
||||
{{ organizations[0].name }}
|
||||
</button>
|
||||
<span class="tw-ml-auto">
|
||||
<button [bitMenuTriggerFor]="orgMenu" class="org-options">
|
||||
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu class="filter-organization-options" #orgMenu>
|
||||
<app-organization-options [organization]="organizations[0]"></app-organization-options>
|
||||
</bit-menu>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
|
||||
Reference in New Issue
Block a user