mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01: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 commit24bb775
to 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-pickae39afe
to 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:
parent
d12ef5c734
commit
09c3bc8f1b
@ -10,6 +10,8 @@
|
||||
"**/app/core/*",
|
||||
"**/reports/*",
|
||||
"**/app/shared/*",
|
||||
"**/organizations/settings/*",
|
||||
"**/organizations/policies/*",
|
||||
"@bitwarden/web-vault/*",
|
||||
"src/**/*"
|
||||
],
|
||||
|
@ -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>
|
||||
|
@ -278,7 +278,7 @@
|
||||
"message": "Search favorites"
|
||||
},
|
||||
"searchType": {
|
||||
"message": "Search type",
|
||||
"message": "Search Type",
|
||||
"description": "Search item type"
|
||||
},
|
||||
"searchVault": {
|
||||
@ -1290,11 +1290,24 @@
|
||||
"twoStepLogin": {
|
||||
"message": "Two-step login"
|
||||
},
|
||||
"twoStepLoginEnforcement": {
|
||||
"message": "Two-step Login Enforcement"
|
||||
},
|
||||
"twoStepLoginDesc": {
|
||||
"message": "Secure your account by requiring an additional step when logging in."
|
||||
},
|
||||
"twoStepLoginOrganizationDesc": {
|
||||
"message": "Require two-step login for your organization's users by configuring providers at the organization level."
|
||||
"twoStepLoginOrganizationDescStart": {
|
||||
"message": "Enforce Bitwarden Two-step Login options for members by using the ",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enforce Bitwarden Two-step Login options for members by using the Two-step Login Policy.'"
|
||||
},
|
||||
"twoStepLoginPolicy": {
|
||||
"message": "Two-step Login Policy"
|
||||
},
|
||||
"twoStepLoginOrganizationDuoDesc": {
|
||||
"message": "To enforce Two-step Login through Duo, use the options below."
|
||||
},
|
||||
"twoStepLoginOrganizationSsoDesc": {
|
||||
"message": "If you have setup SSO or plan to, Two-step Login may already be enforced through your Identity Provider."
|
||||
},
|
||||
"twoStepLoginRecoveryWarning": {
|
||||
"message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place."
|
||||
@ -1541,7 +1554,11 @@
|
||||
},
|
||||
"reportsDesc": {
|
||||
"message": "Identify and close security gaps in your online accounts by clicking the reports below.",
|
||||
"description": "Vault health reports can be used to evaluate the security of your Bitwarden individual or organization vault."
|
||||
"description": "Vault health reports can be used to evaluate the security of your Bitwarden personal or organization vault."
|
||||
},
|
||||
"orgsReportsDesc": {
|
||||
"message": "Identify and close security gaps in your organization's accounts by clicking the reports below.",
|
||||
"description": "Vault health reports can be used to evaluate the security of your Bitwarden personal or organization Vault."
|
||||
},
|
||||
"unsecuredWebsitesReport": {
|
||||
"message": "Unsecure websites"
|
||||
@ -2990,6 +3007,9 @@
|
||||
"myOrganization": {
|
||||
"message": "My organization"
|
||||
},
|
||||
"organizationInfo": {
|
||||
"message": "Organization Info"
|
||||
},
|
||||
"deleteOrganization": {
|
||||
"message": "Delete organization"
|
||||
},
|
||||
@ -3108,7 +3128,7 @@
|
||||
"message": "Enter your installation id"
|
||||
},
|
||||
"limitSubscriptionDesc": {
|
||||
"message": "Set a seat limit for your subscription. Once this limit is reached, you will not be able to invite new users."
|
||||
"message": "Set a seat limit for your subscription. Once this limit is reached, you will not be able to invite new members."
|
||||
},
|
||||
"maxSeatLimit": {
|
||||
"message": "Seat Limit (optional)",
|
||||
@ -3129,7 +3149,7 @@
|
||||
"message": "Adjustments to your subscription will result in prorated changes to your billing totals. If newly invited users exceed your subscription seats, you will immediately receive a prorated charge for the additional users."
|
||||
},
|
||||
"subscriptionUserSeats": {
|
||||
"message": "Your subscription allows for a total of $COUNT$ users.",
|
||||
"message": "Your subscription allows for a total of $COUNT$ members.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@ -3153,10 +3173,10 @@
|
||||
"message": "For additional help in managing your subscription, please contact Customer Support."
|
||||
},
|
||||
"subscriptionUserSeatsUnlimitedAutoscale": {
|
||||
"message": "Adjustments to your subscription will result in prorated changes to your billing totals. If newly invited users exceed your subscription seats, you will immediately receive a prorated charge for the additional users."
|
||||
"message": "Adjustments to your subscription will result in prorated changes to your billing totals. If newly invited members exceed your subscription seats, you will immediately receive a prorated charge for the additional members."
|
||||
},
|
||||
"subscriptionUserSeatsLimitedAutoscale": {
|
||||
"message": "Adjustments to your subscription will result in prorated changes to your billing totals. If newly invited users exceed your subscription seats, you will immediately receive a prorated charge for the additional users until your $MAX$ seat limit is reached.",
|
||||
"message": "Adjustments to your subscription will result in prorated changes to your billing totals. If newly invited members exceed your subscription seats, you will immediately receive a prorated charge for the additional members until your $MAX$ seat limit is reached.",
|
||||
"placeholders": {
|
||||
"max": {
|
||||
"content": "$1",
|
||||
@ -3165,7 +3185,7 @@
|
||||
}
|
||||
},
|
||||
"subscriptionFreePlan": {
|
||||
"message": "You cannot invite more than $COUNT$ users without upgrading your plan.",
|
||||
"message": "You cannot invite more than $COUNT$ members without upgrading your plan.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@ -3174,7 +3194,7 @@
|
||||
}
|
||||
},
|
||||
"subscriptionFamiliesPlan": {
|
||||
"message": "You cannot invite more than $COUNT$ users without upgrading your plan. Please contact Customer Support to upgrade.",
|
||||
"message": "You cannot invite more than $COUNT$ members without upgrading your plan. Please contact Customer Support to upgrade.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@ -3183,7 +3203,7 @@
|
||||
}
|
||||
},
|
||||
"subscriptionSponsoredFamiliesPlan": {
|
||||
"message": "Your subscription allows for a total of $COUNT$ users. Your plan is sponsored and billed to an external organization.",
|
||||
"message": "Your subscription allows for a total of $COUNT$ members. Your plan is sponsored and billed to an external organization.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@ -3192,7 +3212,7 @@
|
||||
}
|
||||
},
|
||||
"subscriptionMaxReached": {
|
||||
"message": "Adjustments to your subscription will result in prorated changes to your billing totals. You cannot invite more than $COUNT$ users without increasing your subscription seats.",
|
||||
"message": "Adjustments to your subscription will result in prorated changes to your billing totals. You cannot invite more than $COUNT$ members without increasing your subscription seats.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@ -3686,6 +3706,12 @@
|
||||
"ssoIdentifierRequired": {
|
||||
"message": "Organization identifier is required."
|
||||
},
|
||||
"ssoIdentifier": {
|
||||
"message": "SSO Identifier"
|
||||
},
|
||||
"ssoIdentifierHint": {
|
||||
"message": "Provide this ID to your members to login with SSO."
|
||||
},
|
||||
"unlinkSso": {
|
||||
"message": "Unlink SSO"
|
||||
},
|
||||
@ -4031,7 +4057,7 @@
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"disableSendExemption": {
|
||||
"message": "Organization users that can manage the organization's policies are exempt from this policy's enforcement."
|
||||
"message": "Organization members that can manage the organization's policies are exempt from this policy's enforcement."
|
||||
},
|
||||
"sendDisabled": {
|
||||
"message": "Send removed",
|
||||
@ -4050,7 +4076,7 @@
|
||||
"description": "'Sends' is a plural noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendOptionsExemption": {
|
||||
"message": "Organization users that can manage the organization's policies are exempt from this policy's enforcement."
|
||||
"message": "Organization members that can manage the organization's policies are exempt from this policy's enforcement."
|
||||
},
|
||||
"disableHideEmail": {
|
||||
"message": "Always show member’s email address with recipients when creating or editing a Send.",
|
||||
@ -4087,6 +4113,9 @@
|
||||
"permissions": {
|
||||
"message": "Permissions"
|
||||
},
|
||||
"permission": {
|
||||
"message": "Permission"
|
||||
},
|
||||
"managerPermissions": {
|
||||
"message": "Manager Permissions"
|
||||
},
|
||||
@ -4361,19 +4390,19 @@
|
||||
"message": "Allow admins to reset master passwords for members."
|
||||
},
|
||||
"resetPasswordPolicyWarning": {
|
||||
"message": "Users in the organization will need to self-enroll or be auto-enrolled before administrators can reset their master password."
|
||||
"message": "Members in the organization will need to self-enroll or be auto-enrolled before administrators can reset their master password."
|
||||
},
|
||||
"resetPasswordPolicyAutoEnroll": {
|
||||
"message": "Automatic enrollment"
|
||||
},
|
||||
"resetPasswordPolicyAutoEnrollDescription": {
|
||||
"message": "All users will be automatically enrolled in password reset once their invite is accepted and will not be allowed to withdraw."
|
||||
"message": "All members will be automatically enrolled in password reset once their invite is accepted and will not be allowed to withdraw."
|
||||
},
|
||||
"resetPasswordPolicyAutoEnrollWarning": {
|
||||
"message": "Users already in the organization will not be retroactively enrolled in password reset. They will need to self-enroll before administrators can reset their master password."
|
||||
"message": "Members already in the organization will not be retroactively enrolled in password reset. They will need to self-enroll before administrators can reset their master password."
|
||||
},
|
||||
"resetPasswordPolicyAutoEnrollCheckbox": {
|
||||
"message": "Require new users to be enrolled automatically"
|
||||
"message": "Require new members to be enrolled automatically"
|
||||
},
|
||||
"resetPasswordAutoEnrollInviteWarning": {
|
||||
"message": "This organization has an Enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password."
|
||||
@ -4492,6 +4521,10 @@
|
||||
"clients": {
|
||||
"message": "Clients"
|
||||
},
|
||||
"client": {
|
||||
"message": "Client",
|
||||
"description": "This is used as a table header to describe which client application created an event log."
|
||||
},
|
||||
"providerAdmin": {
|
||||
"message": "Provider admin"
|
||||
},
|
||||
@ -5376,6 +5409,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputMaxLength": {
|
||||
"message": "Input must not exceed $COUNT$ characters in length.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "20"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fieldsNeedAttention": {
|
||||
"message": "$COUNT$ field(s) above need your attention.",
|
||||
"placeholders": {
|
||||
@ -5391,6 +5433,12 @@
|
||||
"on": {
|
||||
"message": "On"
|
||||
},
|
||||
"members": {
|
||||
"message": "Members"
|
||||
},
|
||||
"reporting": {
|
||||
"message": "Reporting"
|
||||
},
|
||||
"cardBrandMir": {
|
||||
"message": "Mir"
|
||||
},
|
||||
@ -5414,5 +5462,41 @@
|
||||
},
|
||||
"multiSelectClearAll": {
|
||||
"message": "Clear all"
|
||||
},
|
||||
"from": {
|
||||
"message": "From"
|
||||
},
|
||||
"to": {
|
||||
"message": "To"
|
||||
},
|
||||
"member": {
|
||||
"message": "Member"
|
||||
},
|
||||
"update": {
|
||||
"message": "Update"
|
||||
},
|
||||
"role": {
|
||||
"message": "Role"
|
||||
},
|
||||
"canView": {
|
||||
"message": "Can view"
|
||||
},
|
||||
"canViewExceptPass": {
|
||||
"message": "Can view, except passwords"
|
||||
},
|
||||
"canEdit": {
|
||||
"message": "Can edit"
|
||||
},
|
||||
"canEditExceptPass": {
|
||||
"message": "Can edit, except passwords"
|
||||
},
|
||||
"group": {
|
||||
"message": "Group"
|
||||
},
|
||||
"groupAccessAll": {
|
||||
"message": "This group can access and modify all items."
|
||||
},
|
||||
"memberAccessAll": {
|
||||
"message": "This member can access and modify all items."
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { Directive, Input, OnInit, Self } from "@angular/core";
|
||||
import { ControlValueAccessor, UntypedFormControl, NgControl, Validators } from "@angular/forms";
|
||||
|
||||
import { dirtyRequired } from "@bitwarden/angular/validators/dirty.validator";
|
||||
|
||||
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
|
||||
@Directive()
|
||||
export abstract class BaseCvaComponent implements ControlValueAccessor, OnInit {
|
||||
@ -15,10 +13,7 @@ export abstract class BaseCvaComponent implements ControlValueAccessor, OnInit {
|
||||
}
|
||||
|
||||
get isRequired() {
|
||||
return (
|
||||
this.controlDir.control.hasValidator(Validators.required) ||
|
||||
this.controlDir.control.hasValidator(dirtyRequired)
|
||||
);
|
||||
return this.controlDir.control.hasValidator(Validators.required);
|
||||
}
|
||||
|
||||
@Input() label: string;
|
||||
|
@ -1,26 +0,0 @@
|
||||
<div class="form-group">
|
||||
<label>{{ label }}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" readonly [value]="controlValue" />
|
||||
<div class="input-group-append" *ngIf="showLaunch">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'launch' | i18n }}"
|
||||
(click)="launchUri(controlValue)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group-append" *ngIf="showCopy">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||
(click)="copy(controlValue)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,25 +0,0 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
|
||||
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
|
||||
@Component({
|
||||
selector: "app-input-text-readonly",
|
||||
templateUrl: "input-text-readonly.component.html",
|
||||
})
|
||||
export class InputTextReadOnlyComponent {
|
||||
@Input() controlValue: string;
|
||||
@Input() label: string;
|
||||
@Input() showCopy = true;
|
||||
@Input() showLaunch = false;
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
copy(value: string) {
|
||||
this.platformUtilsService.copyToClipboard(value);
|
||||
}
|
||||
|
||||
launchUri(url: string) {
|
||||
this.platformUtilsService.launchUri(url);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<div class="form-group">
|
||||
<label [attr.for]="controlId">
|
||||
{{ label }}
|
||||
<small *ngIf="isRequired" class="text-muted form-text d-inline"
|
||||
>({{ "required" | i18n }})</small
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
[formControl]="internalControl"
|
||||
class="form-control"
|
||||
[attr.id]="controlId"
|
||||
[attr.aria-describedby]="describedById"
|
||||
[attr.aria-invalid]="controlDir.control.invalid"
|
||||
(blur)="onBlurInternal()"
|
||||
/>
|
||||
<div *ngIf="showDescribedBy" [attr.id]="describedById">
|
||||
<small
|
||||
*ngIf="helperText != null && !controlDir.control.hasError(helperTextSameAsError)"
|
||||
class="form-text text-muted"
|
||||
>
|
||||
{{ helperText }}
|
||||
</small>
|
||||
<small class="error-inline" *ngIf="controlDir.control.hasError('required')" role="alert">
|
||||
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ "error" | i18n }}:</span>
|
||||
{{
|
||||
controlDir.control.hasError(helperTextSameAsError)
|
||||
? helperText
|
||||
: ("fieldRequiredError" | i18n: label)
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
@ -1,48 +0,0 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import { BaseCvaComponent } from "./base-cva.component";
|
||||
|
||||
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
|
||||
@Component({
|
||||
selector: "app-input-text[label][controlId]",
|
||||
templateUrl: "input-text.component.html",
|
||||
})
|
||||
export class InputTextComponent extends BaseCvaComponent implements OnInit {
|
||||
@Input() helperTextSameAsError: string;
|
||||
@Input() requiredErrorMessage: string;
|
||||
@Input() stripSpaces = false;
|
||||
|
||||
transformValue: (value: string) => string = null;
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
if (this.stripSpaces) {
|
||||
this.transformValue = this.doStripSpaces;
|
||||
}
|
||||
}
|
||||
|
||||
writeValue(value: string) {
|
||||
this.internalControl.setValue(value == null ? "" : value);
|
||||
}
|
||||
|
||||
protected onValueChangesInternal: any = (value: string) => {
|
||||
let newValue = value;
|
||||
if (this.transformValue != null) {
|
||||
newValue = this.transformValue(value);
|
||||
this.internalControl.setValue(newValue, { emitEvent: false });
|
||||
}
|
||||
this.onChange(newValue);
|
||||
};
|
||||
|
||||
protected onValueChangeInternal(value: string) {
|
||||
let newValue = value;
|
||||
if (this.transformValue != null) {
|
||||
newValue = this.transformValue(value);
|
||||
this.internalControl.setValue(newValue, { emitEvent: false });
|
||||
}
|
||||
}
|
||||
|
||||
private doStripSpaces(value: string) {
|
||||
return value.replace(/ /g, "");
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
<div class="form-group">
|
||||
<label [attr.for]="controlId">
|
||||
{{ label }}
|
||||
<small *ngIf="isRequired" class="text-muted form-text d-inline"
|
||||
>({{ "required" | i18n }})</small
|
||||
>
|
||||
</label>
|
||||
<select
|
||||
class="form-control"
|
||||
[attr.id]="controlId"
|
||||
[attr.aria-invalid]="controlDir.control.invalid"
|
||||
[formControl]="internalControl"
|
||||
(blur)="onBlurInternal()"
|
||||
>
|
||||
<option *ngFor="let o of selectOptions" [ngValue]="o.value" disabled="{{ o.disabled }}">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
@ -1,14 +0,0 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { SelectOptions } from "@bitwarden/angular/interfaces/selectOptions";
|
||||
|
||||
import { BaseCvaComponent } from "./base-cva.component";
|
||||
|
||||
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
|
||||
@Component({
|
||||
selector: "app-select",
|
||||
templateUrl: "select.component.html",
|
||||
})
|
||||
export class SelectComponent extends BaseCvaComponent {
|
||||
@Input() selectOptions: SelectOptions[];
|
||||
}
|
@ -35,6 +35,14 @@
|
||||
[helperText]="'allowSsoDesc' | i18n"
|
||||
></app-input-checkbox>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "ssoIdentifier" | i18n }}</bit-label>
|
||||
<input bitInput type="text" [formControl]="ssoIdentifier" />
|
||||
<bit-hint>{{ "ssoIdentifierHint" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{ "memberDecryptionOption" | i18n }}</label>
|
||||
<div class="form-check form-check-block">
|
||||
@ -80,66 +88,55 @@
|
||||
{{ "keyConnectorWarning" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="keyConnectorUrl">
|
||||
{{ "keyConnectorUrl" | i18n }}
|
||||
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
class="form-control"
|
||||
formControlName="keyConnectorUrl"
|
||||
id="keyConnectorUrl"
|
||||
aria-describedby="keyConnectorUrlDesc"
|
||||
(change)="haveTestedKeyConnector = false"
|
||||
appInputStripSpaces
|
||||
appA11yInvalid
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
(click)="validateKeyConnectorUrl()"
|
||||
[disabled]="!enableTestKeyConnector"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
*ngIf="keyConnectorUrl.pending"
|
||||
></i>
|
||||
<span *ngIf="!keyConnectorUrl.pending">
|
||||
{{ "keyConnectorTest" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="haveTestedKeyConnector" id="keyConnectorUrlDesc" aria-live="polite">
|
||||
<small
|
||||
class="error-inline"
|
||||
*ngIf="keyConnectorUrl.hasError('invalidUrl'); else keyConnectorSuccess"
|
||||
>
|
||||
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ "error" | i18n }}:</span>
|
||||
{{ "keyConnectorTestFail" | i18n }}
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "keyConnectorUrl" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
required
|
||||
formControlName="keyConnectorUrl"
|
||||
appInputStripSpaces
|
||||
(input)="haveTestedKeyConnector = false"
|
||||
/>
|
||||
<button
|
||||
bitSuffix
|
||||
bitButton
|
||||
[disabled]="!enableTestKeyConnector"
|
||||
type="button"
|
||||
(click)="validateKeyConnectorUrl()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
*ngIf="keyConnectorUrl.pending"
|
||||
></i>
|
||||
<span *ngIf="!keyConnectorUrl.pending">
|
||||
{{ "keyConnectorTest" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
<bit-hint
|
||||
aria-live="polite"
|
||||
*ngIf="haveTestedKeyConnector && !keyConnectorUrl.hasError('invalidUrl')"
|
||||
>
|
||||
<small class="text-success">
|
||||
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
|
||||
{{ "keyConnectorTestSuccess" | i18n }}
|
||||
</small>
|
||||
<ng-template #keyConnectorSuccess>
|
||||
<small class="text-success">
|
||||
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
|
||||
{{ "keyConnectorTestSuccess" | i18n }}
|
||||
</small>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
|
||||
<app-select
|
||||
controlId="type"
|
||||
[label]="'type' | i18n"
|
||||
[selectOptions]="ssoTypeOptions"
|
||||
formControlName="configType"
|
||||
>
|
||||
</app-select>
|
||||
<hr />
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "type" | i18n }}</bit-label>
|
||||
<select bitInput formControlName="configType">
|
||||
<option *ngFor="let o of ssoTypeOptions" [ngValue]="o.value" disabled="{{ o.disabled }}">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
|
||||
<!-- OIDC -->
|
||||
@ -150,52 +147,67 @@
|
||||
<div class="config-section">
|
||||
<h2 class="secondary-header">{{ "openIdConnectConfig" | i18n }}</h2>
|
||||
|
||||
<app-input-text-readonly
|
||||
[label]="'callbackPath' | i18n"
|
||||
[controlValue]="callbackPath"
|
||||
></app-input-text-readonly>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "callbackPath" | i18n }}</bit-label>
|
||||
<input bitInput disabled [value]="callbackPath" />
|
||||
<button
|
||||
bitButton
|
||||
bitSuffix
|
||||
type="button"
|
||||
[appCopyClick]="callbackPath"
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text-readonly
|
||||
[label]="'signedOutCallbackPath' | i18n"
|
||||
[controlValue]="signedOutCallbackPath"
|
||||
></app-input-text-readonly>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "signedOutCallbackPath" | i18n }}</bit-label>
|
||||
<input bitInput disabled [value]="signedOutCallbackPath" />
|
||||
<button
|
||||
bitButton
|
||||
bitSuffix
|
||||
type="button"
|
||||
[appCopyClick]="signedOutCallbackPath"
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text
|
||||
[label]="'authority' | i18n"
|
||||
controlId="authority"
|
||||
[stripSpaces]="true"
|
||||
formControlName="authority"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "authority" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="authority" appInputStripSpaces />
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text
|
||||
[label]="'clientId' | i18n"
|
||||
controlId="clientId"
|
||||
[stripSpaces]="true"
|
||||
formControlName="clientId"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "clientId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="clientId" appInputStripSpaces />
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text
|
||||
[label]="'clientSecret' | i18n"
|
||||
controlId="clientSecret"
|
||||
[stripSpaces]="true"
|
||||
formControlName="clientSecret"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "clientSecret" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="clientSecret" appInputStripSpaces />
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text
|
||||
[label]="'metadataAddress' | i18n"
|
||||
controlId="metadataAddress"
|
||||
[stripSpaces]="true"
|
||||
[helperText]="'openIdAuthorityRequired' | i18n"
|
||||
formControlName="metadataAddress"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "metadataAddress" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="metadataAddress" appInputStripSpaces />
|
||||
<bit-hint>{{ "openIdAuthorityRequired" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<app-select
|
||||
controlId="redirectBehavior"
|
||||
[label]="'oidcRedirectBehavior' | i18n"
|
||||
[selectOptions]="connectRedirectOptions"
|
||||
formControlName="redirectBehavior"
|
||||
>
|
||||
</app-select>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "oidcRedirectBehavior" | i18n }}</bit-label>
|
||||
<select bitInput formControlName="redirectBehavior">
|
||||
<option
|
||||
*ngFor="let o of connectRedirectOptions"
|
||||
[ngValue]="o.value"
|
||||
disabled="{{ o.disabled }}"
|
||||
>
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-checkbox
|
||||
controlId="getClaimsFromUserInfoEndpoint"
|
||||
@ -231,47 +243,41 @@
|
||||
</button>
|
||||
</div>
|
||||
<div id="customizations" [hidden]="!showOpenIdCustomizations">
|
||||
<app-input-text
|
||||
[label]="'additionalScopes' | i18n"
|
||||
controlId="additionalScopes"
|
||||
[helperText]="'separateMultipleWithComma' | i18n"
|
||||
formControlName="additionalScopes"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "additionalScopes" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="additionalScopes" />
|
||||
<bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text
|
||||
[label]="'additionalUserIdClaimTypes' | i18n"
|
||||
controlId="additionalUserIdClaimTypes"
|
||||
[helperText]="'separateMultipleWithComma' | i18n"
|
||||
formControlName="additionalUserIdClaimTypes"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "additionalUserIdClaimTypes" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="additionalUserIdClaimTypes" />
|
||||
<bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text
|
||||
[label]="'additionalEmailClaimTypes' | i18n"
|
||||
controlId="additionalEmailClaimTypes"
|
||||
[helperText]="'separateMultipleWithComma' | i18n"
|
||||
formControlName="additionalEmailClaimTypes"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "additionalEmailClaimTypes" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="additionalEmailClaimTypes" />
|
||||
<bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text
|
||||
[label]="'additionalNameClaimTypes' | i18n"
|
||||
controlId="additionalNameClaimTypes"
|
||||
[helperText]="'separateMultipleWithComma' | i18n"
|
||||
formControlName="additionalNameClaimTypes"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "additionalNameClaimTypes" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="additionalNameClaimTypes" />
|
||||
<bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text
|
||||
[label]="'acrValues' | i18n"
|
||||
controlId="acrValues"
|
||||
helperText="acr_values"
|
||||
formControlName="acrValues"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "acrValues" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="acrValues" />
|
||||
<bit-hint>acr_values</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text
|
||||
[label]="'expectedReturnAcrValue' | i18n"
|
||||
controlId="expectedReturnAcrValue"
|
||||
helperText="acr_validation"
|
||||
formControlName="expectedReturnAcrValue"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "expectedReturnAcrValue" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="expectedReturnAcrValue" />
|
||||
<bit-hint>acr_validaton</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -282,53 +288,108 @@
|
||||
<div class="config-section">
|
||||
<h2 class="secondary-header">{{ "samlSpConfig" | i18n }}</h2>
|
||||
|
||||
<app-input-text-readonly
|
||||
[label]="'spEntityId' | i18n"
|
||||
[controlValue]="spEntityId"
|
||||
></app-input-text-readonly>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "spEntityId" | i18n }}</bit-label>
|
||||
<input bitInput disabled [value]="spEntityId" />
|
||||
<button
|
||||
bitButton
|
||||
bitSuffix
|
||||
type="button"
|
||||
[appCopyClick]="spEntityId"
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text-readonly
|
||||
[label]="'spMetadataUrl' | i18n"
|
||||
[controlValue]="spMetadataUrl"
|
||||
[showLaunch]="true"
|
||||
></app-input-text-readonly>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "spMetadataUrl" | i18n }}</bit-label>
|
||||
<input bitInput disabled [value]="spMetadataUrl" />
|
||||
<button
|
||||
bitButton
|
||||
bitSuffix
|
||||
type="button"
|
||||
[appLaunchClick]="spMetadataUrl"
|
||||
[appA11yTitle]="'launch' | i18n"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
bitButton
|
||||
bitSuffix
|
||||
type="button"
|
||||
[appCopyClick]="spMetadataUrl"
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text-readonly
|
||||
[label]="'spAcsUrl' | i18n"
|
||||
[controlValue]="spAcsUrl"
|
||||
></app-input-text-readonly>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "spAcsUrl" | i18n }}</bit-label>
|
||||
<input bitInput disabled [value]="spAcsUrl" />
|
||||
<button
|
||||
bitButton
|
||||
bitSuffix
|
||||
type="button"
|
||||
[appCopyClick]="spAcsUrl"
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-form-field>
|
||||
|
||||
<app-select
|
||||
controlId="spNameIdFormat"
|
||||
[label]="'spNameIdFormat' | i18n"
|
||||
[selectOptions]="saml2NameIdFormatOptions"
|
||||
formControlName="spNameIdFormat"
|
||||
>
|
||||
</app-select>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "spNameIdFormat" | i18n }}</bit-label>
|
||||
<select bitInput formControlName="spNameIdFormat">
|
||||
<option
|
||||
*ngFor="let o of saml2NameIdFormatOptions"
|
||||
[ngValue]="o.value"
|
||||
disabled="{{ o.disabled }}"
|
||||
>
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
|
||||
<app-select
|
||||
controlId="spOutboundSigningAlgorithm"
|
||||
[label]="'spOutboundSigningAlgorithm' | i18n"
|
||||
[selectOptions]="samlSigningAlgorithmOptions"
|
||||
formControlName="spOutboundSigningAlgorithm"
|
||||
>
|
||||
</app-select>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "spOutboundSigningAlgorithm" | i18n }}</bit-label>
|
||||
<select bitInput formControlName="spOutboundSigningAlgorithm">
|
||||
<option
|
||||
*ngFor="let o of samlSigningAlgorithmOptions"
|
||||
[ngValue]="o.value"
|
||||
disabled="{{ o.disabled }}"
|
||||
>
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
|
||||
<app-select
|
||||
controlId="spSigningBehavior"
|
||||
[label]="'spSigningBehavior' | i18n"
|
||||
[selectOptions]="saml2SigningBehaviourOptions"
|
||||
formControlName="spSigningBehavior"
|
||||
>
|
||||
</app-select>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "spSigningBehavior" | i18n }}</bit-label>
|
||||
<select bitInput formControlName="spSigningBehavior">
|
||||
<option
|
||||
*ngFor="let o of saml2SigningBehaviourOptions"
|
||||
[ngValue]="o.value"
|
||||
disabled="{{ o.disabled }}"
|
||||
>
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
|
||||
<app-select
|
||||
controlId="spMinIncomingSigningAlgorithm"
|
||||
[label]="'spMinIncomingSigningAlgorithm' | i18n"
|
||||
[selectOptions]="samlSigningAlgorithmOptions"
|
||||
formControlName="spMinIncomingSigningAlgorithm"
|
||||
>
|
||||
</app-select>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "spMinIncomingSigningAlgorithm" | i18n }}</bit-label>
|
||||
<select bitInput formControlName="spMinIncomingSigningAlgorithm">
|
||||
<option
|
||||
*ngFor="let o of samlSigningAlgorithmOptions"
|
||||
[ngValue]="o.value"
|
||||
disabled="{{ o.disabled }}"
|
||||
>
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-checkbox
|
||||
controlId="spWantAssertionsSigned"
|
||||
@ -347,67 +408,62 @@
|
||||
<div class="config-section">
|
||||
<h2 class="secondary-header">{{ "samlIdpConfig" | i18n }}</h2>
|
||||
|
||||
<app-input-text
|
||||
[label]="'idpEntityId' | i18n"
|
||||
controlId="idpEntityId"
|
||||
formControlName="idpEntityId"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "idpEntityId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="idpEntityId" />
|
||||
</bit-form-field>
|
||||
|
||||
<app-select
|
||||
controlId="idpBindingType"
|
||||
[label]="'idpBindingType' | i18n"
|
||||
[selectOptions]="saml2BindingTypeOptions"
|
||||
formControlName="idpBindingType"
|
||||
>
|
||||
</app-select>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "idpBindingType" | i18n }}</bit-label>
|
||||
<select bitInput formControlName="idpBindingType">
|
||||
<option
|
||||
*ngFor="let o of saml2BindingTypeOptions"
|
||||
[ngValue]="o.value"
|
||||
disabled="{{ o.disabled }}"
|
||||
>
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text
|
||||
[label]="'idpSingleSignOnServiceUrl' | i18n"
|
||||
controlId="idpSingleSignOnServiceUrl"
|
||||
[helperText]="'idpSingleSignOnServiceUrlRequired' | i18n"
|
||||
[stripSpaces]="true"
|
||||
formControlName="idpSingleSignOnServiceUrl"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "idpSingleSignOnServiceUrl" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="idpSingleSignOnServiceUrl"
|
||||
appInputStripSpaces
|
||||
/>
|
||||
<bit-hint>{{ "idpSingleSignOnServiceUrlRequired" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<app-input-text
|
||||
[label]="'idpSingleLogoutServiceUrl' | i18n"
|
||||
controlId="idpSingleLogoutServiceUrl"
|
||||
[stripSpaces]="true"
|
||||
formControlName="idpSingleLogoutServiceUrl"
|
||||
></app-input-text>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "idpSingleLogoutServiceUrl" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="idpSingleLogoutServiceUrl"
|
||||
appInputStripSpaces
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="idpX509PublicCert">
|
||||
{{ "idpX509PublicCert" | i18n }}
|
||||
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
|
||||
</label>
|
||||
<textarea
|
||||
formControlName="idpX509PublicCert"
|
||||
class="form-control form-control-sm text-monospace"
|
||||
rows="6"
|
||||
id="idpX509PublicCert"
|
||||
appA11yInvalid
|
||||
aria-describedby="idpX509PublicCertDesc"
|
||||
></textarea>
|
||||
<small
|
||||
id="idpX509PublicCertDesc"
|
||||
class="error-inline"
|
||||
role="alert"
|
||||
*ngIf="samlForm.get('idpX509PublicCert').hasError('required')"
|
||||
>
|
||||
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ "error" | i18n }}:</span>
|
||||
{{ "fieldRequiredError" | i18n: ("idpX509PublicCert" | i18n) }}
|
||||
</small>
|
||||
</div>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "idpX509PublicCert" | i18n }}</bit-label>
|
||||
<textarea bitInput rows="6" formControlName="idpX509PublicCert"></textarea>
|
||||
</bit-form-field>
|
||||
|
||||
<app-select
|
||||
controlId="idpOutboundSigningAlgorithm"
|
||||
[label]="'idpOutboundSigningAlgorithm' | i18n"
|
||||
[selectOptions]="samlSigningAlgorithmOptions"
|
||||
formControlName="idpOutboundSigningAlgorithm"
|
||||
>
|
||||
</app-select>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "idpOutboundSigningAlgorithm" | i18n }}</bit-label>
|
||||
<select bitInput formControlName="idpOutboundSigningAlgorithm">
|
||||
<option
|
||||
*ngFor="let o of samlSigningAlgorithmOptions"
|
||||
[ngValue]="o.value"
|
||||
disabled="{{ o.disabled }}"
|
||||
>
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</bit-form-field>
|
||||
|
||||
<!--TODO: Uncomment once Unsolicited IdP Response is supported-->
|
||||
<!-- <app-input-checkbox
|
||||
@ -430,19 +486,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<div
|
||||
id="errorSummary"
|
||||
class="error-summary text-danger"
|
||||
*ngIf="this.getErrorCount(ssoConfigForm) as errorCount"
|
||||
>
|
||||
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ "error" | i18n }}:</span>
|
||||
{{
|
||||
(errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural") | i18n: errorCount
|
||||
}}
|
||||
</div>
|
||||
<bit-error-summary [formGroup]="ssoConfigForm"></bit-error-summary>
|
||||
</form>
|
||||
|
@ -1,9 +1,16 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from "@angular/forms";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
UntypedFormGroup,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { concatMap, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { SelectOptions } from "@bitwarden/angular/interfaces/selectOptions";
|
||||
import { dirtyRequired } from "@bitwarden/angular/validators/dirty.validator";
|
||||
import { ControlsOf } from "@bitwarden/angular/types/controls-of";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
|
||||
@ -29,8 +36,7 @@ const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha2
|
||||
selector: "app-org-manage-sso",
|
||||
templateUrl: "sso.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class SsoComponent implements OnInit {
|
||||
export class SsoComponent implements OnInit, OnDestroy {
|
||||
readonly ssoType = SsoType;
|
||||
|
||||
readonly ssoTypeOptions: SelectOptions[] = [
|
||||
@ -75,6 +81,8 @@ export class SsoComponent implements OnInit {
|
||||
{ name: "Form POST", value: OpenIdConnectRedirectBehavior.FormPost },
|
||||
];
|
||||
|
||||
private destory$ = new Subject<void>();
|
||||
|
||||
showOpenIdCustomizations = false;
|
||||
|
||||
loading = true;
|
||||
@ -89,62 +97,69 @@ export class SsoComponent implements OnInit {
|
||||
spMetadataUrl: string;
|
||||
spAcsUrl: string;
|
||||
|
||||
enabled = this.formBuilder.control(false);
|
||||
private enabled = this.formBuilder.control(false);
|
||||
|
||||
openIdForm = this.formBuilder.group(
|
||||
private ssoIdentifier = this.formBuilder.control("", {
|
||||
validators: [Validators.maxLength(50), Validators.required],
|
||||
});
|
||||
|
||||
private openIdForm = this.formBuilder.group<ControlsOf<SsoConfigView["openId"]>>(
|
||||
{
|
||||
authority: ["", dirtyRequired],
|
||||
clientId: ["", dirtyRequired],
|
||||
clientSecret: ["", dirtyRequired],
|
||||
metadataAddress: [],
|
||||
redirectBehavior: [OpenIdConnectRedirectBehavior.RedirectGet, dirtyRequired],
|
||||
getClaimsFromUserInfoEndpoint: [],
|
||||
additionalScopes: [],
|
||||
additionalUserIdClaimTypes: [],
|
||||
additionalEmailClaimTypes: [],
|
||||
additionalNameClaimTypes: [],
|
||||
acrValues: [],
|
||||
expectedReturnAcrValue: [],
|
||||
authority: new FormControl("", Validators.required),
|
||||
clientId: new FormControl("", Validators.required),
|
||||
clientSecret: new FormControl("", Validators.required),
|
||||
metadataAddress: new FormControl(),
|
||||
redirectBehavior: new FormControl(
|
||||
OpenIdConnectRedirectBehavior.RedirectGet,
|
||||
Validators.required
|
||||
),
|
||||
getClaimsFromUserInfoEndpoint: new FormControl(),
|
||||
additionalScopes: new FormControl(),
|
||||
additionalUserIdClaimTypes: new FormControl(),
|
||||
additionalEmailClaimTypes: new FormControl(),
|
||||
additionalNameClaimTypes: new FormControl(),
|
||||
acrValues: new FormControl(),
|
||||
expectedReturnAcrValue: new FormControl(),
|
||||
},
|
||||
{
|
||||
updateOn: "blur",
|
||||
}
|
||||
);
|
||||
|
||||
samlForm = this.formBuilder.group(
|
||||
private samlForm = this.formBuilder.group<ControlsOf<SsoConfigView["saml"]>>(
|
||||
{
|
||||
spNameIdFormat: [Saml2NameIdFormat.NotConfigured],
|
||||
spOutboundSigningAlgorithm: [defaultSigningAlgorithm],
|
||||
spSigningBehavior: [Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned],
|
||||
spMinIncomingSigningAlgorithm: [defaultSigningAlgorithm],
|
||||
spWantAssertionsSigned: [],
|
||||
spValidateCertificates: [],
|
||||
spNameIdFormat: new FormControl(Saml2NameIdFormat.NotConfigured),
|
||||
spOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm),
|
||||
spSigningBehavior: new FormControl(Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned),
|
||||
spMinIncomingSigningAlgorithm: new FormControl(defaultSigningAlgorithm),
|
||||
spWantAssertionsSigned: new FormControl(),
|
||||
spValidateCertificates: new FormControl(),
|
||||
|
||||
idpEntityId: ["", dirtyRequired],
|
||||
idpBindingType: [Saml2BindingType.HttpRedirect],
|
||||
idpSingleSignOnServiceUrl: [],
|
||||
idpSingleLogoutServiceUrl: [],
|
||||
idpX509PublicCert: ["", dirtyRequired],
|
||||
idpOutboundSigningAlgorithm: [defaultSigningAlgorithm],
|
||||
idpAllowUnsolicitedAuthnResponse: [],
|
||||
idpAllowOutboundLogoutRequests: [true],
|
||||
idpWantAuthnRequestsSigned: [],
|
||||
idpEntityId: new FormControl("", Validators.required),
|
||||
idpBindingType: new FormControl(Saml2BindingType.HttpRedirect),
|
||||
idpSingleSignOnServiceUrl: new FormControl(),
|
||||
idpSingleLogoutServiceUrl: new FormControl(),
|
||||
idpX509PublicCert: new FormControl("", Validators.required),
|
||||
idpOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm),
|
||||
idpAllowUnsolicitedAuthnResponse: new FormControl(),
|
||||
idpAllowOutboundLogoutRequests: new FormControl(true),
|
||||
idpWantAuthnRequestsSigned: new FormControl(),
|
||||
},
|
||||
{
|
||||
updateOn: "blur",
|
||||
}
|
||||
);
|
||||
|
||||
ssoConfigForm = this.formBuilder.group({
|
||||
configType: [SsoType.None],
|
||||
keyConnectorEnabled: [false],
|
||||
keyConnectorUrl: [""],
|
||||
private ssoConfigForm = this.formBuilder.group<ControlsOf<SsoConfigView>>({
|
||||
configType: new FormControl(SsoType.None),
|
||||
keyConnectorEnabled: new FormControl(false),
|
||||
keyConnectorUrl: new FormControl(""),
|
||||
openId: this.openIdForm,
|
||||
saml: this.samlForm,
|
||||
});
|
||||
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private formBuilder: FormBuilder,
|
||||
private route: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@ -154,32 +169,41 @@ export class SsoComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.ssoConfigForm.get("configType").valueChanges.subscribe((newType: SsoType) => {
|
||||
if (newType === SsoType.OpenIdConnect) {
|
||||
this.openIdForm.enable();
|
||||
this.samlForm.disable();
|
||||
} else if (newType === SsoType.Saml2) {
|
||||
this.openIdForm.disable();
|
||||
this.samlForm.enable();
|
||||
} else {
|
||||
this.openIdForm.disable();
|
||||
this.samlForm.disable();
|
||||
}
|
||||
});
|
||||
this.ssoConfigForm
|
||||
.get("configType")
|
||||
.valueChanges.pipe(takeUntil(this.destory$))
|
||||
.subscribe((newType: SsoType) => {
|
||||
if (newType === SsoType.OpenIdConnect) {
|
||||
this.openIdForm.enable();
|
||||
this.samlForm.disable();
|
||||
} else if (newType === SsoType.Saml2) {
|
||||
this.openIdForm.disable();
|
||||
this.samlForm.enable();
|
||||
} else {
|
||||
this.openIdForm.disable();
|
||||
this.samlForm.disable();
|
||||
}
|
||||
});
|
||||
|
||||
this.samlForm
|
||||
.get("spSigningBehavior")
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
.valueChanges.subscribe(() =>
|
||||
this.samlForm.get("idpX509PublicCert").updateValueAndValidity()
|
||||
);
|
||||
.valueChanges.pipe(takeUntil(this.destory$))
|
||||
.subscribe(() => this.samlForm.get("idpX509PublicCert").updateValueAndValidity());
|
||||
|
||||
// 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.route.params
|
||||
.pipe(
|
||||
concatMap(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
}),
|
||||
takeUntil(this.destory$)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destory$.next();
|
||||
this.destory$.complete();
|
||||
}
|
||||
|
||||
async load() {
|
||||
@ -199,7 +223,8 @@ export class SsoComponent implements OnInit {
|
||||
async submit() {
|
||||
this.validateForm(this.ssoConfigForm);
|
||||
|
||||
if (this.ssoConfigForm.get("keyConnectorEnabled").value) {
|
||||
if (this.ssoConfigForm.value.keyConnectorEnabled) {
|
||||
this.haveTestedKeyConnector = false;
|
||||
await this.validateKeyConnectorUrl();
|
||||
}
|
||||
|
||||
@ -210,7 +235,8 @@ export class SsoComponent implements OnInit {
|
||||
|
||||
const request = new OrganizationSsoRequest();
|
||||
request.enabled = this.enabled.value;
|
||||
request.data = SsoConfigApi.fromView(this.ssoConfigForm.value as SsoConfigView);
|
||||
request.identifier = this.ssoIdentifier.value;
|
||||
request.data = SsoConfigApi.fromView(this.ssoConfigForm.getRawValue());
|
||||
|
||||
this.formPromise = this.organizationApiService.updateSso(this.organizationId, request);
|
||||
|
||||
@ -237,7 +263,7 @@ export class SsoComponent implements OnInit {
|
||||
this.keyConnectorUrl.updateValueAndValidity();
|
||||
} catch {
|
||||
this.keyConnectorUrl.setErrors({
|
||||
invalidUrl: true,
|
||||
invalidUrl: { message: this.i18nService.t("keyConnectorTestFail") },
|
||||
});
|
||||
}
|
||||
|
||||
@ -294,6 +320,7 @@ export class SsoComponent implements OnInit {
|
||||
|
||||
private populateForm(ssoSettings: OrganizationSsoResponse) {
|
||||
this.enabled.setValue(ssoSettings.enabled);
|
||||
this.ssoIdentifier.setValue(ssoSettings.identifier);
|
||||
if (ssoSettings.data != null) {
|
||||
const ssoConfigView = new SsoConfigView(ssoSettings.data);
|
||||
this.ssoConfigForm.patchValue(ssoConfigView);
|
||||
|
@ -2,11 +2,11 @@ import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
|
||||
import { canAccessManageTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { canAccessSettingsTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
import { OrganizationPermissionsGuard } from "@bitwarden/web-vault/app/organizations/guards/org-permissions.guard";
|
||||
import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/organizations/layouts/organization-layout.component";
|
||||
import { ManageComponent } from "@bitwarden/web-vault/app/organizations/manage/manage.component";
|
||||
import { SettingsComponent } from "@bitwarden/web-vault/app/organizations/settings/settings.component";
|
||||
|
||||
import { ScimComponent } from "./manage/scim.component";
|
||||
import { SsoComponent } from "./manage/sso.component";
|
||||
@ -18,11 +18,11 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard, OrganizationPermissionsGuard],
|
||||
children: [
|
||||
{
|
||||
path: "manage",
|
||||
component: ManageComponent,
|
||||
path: "settings",
|
||||
component: SettingsComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
organizationPermissions: canAccessManageTab,
|
||||
organizationPermissions: canAccessSettingsTab,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user