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

[AC-1011] Admin Console / Billing code ownership (#4973)

* refactor: move SCIM component to admin-console, refs EC-1011

* refactor: move scimProviderType to admin-console, refs EC-1011

* refactor: move scim-config.api to admin-console, refs EC-1011

* refactor: create models folder and nest existing api contents, refs EC-1011

* refactor: move scim-config to admin-console models, refs EC-1011

* refactor: move billing.component to billing, refs EC-1011

* refactor: remove nested app folder from new billing structure, refs EC-1011

* refactor: move organizations/billing to billing, refs EC-1011

* refactor: move add-credit and adjust-payment to billing/settings, refs EC-1011

* refactor: billing history/sync to billing, refs EC-1011

* refactor: move org plans, payment/method to billing/settings, refs EC-1011

* fix: update legacy file paths for payment-method and tax-info, refs EC-1011

* fix: update imports for scim component, refs EC-1011

* refactor: move subscription and tax-info into billing, refs EC-1011

* refactor: move user-subscription to billing, refs EC-1011

* refactor: move images/cards to billing and update base path, refs EC-1011

* refactor: move payment-method, plan subscription, and plan to billing, refs EC-1011

* refactor: move transaction-type to billing, refs EC-1011

* refactor: move billing-sync-config to billing, refs EC-1011

* refactor: move billing-sync and bit-pay-invoice request to billing, refs EC-1011

* refactor: move org subscription and tax info update requests to billing, refs EC-1011

* fix: broken paths to billing, refs EC-1011

* refactor: move payment request to billing, refs EC-1011

* fix: update remaining imports for payment-request, refs EC-1011

* refactor: move tax-info-update to billing, refs EC-1011

* refactor: move billing-payment, billing-history, and billing responses to billing, refs EC-1011

* refactor: move organization-subscription-responset to billing, refs EC-1011

* refactor: move payment and plan responses to billing, refs EC-1011

* refactor: move subscription response to billing ,refs EC-1011

* refactor: move tax info and rate responses to billing, refs EC-1011

* fix: update remaining path to base response for tax-rate response, refs EC-1011

* refactor: (browser) move organization-service to admin-console, refs EC-1011

* refactor: (browser) move organizaiton-service to admin-console, refs EC-1011

* refactor: (cli) move share command to admin-console, refs EC-1011

* refactor: move organization-collect request model to admin-console, refs EC-1011

* refactor: (web) move organization, collection/user responses to admin-console, refs EC-1011

* refactor: (cli) move selection-read-only to admin-console, refs EC-1011

* refactor: (desktop) move organization-filter to admin-console, refs EC-1011

* refactor: (web) move organization-switcher to admin-console, refs EC-1011

* refactor: (web) move access-selector to admin-console, refs EC-1011

* refactor: (web) move create folder to admin-console, refs EC-1011

* refactor: (web) move org guards folder to admin-console, refs EC-1011

* refactor: (web) move org layout to admin-console, refs EC-1011

* refactor: move manage collections to admin console, refs EC-1011

* refactor: (web) move collection-dialog to admin-console, refs EC-1011

* refactor: (web) move entity users/events and events component to admin-console, refs EC-1011

* refactor: (web) move groups/group-add-edit to admin-console, refs EC-1011

* refactor: (web) move manage, org-manage module, and user-confirm to admin-console, refs EC-1011

* refactor: (web) move people to admin-console, refs EC-1011

* refactor: (web) move reset-password to admin-console, refs EC-1011

* refactor: (web) move organization-routing and module to admin-console, refs EC-1011

* refactor: move admin-console and billing within app scope, refs EC-1011

* fix: update leftover merge conflicts, refs EC-1011

* refactor: (web) member-dialog to admin-console, refs EC-1011

* refactor: (web) move policies to admin-console, refs EC-1011

* refactor: (web) move reporting to admin-console, refs EC-1011

* refactor: (web) move settings to admin-console, refs EC-1011

* refactor: (web) move sponsorships to admin-console, refs EC-1011

* refactor: (web) move tools to admin-console, refs EC-1011

* refactor: (web) move users to admin-console, refs EC-1011

* refactor: (web) move collections to admin-console, refs EC-1011

* refactor: (web) move create-organization to admin-console, refs EC-1011

* refactor: (web) move licensed components to admin-console, refs EC-1011

* refactor: (web) move bit organization modules to admin-console, refs EC-1011

* fix: update leftover import statements for organizations.module, refs EC-1011

* refactor: (web) move personal vault and max timeout to admin-console, refs EC-1011

* refactor: (web) move providers to admin-console, refs EC-1011

* refactor: (libs) move organization service to admin-console, refs EC-1011

* refactor: (libs) move profile org/provider responses and other misc org responses to admin-console, refs EC-1011

* refactor: (libs) move provider request and selectionion-read-only request to admin-console, refs EC-1011

* fix: update missed import path for provider-user-update request, refs EC-1011

* refactor: (libs) move abstractions to admin-console, refs EC-1011

* refactor: (libs) move org/provider enums to admin-console, refs EC-1011

* fix: update downstream import statements from libs changes, refs EC-1011

* refactor: (libs) move data files to admin-console, refs EC-1011

* refactor: (libs) move domain to admin-console, refs EC-1011

* refactor: (libs) move request objects to admin-console, refs EC-1011

* fix: update downstream import changes from libs, refs EC-1011

* refactor: move leftover provider files to admin-console, refs EC-1011

* refactor: (browser) move group policy environment to admin-console, refs EC-1011

* fix: (browser) update downstream import statements, refs EC-1011

* fix: (desktop) update downstream libs moves, refs EC-1011

* fix: (cli) update downstream import changes from libs, refs EC-1011

* refactor: move org-auth related files to admin-console, refs EC-1011

* refactor: (libs) move request objects to admin-console, refs EC-1011

* refactor: move persmissions to admin-console, refs EC-1011

* refactor: move sponsored families to admin-console and fix libs changes, refs EC-1011

* refactor: move collections to admin-console, refs EC-1011

* refactor: move spec file back to spec scope, refs EC-1011

* fix: update downstream imports due to libs changes, refs EC-1011

* fix: udpate downstream import changes due to libs, refs EC-1011

* fix: update downstream imports due to libs changes, refs EC-1011

* fix: update downstream imports from libs changes, refs EC-1011

* fix: update path malformation in jslib-services.module, refs EC-1011

* fix: lint errors from improper casing, refs AC-1011

* fix: update downstream filename changes, refs AC-1011

* fix: (cli) update downstream filename changes, refs AC-1011

* fix: (desktop) update downstream filename changes, refs AC-1011

* fix: (browser) update downstream filename changes, refs AC-1011

* fix: lint errors, refs AC-1011

* fix: prettier, refs AC-1011

* fix: lint fixes for import order, refs AC-1011

* fix: update import path for provider user type, refs AC-1011

* fix: update new codes import paths for admin console structure, refs AC-1011

* fix: lint/prettier, refs AC-1011

* fix: update layout stories path, refs AC-1011

* fix: update comoponents card icons base variable in styles, refs AC-1011

* fix: update provider service path in permissions guard spec, refs AC-1011

* fix: update provider permission guard path, refs AC-1011

* fix: remove unecessary TODO for shared index export statement, refs AC-1011

* refactor: move browser-organization service and cli organization-user response out of admin-console, refs AC-1011

* refactor: move web/browser/desktop collections component to vault domain, refs AC-1011

* refactor: move organization.module out of admin-console scope, refs AC-1011

* fix: prettier, refs AC-1011

* refactor: move organizations-api-key.request out of admin-console scope, refs AC-1011
This commit is contained in:
Vincent Salucci
2023-03-22 10:03:50 -05:00
committed by GitHub
parent a7fea2ff3a
commit 780a563ce0
557 changed files with 1260 additions and 1246 deletions

View File

@@ -0,0 +1,63 @@
import { Directive, Input, OnInit, Self } from "@angular/core";
import { ControlValueAccessor, UntypedFormControl, NgControl, Validators } from "@angular/forms";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Directive()
export abstract class BaseCvaComponent implements ControlValueAccessor, OnInit {
get describedById() {
return this.showDescribedBy ? this.controlId + "Desc" : null;
}
get showDescribedBy() {
return this.helperText != null || this.controlDir.control.hasError("required");
}
get isRequired() {
return this.controlDir.control.hasValidator(Validators.required);
}
@Input() label: string;
@Input() controlId: string;
@Input() helperText: string;
internalControl = new UntypedFormControl("");
protected onChange: any;
protected onTouched: any;
constructor(@Self() public controlDir: NgControl) {
this.controlDir.valueAccessor = this;
}
ngOnInit() {
this.internalControl.valueChanges.subscribe(this.onValueChangesInternal);
}
onBlurInternal() {
this.onTouched();
}
// CVA interfaces
writeValue(value: string) {
this.internalControl.setValue(value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.internalControl.disable();
} else {
this.internalControl.enable();
}
}
protected onValueChangesInternal: any = (value: string) => this.onChange(value);
// End CVA interfaces
}

View File

@@ -0,0 +1,16 @@
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[attr.id]="controlId"
[attr.aria-describedby]="describedById"
[formControl]="internalControl"
(blur)="onBlurInternal()"
/>
<label class="form-check-label" [attr.for]="controlId">{{ label }}</label>
</div>
<small *ngIf="showDescribedBy" [attr.id]="describedById" class="form-text text-muted">{{
helperText
}}</small>
</div>

View File

@@ -0,0 +1,10 @@
import { Component } 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-checkbox",
templateUrl: "input-checkbox.component.html",
})
export class InputCheckboxComponent extends BaseCvaComponent {}

View File

@@ -0,0 +1,99 @@
<div class="page-header">
<h1>{{ "scim" | i18n }}</h1>
</div>
<p>{{ "scimDescription" | i18n }}</p>
<div *ngIf="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
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
[formGroup]="formData"
*ngIf="!loading"
>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
aria-describedby="scimEnabledCheckboxDescHelpText"
/>
<label class="form-check-label" for="enabled">{{ "scimEnabledCheckboxDesc" | i18n }}</label>
<div id="scimEnabledCheckboxDescHelpText">
<small class="form-text text-muted">{{ "scimEnabledCheckboxDescHelpText" | i18n }}</small>
</div>
</div>
</div>
<bit-form-field *ngIf="showScimSettings">
<bit-label>{{ "scimUrl" | i18n }}</bit-label>
<input bitInput type="text" formControlName="endpointUrl" />
<button
type="button"
bitSuffix
bitIconButton="bwi-clone"
(click)="copyScimUrl()"
[appA11yTitle]="'copyScimUrl' | i18n"
></button>
</bit-form-field>
<bit-form-field *ngIf="showScimSettings">
<bit-label>{{ "scimApiKey" | i18n }}</bit-label>
<input
bitInput
[type]="showScimKey ? 'text' : 'password'"
formControlName="clientSecret"
id="clientSecret"
/>
<ng-container>
<button
type="button"
bitSuffix
[disabled]="$any(rotateButton).loading"
[bitIconButton]="showScimKey ? 'bwi-eye-slash' : 'bwi-eye'"
(click)="toggleScimKey()"
[appA11yTitle]="'toggleVisibility' | i18n"
></button>
</ng-container>
<ng-container #rotateButton [appApiAction]="rotatePromise">
<!-- TODO: Convert to async actions -->
<button
[loading]="$any(rotateButton).loading"
type="button"
bitSuffix
bitIconButton="bwi-generate"
(click)="rotateScimKey()"
[appA11yTitle]="'rotateScimKey' | i18n"
></button>
</ng-container>
<button
type="button"
bitSuffix
bitIconButton="bwi-clone"
(click)="copyScimKey()"
[appA11yTitle]="'copyScimKey' | i18n"
></button>
<bit-hint>{{ "scimApiKeyHelperText" | i18n }}</bit-hint>
</bit-form-field>
<button
type="submit"
buttonType="primary"
bitButton
[loading]="form.loading"
[disabled]="form.loading"
>
{{ "save" | i18n }}
</button>
</form>

View File

@@ -0,0 +1,172 @@
import { Component, OnInit } from "@angular/core";
import { UntypedFormBuilder, FormControl } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums/organization-api-key-type";
import { OrganizationConnectionType } from "@bitwarden/common/admin-console/enums/organization-connection-type";
import { ScimConfigApi } from "@bitwarden/common/admin-console/models/api/scim-config.api";
import { OrganizationConnectionRequest } from "@bitwarden/common/admin-console/models/request/organization-connection.request";
import { ScimConfigRequest } from "@bitwarden/common/admin-console/models/request/scim-config.request";
import { OrganizationConnectionResponse } from "@bitwarden/common/admin-console/models/response/organization-connection.response";
import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response";
import { OrganizationApiKeyRequest } from "@bitwarden/common/models/request/organization-api-key.request";
@Component({
selector: "app-org-manage-scim",
templateUrl: "scim.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ScimComponent implements OnInit {
loading = true;
organizationId: string;
existingConnectionId: string;
formPromise: Promise<OrganizationConnectionResponse<ScimConfigApi>>;
rotatePromise: Promise<ApiKeyResponse>;
enabled = new FormControl(false);
showScimSettings = false;
showScimKey = false;
formData = this.formBuilder.group({
endpointUrl: new FormControl({ value: "", disabled: true }),
clientSecret: new FormControl({ value: "", disabled: true }),
});
constructor(
private formBuilder: UntypedFormBuilder,
private route: ActivatedRoute,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private environmentService: EnvironmentService,
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();
});
}
async load() {
const connection = await this.apiService.getOrganizationConnection(
this.organizationId,
OrganizationConnectionType.Scim,
ScimConfigApi
);
await this.setConnectionFormValues(connection);
}
async loadApiKey() {
const apiKeyRequest = new OrganizationApiKeyRequest();
apiKeyRequest.type = OrganizationApiKeyType.Scim;
apiKeyRequest.masterPasswordHash = "N/A";
const apiKeyResponse = await this.organizationApiService.getOrCreateApiKey(
this.organizationId,
apiKeyRequest
);
this.formData.setValue({
endpointUrl: this.getScimEndpointUrl(),
clientSecret: apiKeyResponse.apiKey,
});
}
async copyScimUrl() {
this.platformUtilsService.copyToClipboard(this.getScimEndpointUrl());
}
async rotateScimKey() {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("rotateScimKeyWarning"),
this.i18nService.t("rotateScimKey"),
this.i18nService.t("rotateKey"),
this.i18nService.t("cancel"),
"warning"
);
if (!confirmed) {
return false;
}
const request = new OrganizationApiKeyRequest();
request.type = OrganizationApiKeyType.Scim;
request.masterPasswordHash = "N/A";
this.rotatePromise = this.organizationApiService.rotateApiKey(this.organizationId, request);
try {
const response = await this.rotatePromise;
this.formData.setValue({
endpointUrl: this.getScimEndpointUrl(),
clientSecret: response.apiKey,
});
this.platformUtilsService.showToast("success", null, this.i18nService.t("scimApiKeyRotated"));
} catch {
// Logged by appApiAction, do nothing
}
this.rotatePromise = null;
}
async copyScimKey() {
this.platformUtilsService.copyToClipboard(this.formData.get("clientSecret").value);
}
async submit() {
try {
const request = new OrganizationConnectionRequest(
this.organizationId,
OrganizationConnectionType.Scim,
true,
new ScimConfigRequest(this.enabled.value)
);
if (this.existingConnectionId == null) {
this.formPromise = this.apiService.createOrganizationConnection(request, ScimConfigApi);
} else {
this.formPromise = this.apiService.updateOrganizationConnection(
request,
ScimConfigApi,
this.existingConnectionId
);
}
const response = (await this.formPromise) as OrganizationConnectionResponse<ScimConfigApi>;
await this.setConnectionFormValues(response);
this.platformUtilsService.showToast("success", null, this.i18nService.t("scimSettingsSaved"));
} catch (e) {
// Logged by appApiAction, do nothing
}
this.formPromise = null;
}
getScimEndpointUrl() {
return this.environmentService.getScimUrl() + "/" + this.organizationId;
}
toggleScimKey() {
this.showScimKey = !this.showScimKey;
document.getElementById("clientSecret").focus();
}
private async setConnectionFormValues(connection: OrganizationConnectionResponse<ScimConfigApi>) {
this.existingConnectionId = connection?.id;
if (connection !== null && connection.config?.enabled) {
this.showScimSettings = true;
this.enabled.setValue(true);
this.formData.setValue({
endpointUrl: this.getScimEndpointUrl(),
clientSecret: "",
});
await this.loadApiKey();
} else {
this.showScimSettings = false;
this.enabled.setValue(false);
}
this.loading = false;
}
}

View File

@@ -0,0 +1,64 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/auth/guards/auth.guard";
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component";
import { SettingsComponent } from "@bitwarden/web-vault/app/admin-console/organizations/settings/settings.component";
import { SsoComponent } from "../../auth/sso/sso.component";
import { DomainVerificationComponent } from "../../organizations/manage/domain-verification/domain-verification.component";
import { ScimComponent } from "./manage/scim.component";
const routes: Routes = [
{
path: "organizations/:organizationId",
component: OrganizationLayoutComponent,
canActivate: [AuthGuard, OrganizationPermissionsGuard],
children: [
{
path: "settings",
component: SettingsComponent,
canActivate: [OrganizationPermissionsGuard],
data: {
organizationPermissions: canAccessSettingsTab,
},
children: [
{
path: "domain-verification",
component: DomainVerificationComponent,
canActivate: [OrganizationPermissionsGuard],
data: {
organizationPermissions: (org: Organization) => org.canManageDomainVerification,
},
},
{
path: "sso",
component: SsoComponent,
canActivate: [OrganizationPermissionsGuard],
data: {
organizationPermissions: (org: Organization) => org.canManageSso,
},
},
{
path: "scim",
component: ScimComponent,
canActivate: [OrganizationPermissionsGuard],
data: {
organizationPermissions: (org: Organization) => org.canManageScim,
},
},
],
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrganizationsRoutingModule {}

View File

@@ -0,0 +1,23 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module";
import { SsoComponent } from "../../auth/sso/sso.component";
import { DomainAddEditDialogComponent } from "../../organizations/manage/domain-verification/domain-add-edit-dialog/domain-add-edit-dialog.component";
import { DomainVerificationComponent } from "../../organizations/manage/domain-verification/domain-verification.component";
import { InputCheckboxComponent } from "./components/input-checkbox.component";
import { ScimComponent } from "./manage/scim.component";
import { OrganizationsRoutingModule } from "./organizations-routing.module";
@NgModule({
imports: [SharedModule, OrganizationsRoutingModule],
declarations: [
InputCheckboxComponent,
SsoComponent,
ScimComponent,
DomainVerificationComponent,
DomainAddEditDialogComponent,
],
})
export class OrganizationsModule {}

View File

@@ -0,0 +1,19 @@
<app-callout type="warning">
{{ "experimentalFeature" | i18n }}
<a href="https://bitwarden.com/help/auto-fill-browser/" target="_blank" rel="noopener">{{
"learnMoreAboutAutofill" | i18n
}}</a>
</app-callout>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { Component } from "@angular/core";
import { PolicyType } from "@bitwarden/common/admin-console/enums/policy-type";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import {
BasePolicy,
BasePolicyComponent,
} from "@bitwarden/web-vault/app/admin-console/organizations/policies/base-policy.component";
export class ActivateAutofillPolicy extends BasePolicy {
name = "activateAutofill";
description = "activateAutofillDesc";
type = PolicyType.ActivateAutofill;
component = ActivateAutofillPolicyComponent;
display(organization: Organization) {
return organization.useActivateAutofillPolicy;
}
}
@Component({
selector: "policy-activate-autofill",
templateUrl: "activate-autofill.component.html",
})
export class ActivateAutofillPolicyComponent extends BasePolicyComponent {}

View File

@@ -0,0 +1,12 @@
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { Component } from "@angular/core";
import { PolicyType } from "@bitwarden/common/admin-console/enums/policy-type";
import {
BasePolicy,
BasePolicyComponent,
} from "@bitwarden/web-vault/app/admin-console/organizations/policies/base-policy.component";
export class DisablePersonalVaultExportPolicy extends BasePolicy {
name = "disablePersonalVaultExport";
description = "disablePersonalVaultExportDesc";
type = PolicyType.DisablePersonalVaultExport;
component = DisablePersonalVaultExportPolicyComponent;
}
@Component({
selector: "policy-disable-personal-vault-export",
templateUrl: "disable-personal-vault-export.component.html",
})
export class DisablePersonalVaultExportPolicyComponent extends BasePolicyComponent {}

View File

@@ -0,0 +1,47 @@
<app-callout type="tip" title="{{ 'prerequisite' | i18n }}">
{{ "requireSsoPolicyReq" | i18n }}
</app-callout>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>
<div [formGroup]="data">
<div class="form-group">
<label for="hours">{{ "maximumVaultTimeoutLabel" | i18n }}</label>
<div class="row">
<div class="col-6">
<input
id="hours"
class="form-control"
type="number"
min="0"
name="hours"
formControlName="hours"
/>
<small>{{ "hours" | i18n }}</small>
</div>
<div class="col-6">
<input
id="minutes"
class="form-control"
type="number"
min="0"
max="59"
name="minutes"
formControlName="minutes"
/>
<small>{{ "minutes" | i18n }}</small>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,69 @@
import { Component } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PolicyType } from "@bitwarden/common/admin-console/enums/policy-type";
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
import {
BasePolicy,
BasePolicyComponent,
} from "@bitwarden/web-vault/app/admin-console/organizations/policies/base-policy.component";
export class MaximumVaultTimeoutPolicy extends BasePolicy {
name = "maximumVaultTimeout";
description = "maximumVaultTimeoutDesc";
type = PolicyType.MaximumVaultTimeout;
component = MaximumVaultTimeoutPolicyComponent;
}
@Component({
selector: "policy-maximum-timeout",
templateUrl: "maximum-vault-timeout.component.html",
})
export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
data = this.formBuilder.group({
hours: [null],
minutes: [null],
});
constructor(private formBuilder: UntypedFormBuilder, private i18nService: I18nService) {
super();
}
loadData() {
const minutes = this.policyResponse.data?.minutes;
if (minutes == null) {
return;
}
this.data.patchValue({
hours: Math.floor(minutes / 60),
minutes: minutes % 60,
});
}
buildRequestData() {
if (this.data.value.hours == null && this.data.value.minutes == null) {
return null;
}
return {
minutes: this.data.value.hours * 60 + this.data.value.minutes,
};
}
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
if (this.enabled.value && !singleOrgEnabled) {
throw new Error(this.i18nService.t("requireSsoPolicyReqError"));
}
const data = this.buildRequestData();
if (data?.minutes == null || data?.minutes <= 0) {
throw new Error(this.i18nService.t("invalidMaximumVaultTimeout"));
}
return super.buildRequest(policiesEnabledMap);
}
}

View File

@@ -0,0 +1,48 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="addTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="addTitle">
{{ "addExistingOrganization" | i18n }}
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="card-body text-center" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
<ng-container *ngIf="!loading">
<table class="table table-hover table-list">
<tr *ngFor="let o of organizations">
<td width="30">
<bit-avatar [text]="o.name" [id]="o.id" size="small"></bit-avatar>
</td>
<td>
{{ o.name }}
</td>
<td>
<button
class="btn btn-outline-secondary pull-right"
(click)="add(o)"
[disabled]="formPromise"
>
Add
</button>
</td>
</tr>
</table>
</ng-container>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,84 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Provider } from "@bitwarden/common/models/domain/provider";
import { WebProviderService } from "../services/web-provider.service";
@Component({
selector: "provider-add-organization",
templateUrl: "add-organization.component.html",
})
export class AddOrganizationComponent implements OnInit {
@Input() providerId: string;
@Input() organizations: Organization[];
@Output() onAddedOrganization = new EventEmitter();
provider: Provider;
formPromise: Promise<any>;
loading = true;
constructor(
private providerService: ProviderService,
private webProviderService: WebProviderService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private validationService: ValidationService
) {}
async ngOnInit() {
await this.load();
}
async load() {
if (this.providerId == null) {
return;
}
this.provider = await this.providerService.get(this.providerId);
this.loading = false;
}
async add(organization: Organization) {
if (this.formPromise) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("addOrganizationConfirmation", organization.name, this.provider.name),
organization.name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
try {
this.formPromise = this.webProviderService.addOrganizationToProvider(
this.providerId,
organization.id
);
await this.formPromise;
} catch (e) {
this.validationService.showError(e);
return;
} finally {
this.formPromise = null;
}
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("organizationJoinedProvider")
);
this.onAddedOrganization.emit();
}
}

View File

@@ -0,0 +1,102 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<div class="page-header d-flex">
<h1>{{ "clients" | i18n }}</h1>
<div class="ml-auto d-flex">
<div>
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
<input
type="search"
class="form-control form-control-sm"
id="search"
placeholder="{{ 'search' | i18n }}"
[(ngModel)]="searchText"
/>
</div>
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newClientOrganization" | i18n }}
</a>
<button
class="btn btn-sm btn-outline-primary ml-3"
(click)="addExistingOrganization()"
*ngIf="manageOrganizations && showAddExisting"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "addExistingOrganization" | i18n }}
</button>
</div>
</div>
<ng-container *ngIf="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="!loading && (clients | search : searchText : 'organizationName' : 'id') as searchedClients"
>
<p *ngIf="!searchedClients.length">{{ "noClientsInList" | i18n }}</p>
<ng-container *ngIf="searchedClients.length">
<table
class="table table-hover table-list"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<thead>
<tr>
<th colspan="2">{{ "name" | i18n }}</th>
<th>{{ "numberOfUsers" | i18n }}</th>
<th>{{ "billingPlan" | i18n }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let o of searchedClients">
<td width="30">
<bit-avatar [text]="o.organizationName" [id]="o.id" size="small"></bit-avatar>
</td>
<td>
<a [routerLink]="['/organizations', o.organizationId]">{{ o.organizationName }}</a>
</td>
<td>
<span>{{ o.userCount }}</span>
<span *ngIf="o.seats != null"> / {{ o.seats }}</span>
</td>
<td>
<span>{{ o.plan }}</span>
</td>
<td class="table-list-options" *ngIf="manageOrganizations">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
</ng-container>
<ng-template #add></ng-template>

View File

@@ -0,0 +1,185 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderUserType } from "@bitwarden/common/admin-console/enums/provider-user-type";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { PlanType } from "@bitwarden/common/billing/enums/plan-type";
import { WebProviderService } from "../services/web-provider.service";
import { AddOrganizationComponent } from "./add-organization.component";
const DisallowedPlanTypes = [
PlanType.Free,
PlanType.FamiliesAnnually2019,
PlanType.FamiliesAnnually,
];
@Component({
templateUrl: "clients.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ClientsComponent implements OnInit {
@ViewChild("add", { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
providerId: string;
searchText: string;
addableOrganizations: Organization[];
loading = true;
manageOrganizations = false;
showAddExisting = false;
clients: ProviderOrganizationOrganizationDetailsResponse[];
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
protected didScroll = false;
protected pageSize = 100;
protected actionPromise: Promise<unknown>;
private pagedClientsCount = 0;
constructor(
private route: ActivatedRoute,
private providerService: ProviderService,
private apiService: ApiService,
private searchService: SearchService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private validationService: ValidationService,
private webProviderService: WebProviderService,
private logService: LogService,
private modalService: ModalService,
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction
) {}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
await this.load();
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
});
});
}
async load() {
const response = await this.apiService.getProviderClients(this.providerId);
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
this.manageOrganizations =
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin;
const candidateOrgs = (await this.organizationService.getAll()).filter(
(o) => o.isOwner && o.providerId == null
);
const allowedOrgsIds = await Promise.all(
candidateOrgs.map((o) => this.organizationApiService.get(o.id))
).then((orgs) =>
orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id)
);
this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id));
this.showAddExisting = this.addableOrganizations.length !== 0;
this.loading = false;
}
isPaging() {
const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
}
return !searching && this.clients && this.clients.length > this.pageSize;
}
isSearching() {
return this.searchService.isSearchable(this.searchText);
}
async resetPaging() {
this.pagedClients = [];
this.loadMore();
}
loadMore() {
if (!this.clients || this.clients.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedClients.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
pagedSize = this.pagedClientsCount;
}
if (this.clients.length > pagedLength) {
this.pagedClients = this.pagedClients.concat(
this.clients.slice(pagedLength, pagedLength + pagedSize)
);
}
this.pagedClientsCount = this.pagedClients.length;
this.didScroll = this.pagedClients.length > this.pageSize;
}
async addExistingOrganization() {
const [modal] = await this.modalService.openViewRef(
AddOrganizationComponent,
this.addModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.organizations = this.addableOrganizations;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
comp.onAddedOrganization.subscribe(async () => {
try {
await this.load();
modal.close();
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
});
}
);
}
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("detachOrganizationConfirmation"),
organization.organizationName,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
this.actionPromise = this.webProviderService.detachOrganizastion(
this.providerId,
organization.id
);
try {
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("detachedOrganization", organization.organizationName)
);
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
}

View File

@@ -0,0 +1,5 @@
<div class="page-header">
<h1>{{ "newClientOrganization" | i18n }}</h1>
</div>
<p>{{ "newClientOrganizationDesc" | i18n }}</p>
<app-organization-plans [providerId]="providerId"></app-organization-plans>

View File

@@ -0,0 +1,25 @@
import { Component, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing/settings/organization-plans.component";
@Component({
selector: "app-create-organization",
templateUrl: "create-organization.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class CreateOrganizationComponent implements OnInit {
@ViewChild(OrganizationPlansComponent, { static: true })
orgPlansComponent: OrganizationPlansComponent;
providerId: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
});
}
}

View File

@@ -0,0 +1,124 @@
import { ActivatedRouteSnapshot, Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderUserType } from "@bitwarden/common/admin-console/enums/provider-user-type";
import { Provider } from "@bitwarden/common/models/domain/provider";
import { ProviderPermissionsGuard } from "./provider-permissions.guard";
const providerFactory = (props: Partial<Provider> = {}) =>
Object.assign(
new Provider(),
{
id: "myProviderId",
enabled: true,
type: ProviderUserType.ServiceUser,
},
props
);
describe("Provider Permissions Guard", () => {
let router: MockProxy<Router>;
let providerService: MockProxy<ProviderService>;
let route: MockProxy<ActivatedRouteSnapshot>;
let providerPermissionsGuard: ProviderPermissionsGuard;
beforeEach(() => {
router = mock<Router>();
providerService = mock<ProviderService>();
route = mock<ActivatedRouteSnapshot>({
params: {
providerId: providerFactory().id,
},
data: {
providerPermissions: null,
},
});
providerPermissionsGuard = new ProviderPermissionsGuard(
providerService,
router,
mock<PlatformUtilsService>(),
mock<I18nService>()
);
});
it("blocks navigation if provider does not exist", async () => {
providerService.get.mockResolvedValue(null);
const actual = await providerPermissionsGuard.canActivate(route);
expect(actual).not.toBe(true);
});
it("permits navigation if no permissions are specified", async () => {
const provider = providerFactory();
providerService.get.calledWith(provider.id).mockResolvedValue(provider);
const actual = await providerPermissionsGuard.canActivate(route);
expect(actual).toBe(true);
});
it("permits navigation if the user has permissions", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockImplementation((provider) => true);
route.data = {
providerPermissions: permissionsCallback,
};
const provider = providerFactory();
providerService.get.calledWith(provider.id).mockResolvedValue(provider);
const actual = await providerPermissionsGuard.canActivate(route);
expect(permissionsCallback).toHaveBeenCalled();
expect(actual).toBe(true);
});
it("blocks navigation if the user does not have permissions", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockImplementation((org) => false);
route.data = {
providerPermissions: permissionsCallback,
};
const provider = providerFactory();
providerService.get.calledWith(provider.id).mockResolvedValue(provider);
const actual = await providerPermissionsGuard.canActivate(route);
expect(permissionsCallback).toHaveBeenCalled();
expect(actual).not.toBe(true);
});
describe("given a disabled organization", () => {
it("blocks navigation if user is not an admin", async () => {
const org = providerFactory({
type: ProviderUserType.ServiceUser,
enabled: false,
});
providerService.get.calledWith(org.id).mockResolvedValue(org);
const actual = await providerPermissionsGuard.canActivate(route);
expect(actual).not.toBe(true);
});
it("permits navigation if user is an admin", async () => {
const org = providerFactory({
type: ProviderUserType.ProviderAdmin,
enabled: false,
});
providerService.get.calledWith(org.id).mockResolvedValue(org);
const actual = await providerPermissionsGuard.canActivate(route);
expect(actual).toBe(true);
});
});
});

View File

@@ -0,0 +1,39 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { Provider } from "@bitwarden/common/models/domain/provider";
@Injectable()
export class ProviderPermissionsGuard implements CanActivate {
constructor(
private providerService: ProviderService,
private router: Router,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService
) {}
async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.providerService.get(route.params.providerId);
if (provider == null) {
return this.router.createUrlTree(["/"]);
}
if (!provider.isProviderAdmin && !provider.enabled) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("providerIsDisabled"));
return this.router.createUrlTree(["/"]);
}
const permissionsCallback: (provider: Provider) => boolean = route.data?.providerPermissions;
const hasSpecifiedPermissions = permissionsCallback == null || permissionsCallback(provider);
if (!hasSpecifiedPermissions) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("accessDenied"));
return this.router.createUrlTree(["/providers", provider.id]);
}
return true;
}
}

View File

@@ -0,0 +1,46 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div>
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</div>
</div>
<div class="container" *ngIf="!loading && !authed">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "joinProvider" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<p class="text-center">
{{ providerName }}
<strong class="d-block mt-2">{{ email }}</strong>
</p>
<p>{{ "joinProviderDesc" | i18n }}</p>
<hr />
<div class="d-flex">
<a
routerLink="/login"
[queryParams]="{ email: email }"
class="btn btn-primary btn-block"
>
{{ "logIn" | i18n }}
</a>
<a
routerLink="/register"
[queryParams]="{ email: email }"
class="btn btn-primary btn-block ml-2 mt-0"
>
{{ "createAccount" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,54 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Params, 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 { StateService } from "@bitwarden/common/abstractions/state.service";
import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request";
import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component";
@Component({
selector: "app-accept-provider",
templateUrl: "accept-provider.component.html",
})
export class AcceptProviderComponent extends BaseAcceptComponent {
providerName: string;
failedMessage = "providerInviteAcceptFailed";
requiredParameters = ["providerId", "providerUserId", "token"];
constructor(
router: Router,
i18nService: I18nService,
route: ActivatedRoute,
stateService: StateService,
private apiService: ApiService,
platformUtilService: PlatformUtilsService
) {
super(router, platformUtilService, i18nService, route, stateService);
}
async authedHandler(qParams: Params) {
const request = new ProviderUserAcceptRequest();
request.token = qParams.token;
await this.apiService.postProviderUserAccept(
qParams.providerId,
qParams.providerUserId,
request
);
this.platformUtilService.showToast(
"success",
this.i18nService.t("inviteAccepted"),
this.i18nService.t("providerInviteAcceptedDesc"),
{ timeout: 10000 }
);
this.router.navigate(["/vault"]);
}
async unauthedHandler(qParams: Params) {
this.providerName = qParams.providerName;
}
}

View File

@@ -0,0 +1,33 @@
import { Component, Input } from "@angular/core";
import { ProviderUserStatusType } from "@bitwarden/common/admin-console/enums/provider-user-status-type";
import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk-confirm.request";
import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request";
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-confirm.component";
import { BulkUserDetails } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-status.component";
@Component({
templateUrl:
"../../../../../../../../apps/web/src/app/organizations/members/components/bulk/bulk-confirm.component.html",
})
export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
@Input() providerId: string;
protected isAccepted(user: BulkUserDetails) {
return user.status === ProviderUserStatusType.Accepted;
}
protected async getPublicKeys() {
const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id));
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
}
protected getCryptoKey() {
return this.cryptoService.getProviderKey(this.providerId);
}
protected async postConfirmRequest(userIdsWithKeys: any[]) {
const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys);
return await this.apiService.postProviderUserBulkConfirm(this.providerId, request);
}
}

View File

@@ -0,0 +1,21 @@
import { Component, Input } from "@angular/core";
import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request";
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-remove.component";
@Component({
templateUrl:
"../../../../../../../../apps/web/src/app/organizations/members/components/bulk/bulk-remove.component.html",
})
export class BulkRemoveComponent extends OrganizationBulkRemoveComponent {
@Input() providerId: string;
async deleteUsers() {
const request = new ProviderUserBulkRequest(this.users.map((user) => user.id));
return await this.apiService.deleteManyProviderUsers(this.providerId, request);
}
protected get removeUsersWarning() {
return this.i18nService.t("removeUsersWarning");
}
}

View File

@@ -0,0 +1,107 @@
<div class="page-header d-flex">
<h1>{{ "eventLogs" | i18n }}</h1>
<div class="ml-auto d-flex">
<div class="form-inline">
<label class="sr-only" for="start">{{ "startDate" | i18n }}</label>
<input
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>
<input
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">
<button
type="button"
class="btn btn-sm btn-outline-primary ml-3"
(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 }}
</button>
</form>
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
<button
type="button"
class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
[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>
</button>
</form>
</div>
</div>
<ng-container *ngIf="!loaded">
<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="loaded">
<p *ngIf="!events || !events.length">{{ "noEventsInList" | i18n }}</p>
<table class="table table-hover" *ngIf="events && events.length">
<thead>
<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>
</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>
</td>
<td>
<span title="{{ e.userEmail }}">{{ e.userName }}</span>
</td>
<td [innerHTML]="e.message"></td>
</tr>
</tbody>
</table>
<button
#moreBtn
[appApiAction]="morePromise"
type="button"
class="btn btn-block btn-link btn-submit"
(click)="loadEvents(false)"
[disabled]="loaded && $any(moreBtn).loading"
*ngIf="continuationToken"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "loadMore" | i18n }}</span>
</button>
</ng-container>

View File

@@ -0,0 +1,95 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ExportService } from "@bitwarden/common/abstractions/export.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { EventResponse } from "@bitwarden/common/models/response/event.response";
import { BaseEventsComponent } from "@bitwarden/web-vault/app/common/base.events.component";
import { EventService } from "@bitwarden/web-vault/app/core";
@Component({
selector: "provider-events",
templateUrl: "events.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class EventsComponent extends BaseEventsComponent implements OnInit {
exportFileName = "provider-events";
providerId: string;
private providerUsersUserIdMap = new Map<string, any>();
private providerUsersIdMap = new Map<string, any>();
constructor(
private apiService: ApiService,
private route: ActivatedRoute,
eventService: EventService,
i18nService: I18nService,
private providerService: ProviderService,
exportService: ExportService,
platformUtilsService: PlatformUtilsService,
private router: Router,
logService: LogService,
private userNamePipe: UserNamePipe,
fileDownloadService: FileDownloadService
) {
super(
eventService,
i18nService,
exportService,
platformUtilsService,
logService,
fileDownloadService
);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
const provider = await this.providerService.get(this.providerId);
if (provider == null || !provider.useEvents) {
this.router.navigate(["/providers", this.providerId]);
return;
}
await this.load();
});
}
async load() {
const response = await this.apiService.getProviderUsers(this.providerId);
response.data.forEach((u) => {
const name = this.userNamePipe.transform(u);
this.providerUsersIdMap.set(u.id, { name: name, email: u.email });
this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email });
});
await this.loadEvents(true);
this.loaded = true;
}
protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
return this.apiService.getEventsProvider(
this.providerId,
startDate,
endDate,
continuationToken
);
}
protected getUserName(r: EventResponse, userId: string) {
if (r.installationId != null) {
return `Installation: ${r.installationId}`;
}
if (userId != null && this.providerUsersUserIdMap.has(userId)) {
return this.providerUsersUserIdMap.get(userId);
}
return null;
}
}

View File

@@ -0,0 +1,30 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card" *ngIf="provider">
<div class="card-header">{{ "manage" | i18n }}</div>
<div class="list-group list-group-flush">
<a
routerLink="people"
class="list-group-item"
routerLinkActive="active"
*ngIf="provider.canManageUsers"
>
{{ "people" | i18n }}
</a>
<a
routerLink="events"
class="list-group-item"
routerLinkActive="active"
*ngIf="provider.canAccessEventLogs && accessEvents"
>
{{ "eventLogs" | i18n }}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { Provider } from "@bitwarden/common/models/domain/provider";
@Component({
selector: "provider-manage",
templateUrl: "manage.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ManageComponent implements OnInit {
provider: Provider;
accessEvents = false;
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
this.provider = await this.providerService.get(params.providerId);
this.accessEvents = this.provider.useEvents;
});
}
}

View File

@@ -0,0 +1,217 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable @angular-eslint/template/button-has-type -->
<div class="page-header d-flex">
<h1>{{ "people" | i18n }}</h1>
<div class="ml-auto d-flex">
<div class="btn-group btn-group-sm" role="group">
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: status == null }"
(click)="filter(null)"
>
{{ "all" | i18n }}
<span bitBadge badgeType="info" *ngIf="allCount">{{ allCount }}</span>
</button>
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: status == userStatusType.Invited }"
(click)="filter(userStatusType.Invited)"
>
{{ "invited" | i18n }}
<span bitBadge badgeType="info" *ngIf="invitedCount">{{ invitedCount }}</span>
</button>
<button
type="button"
class="btn btn-outline-secondary"
[ngClass]="{ active: status == userStatusType.Accepted }"
(click)="filter(userStatusType.Accepted)"
>
{{ "accepted" | i18n }}
<span bitBadge badgeType="warning" *ngIf="acceptedCount">{{ acceptedCount }}</span>
</button>
</div>
<div class="ml-3">
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
<input
type="search"
class="form-control form-control-sm"
id="search"
placeholder="{{ 'search' | i18n }}"
[(ngModel)]="searchText"
/>
</div>
<div class="dropdown ml-3" appListDropdown>
<button
class="btn btn-sm btn-outline-secondary dropdown-toggle"
type="button"
id="bulkActionsButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "reinviteSelected" | i18n }}
</button>
<button
class="dropdown-item text-success"
appStopClick
(click)="bulkConfirm()"
*ngIf="showBulkConfirmUsers"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirmSelected" | i18n }}
</button>
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i>
{{ "selectAll" | i18n }}
</button>
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
<i class="bwi bwi-fw bwi-minus-square" aria-hidden="true"></i>
{{ "unselectAll" | i18n }}
</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "inviteUser" | i18n }}
</button>
</div>
</div>
<ng-container *ngIf="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="
!loading &&
(isPaging()
? pagedUsers
: (users | search : searchText : 'name' : 'email' : 'id')) as searchedUsers
"
>
<p *ngIf="!searchedUsers.length">{{ "noUsersInList" | i18n }}</p>
<ng-container *ngIf="searchedUsers.length">
<app-callout
type="info"
title="{{ 'confirmUsers' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="showConfirmUsers"
>
{{ "providerUsersNeedConfirmed" | i18n }}
</app-callout>
<table
class="table table-hover table-list"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<tbody>
<tr *ngFor="let u of searchedUsers">
<td (click)="checkUser(u)" class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="$any(u).checked" appStopProp />
</td>
<td width="30">
<bit-avatar [text]="u | userName" [id]="u.userId" size="small"></bit-avatar>
</td>
<td>
<a href="#" appStopClick (click)="edit(u)">{{ u.email }}</a>
<span bitBadge badgeType="secondary" *ngIf="u.status === userStatusType.Invited">{{
"invited" | i18n
}}</span>
<span bitBadge badgeType="warning" *ngIf="u.status === userStatusType.Accepted">{{
"accepted" | i18n
}}</span>
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
</td>
<td>
<ng-container *ngIf="$any(u).twoFactorEnabled">
<i
class="bwi bwi-lock"
title="{{ 'userUsingTwoStep' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "userUsingTwoStep" | i18n }}</span>
</ng-container>
</td>
<td>
<span *ngIf="u.type === userType.ProviderAdmin">{{ "providerAdmin" | i18n }}</span>
<span *ngIf="u.type === userType.ServiceUser">{{ "serviceUser" | i18n }}</span>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a
class="dropdown-item"
href="#"
appStopClick
(click)="reinvite(u)"
*ngIf="u.status === userStatusType.Invited"
>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "resendInvitation" | i18n }}
</a>
<a
class="dropdown-item text-success"
href="#"
appStopClick
(click)="confirm(u)"
*ngIf="u.status === userStatusType.Accepted"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirm" | i18n }}
</a>
<a
class="dropdown-item"
href="#"
appStopClick
(click)="events(u)"
*ngIf="accessEvents && u.status === userStatusType.Confirmed"
>
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
{{ "eventLogs" | i18n }}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
</ng-container>
<ng-template #addEdit></ng-template>
<ng-template #eventsTemplate></ng-template>
<ng-template #confirmTemplate></ng-template>
<ng-template #bulkStatusTemplate></ng-template>
<ng-template #bulkConfirmTemplate></ng-template>
<ng-template #bulkRemoveTemplate></ng-template>

View File

@@ -0,0 +1,296 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderUserStatusType } from "@bitwarden/common/admin-console/enums/provider-user-status-type";
import { ProviderUserType } from "@bitwarden/common/admin-console/enums/provider-user-type";
import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request";
import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-confirm.request";
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { EntityEventsComponent } from "@bitwarden/web-vault/app/admin-console/organizations/manage/entity-events.component";
import { BasePeopleComponent } from "@bitwarden/web-vault/app/common/base.people.component";
import { BulkStatusComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-status.component";
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
import { UserAddEditComponent } from "./user-add-edit.component";
@Component({
selector: "provider-people",
templateUrl: "people.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class PeopleComponent
extends BasePeopleComponent<ProviderUserUserDetailsResponse>
implements OnInit
{
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
groupsModalRef: ViewContainerRef;
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true })
eventsModalRef: ViewContainerRef;
@ViewChild("bulkStatusTemplate", { read: ViewContainerRef, static: true })
bulkStatusModalRef: ViewContainerRef;
@ViewChild("bulkConfirmTemplate", { read: ViewContainerRef, static: true })
bulkConfirmModalRef: ViewContainerRef;
@ViewChild("bulkRemoveTemplate", { read: ViewContainerRef, static: true })
bulkRemoveModalRef: ViewContainerRef;
userType = ProviderUserType;
userStatusType = ProviderUserStatusType;
providerId: string;
accessEvents = false;
constructor(
apiService: ApiService,
private route: ActivatedRoute,
i18nService: I18nService,
modalService: ModalService,
platformUtilsService: PlatformUtilsService,
cryptoService: CryptoService,
private router: Router,
searchService: SearchService,
validationService: ValidationService,
logService: LogService,
searchPipe: SearchPipe,
userNamePipe: UserNamePipe,
stateService: StateService,
private providerService: ProviderService
) {
super(
apiService,
searchService,
i18nService,
platformUtilsService,
cryptoService,
validationService,
modalService,
logService,
searchPipe,
userNamePipe,
stateService
);
}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
const provider = await this.providerService.get(this.providerId);
if (!provider.canManageUsers) {
this.router.navigate(["../"], { relativeTo: this.route });
return;
}
this.accessEvents = provider.useEvents;
await this.load();
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
if (qParams.viewEvents != null) {
const user = this.users.filter((u) => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
this.events(user[0]);
}
}
});
});
}
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
return this.apiService.getProviderUsers(this.providerId);
}
deleteUser(id: string): Promise<any> {
return this.apiService.deleteProviderUser(this.providerId, id);
}
revokeUser(id: string): Promise<any> {
// Not implemented.
return null;
}
restoreUser(id: string): Promise<any> {
// Not implemented.
return null;
}
reinviteUser(id: string): Promise<any> {
return this.apiService.postProviderUserReinvite(this.providerId, id);
}
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer);
const request = new ProviderUserConfirmRequest();
request.key = key.encryptedString;
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
}
async edit(user: ProviderUserUserDetailsResponse) {
const [modal] = await this.modalService.openViewRef(
UserAddEditComponent,
this.addEditModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId;
comp.providerUserId = user != null ? user.id : null;
comp.onSavedUser.subscribe(() => {
modal.close();
this.load();
});
comp.onDeletedUser.subscribe(() => {
modal.close();
this.removeUser(user);
});
}
);
}
async events(user: ProviderUserUserDetailsResponse) {
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId;
comp.entityId = user.id;
comp.showUser = false;
comp.entity = "user";
});
}
async bulkRemove() {
if (this.actionPromise != null) {
return;
}
const [modal] = await this.modalService.openViewRef(
BulkRemoveComponent,
this.bulkRemoveModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
}
);
await modal.onClosedPromise();
await this.load();
}
async bulkReinvite() {
if (this.actionPromise != null) {
return;
}
const users = this.getCheckedUsers();
const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited);
if (filteredUsers.length <= 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("noSelectedUsersApplicable")
);
return;
}
try {
const request = new ProviderUserBulkRequest(filteredUsers.map((user) => user.id));
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
this.showBulkStatus(
users,
filteredUsers,
response,
this.i18nService.t("bulkReinviteMessage")
);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async bulkConfirm() {
if (this.actionPromise != null) {
return;
}
const [modal] = await this.modalService.openViewRef(
BulkConfirmComponent,
this.bulkConfirmModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
}
);
await modal.onClosedPromise();
await this.load();
}
private async showBulkStatus(
users: ProviderUserUserDetailsResponse[],
filteredUsers: ProviderUserUserDetailsResponse[],
request: Promise<ListResponse<ProviderUserBulkResponse>>,
successfullMessage: string
) {
const [modal, childComponent] = await this.modalService.openViewRef(
BulkStatusComponent,
this.bulkStatusModalRef,
(comp) => {
comp.loading = true;
}
);
// Workaround to handle closing the modal shortly after it has been opened
let close = false;
modal.onShown.subscribe(() => {
if (close) {
modal.close();
}
});
try {
const response = await request;
if (modal) {
const keyedErrors: any = response.data
.filter((r) => r.error !== "")
.reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
childComponent.users = users.map((user) => {
let message = keyedErrors[user.id] ?? successfullMessage;
// eslint-disable-next-line
if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
message = this.i18nService.t("bulkFilteredMessage");
}
return {
user: user,
error: keyedErrors.hasOwnProperty(user.id), // eslint-disable-line
message: message,
};
});
childComponent.loading = false;
}
} catch {
close = true;
modal.close();
}
}
}

View File

@@ -0,0 +1,124 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header">
<h2 class="modal-title" id="userAddEditTitle">
{{ title }}
<small class="text-muted" *ngIf="name">{{ name }}</small>
</h2>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" *ngIf="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>
<div class="modal-body" *ngIf="!loading">
<ng-container *ngIf="!editMode">
<p>{{ "providerInviteUserDesc" | i18n }}</p>
<div class="form-group mb-4">
<label for="emails">{{ "email" | i18n }}</label>
<input
id="emails"
class="form-control"
type="text"
name="Emails"
[(ngModel)]="emails"
required
appAutoFocus
/>
<small class="text-muted">{{ "inviteMultipleEmailDesc" | i18n : "20" }}</small>
</div>
</ng-container>
<h3>
{{ "userType" | i18n }}
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/provider-users/"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</h3>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
name="userType"
id="userTypeServiceUser"
[value]="userType.ServiceUser"
[(ngModel)]="type"
/>
<label class="form-check-label" for="userTypeServiceUser">
{{ "serviceUser" | i18n }}
<small>{{ "serviceUserDesc" | i18n }}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
name="userType"
id="userTypeProviderAdmin"
[value]="userType.ProviderAdmin"
[(ngModel)]="type"
/>
<label class="form-check-label" for="userTypeProviderAdmin">
{{ "providerAdmin" | i18n }}
<small>{{ "providerAdminDesc" | i18n }}</small>
</label>
</div>
</div>
<div class="modal-footer">
<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>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
<div class="ml-auto">
<button
#deleteBtn
type="button"
(click)="delete()"
class="btn btn-outline-danger"
appA11yTitle="{{ 'delete' | i18n }}"
*ngIf="editMode"
[disabled]="$any(deleteBtn).loading"
[appApiAction]="deletePromise"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="$any(deleteBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
</div>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,118 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ProviderUserType } from "@bitwarden/common/admin-console/enums/provider-user-type";
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
import { ProviderUserInviteRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-invite.request";
import { ProviderUserUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-update.request";
@Component({
selector: "provider-user-add-edit",
templateUrl: "user-add-edit.component.html",
})
export class UserAddEditComponent implements OnInit {
@Input() name: string;
@Input() providerUserId: string;
@Input() providerId: string;
@Output() onSavedUser = new EventEmitter();
@Output() onDeletedUser = new EventEmitter();
loading = true;
editMode = false;
title: string;
emails: string;
type: ProviderUserType = ProviderUserType.ServiceUser;
permissions = new PermissionsApi();
showCustom = false;
access: "all" | "selected" = "selected";
formPromise: Promise<any>;
deletePromise: Promise<any>;
userType = ProviderUserType;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() {
this.editMode = this.loading = this.providerUserId != null;
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t("editUser");
try {
const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId);
this.type = user.type;
} catch (e) {
this.logService.error(e);
}
} else {
this.title = this.i18nService.t("inviteUser");
}
this.loading = false;
}
async submit() {
try {
if (this.editMode) {
const request = new ProviderUserUpdateRequest();
request.type = this.type;
this.formPromise = this.apiService.putProviderUser(
this.providerId,
this.providerUserId,
request
);
} else {
const request = new ProviderUserInviteRequest();
request.emails = this.emails.trim().split(/\s*,\s*/);
request.type = this.type;
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
}
await this.formPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name)
);
this.onSavedUser.emit();
} catch (e) {
this.logService.error(e);
}
}
async delete() {
if (!this.editMode) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("removeUserConfirmation"),
this.name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
try {
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
await this.deletePromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("removedUserId", this.name)
);
this.onDeletedUser.emit();
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -0,0 +1,44 @@
<app-navbar></app-navbar>
<div class="org-nav" *ngIf="provider">
<div class="container d-flex">
<div class="d-flex flex-column">
<div class="my-auto d-flex align-items-center pl-1">
<bit-avatar [text]="provider.name" [id]="provider.id"></bit-avatar>
<div class="org-name ml-3">
<span>{{ provider.name }}</span>
<small class="text-muted">{{ "provider" | i18n }}</small>
</div>
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
<div class="card-body py-2">
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
{{ "providerIsDisabled" | i18n }}
</div>
</div>
</div>
<ul class="nav nav-tabs" *ngIf="showMenuBar">
<li class="nav-item">
<a class="nav-link" routerLink="clients" routerLinkActive="active">
<i class="bwi bwi-bank" aria-hidden="true"></i>
{{ "clients" | i18n }}
</a>
</li>
<li class="nav-item" *ngIf="showManageTab">
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
<i class="bwi bwi-sliders" aria-hidden="true"></i>
{{ "manage" | i18n }}
</a>
</li>
<li class="nav-item" *ngIf="showSettingsTab">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="bwi bwi-cogs" aria-hidden="true"></i>
{{ "settings" | i18n }}
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container page-content">
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>

View File

@@ -0,0 +1,51 @@
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { Provider } from "@bitwarden/common/models/domain/provider";
@Component({
selector: "providers-layout",
templateUrl: "providers-layout.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ProvidersLayoutComponent {
provider: Provider;
private providerId: string;
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
ngOnInit() {
document.body.classList.remove("layout_frontend");
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.params.subscribe(async (params) => {
this.providerId = params.providerId;
await this.load();
});
}
async load() {
this.provider = await this.providerService.get(this.providerId);
}
get showMenuBar() {
return this.showManageTab || this.showSettingsTab;
}
get showManageTab() {
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
}
get showSettingsTab() {
return this.provider.isProviderAdmin;
}
get manageRoute(): string {
switch (true) {
case this.provider.canManageUsers:
return "manage/people";
case this.provider.canAccessEventLogs:
return "manage/events";
}
}
}

View File

@@ -0,0 +1,119 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/auth/guards/auth.guard";
import { Provider } from "@bitwarden/common/models/domain/provider";
import { ProvidersComponent } from "@bitwarden/web-vault/app/admin-console/providers/providers.component";
import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component";
import { ClientsComponent } from "./clients/clients.component";
import { CreateOrganizationComponent } from "./clients/create-organization.component";
import { ProviderPermissionsGuard } from "./guards/provider-permissions.guard";
import { AcceptProviderComponent } from "./manage/accept-provider.component";
import { EventsComponent } from "./manage/events.component";
import { ManageComponent } from "./manage/manage.component";
import { PeopleComponent } from "./manage/people.component";
import { ProvidersLayoutComponent } from "./providers-layout.component";
import { AccountComponent } from "./settings/account.component";
import { SettingsComponent } from "./settings/settings.component";
import { SetupProviderComponent } from "./setup/setup-provider.component";
import { SetupComponent } from "./setup/setup.component";
const routes: Routes = [
{
path: "",
canActivate: [AuthGuard],
component: ProvidersComponent,
},
{
path: "",
component: FrontendLayoutComponent,
children: [
{
path: "setup-provider",
component: SetupProviderComponent,
data: { titleId: "setupProvider" },
},
{
path: "accept-provider",
component: AcceptProviderComponent,
data: { titleId: "acceptProvider" },
},
],
},
{
path: "",
canActivate: [AuthGuard],
children: [
{
path: "setup",
component: SetupComponent,
},
{
path: ":providerId",
component: ProvidersLayoutComponent,
canActivate: [ProviderPermissionsGuard],
children: [
{ path: "", pathMatch: "full", redirectTo: "clients" },
{ path: "clients/create", component: CreateOrganizationComponent },
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
{
path: "manage",
component: ManageComponent,
children: [
{
path: "",
pathMatch: "full",
redirectTo: "people",
},
{
path: "people",
component: PeopleComponent,
canActivate: [ProviderPermissionsGuard],
data: {
titleId: "people",
providerPermissions: (provider: Provider) => provider.canManageUsers,
},
},
{
path: "events",
component: EventsComponent,
canActivate: [ProviderPermissionsGuard],
data: {
titleId: "eventLogs",
providerPermissions: (provider: Provider) => provider.canAccessEventLogs,
},
},
],
},
{
path: "settings",
component: SettingsComponent,
children: [
{
path: "",
pathMatch: "full",
redirectTo: "account",
},
{
path: "account",
component: AccountComponent,
canActivate: [ProviderPermissionsGuard],
data: {
titleId: "myProvider",
providerPermissions: (provider: Provider) => provider.isProviderAdmin,
},
},
],
},
],
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProvidersRoutingModule {}

View File

@@ -0,0 +1,56 @@
import { CommonModule } from "@angular/common";
import { ComponentFactoryResolver, NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
import { AddOrganizationComponent } from "./clients/add-organization.component";
import { ClientsComponent } from "./clients/clients.component";
import { CreateOrganizationComponent } from "./clients/create-organization.component";
import { ProviderPermissionsGuard } from "./guards/provider-permissions.guard";
import { AcceptProviderComponent } from "./manage/accept-provider.component";
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component";
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component";
import { EventsComponent } from "./manage/events.component";
import { ManageComponent } from "./manage/manage.component";
import { PeopleComponent } from "./manage/people.component";
import { UserAddEditComponent } from "./manage/user-add-edit.component";
import { ProvidersLayoutComponent } from "./providers-layout.component";
import { ProvidersRoutingModule } from "./providers-routing.module";
import { WebProviderService } from "./services/web-provider.service";
import { AccountComponent } from "./settings/account.component";
import { SettingsComponent } from "./settings/settings.component";
import { SetupProviderComponent } from "./setup/setup-provider.component";
import { SetupComponent } from "./setup/setup.component";
@NgModule({
imports: [CommonModule, FormsModule, OssModule, JslibModule, ProvidersRoutingModule],
declarations: [
AcceptProviderComponent,
AccountComponent,
AddOrganizationComponent,
BulkConfirmComponent,
BulkRemoveComponent,
ClientsComponent,
CreateOrganizationComponent,
EventsComponent,
ManageComponent,
PeopleComponent,
ProvidersLayoutComponent,
SettingsComponent,
SetupComponent,
SetupProviderComponent,
UserAddEditComponent,
],
providers: [WebProviderService, ProviderPermissionsGuard],
})
export class ProvidersModule {
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
modalService.registerComponentFactoryResolver(
AddOrganizationComponent,
componentFactoryResolver
);
}
}

View File

@@ -0,0 +1,35 @@
import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { ProviderAddOrganizationRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-add-organization.request";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@Injectable()
export class WebProviderService {
constructor(
private cryptoService: CryptoService,
private syncService: SyncService,
private apiService: ApiService
) {}
async addOrganizationToProvider(providerId: string, organizationId: string) {
const orgKey = await this.cryptoService.getOrgKey(organizationId);
const providerKey = await this.cryptoService.getProviderKey(providerId);
const encryptedOrgKey = await this.cryptoService.encrypt(orgKey.key, providerKey);
const request = new ProviderAddOrganizationRequest();
request.organizationId = organizationId;
request.key = encryptedOrgKey.encryptedString;
const response = await this.apiService.postProviderAddOrganization(providerId, request);
await this.syncService.fullSync(true);
return response;
}
async detachOrganizastion(providerId: string, organizationId: string): Promise<any> {
await this.apiService.deleteProviderOrganization(providerId, organizationId);
await this.syncService.fullSync(true);
}
}

View File

@@ -0,0 +1,52 @@
<div class="page-header">
<h1>{{ "myProvider" | i18n }}</h1>
</div>
<div *ngIf="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="provider && !loading"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="row">
<div class="col-6">
<div class="form-group">
<label for="name">{{ "providerName" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="provider.name"
[disabled]="selfHosted"
/>
</div>
<div class="form-group">
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
<input
id="billingEmail"
class="form-control"
type="text"
name="BillingEmail"
[(ngModel)]="provider.billingEmail"
[disabled]="selfHosted"
/>
</div>
</div>
<div class="col-6">
<bit-avatar [text]="provider.name" [id]="provider.id" size="large"></bit-avatar>
</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>
</form>

View File

@@ -0,0 +1,65 @@
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request";
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@Component({
selector: "provider-account",
templateUrl: "account.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class AccountComponent {
selfHosted = false;
loading = true;
provider: ProviderResponse;
formPromise: Promise<any>;
taxFormPromise: Promise<any>;
private providerId: string;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private route: ActivatedRoute,
private syncService: SyncService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
try {
this.provider = await this.apiService.getProvider(this.providerId);
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
});
this.loading = false;
}
async submit() {
try {
const request = new ProviderUpdateRequest();
request.name = this.provider.name;
request.businessName = this.provider.businessName;
request.billingEmail = this.provider.billingEmail;
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
return this.syncService.fullSync(true);
});
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerUpdated"));
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
}
}

View File

@@ -0,0 +1,17 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card">
<div class="card-header">{{ "settings" | i18n }}</div>
<div class="list-group list-group-flush">
<a routerLink="account" class="list-group-item" routerLinkActive="active">
{{ "myProvider" | i18n }}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
@Component({
selector: "provider-settings",
templateUrl: "settings.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SettingsComponent {
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
await this.providerService.get(params.providerId);
});
}
}

View File

@@ -0,0 +1,35 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div>
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</div>
</div>
<div class="container" *ngIf="!loading && !authed">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "setupProvider" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<p>{{ "setupProviderLoginDesc" | i18n }}</p>
<hr />
<div class="d-flex">
<a
routerLink="/login"
[queryParams]="{ email: email }"
class="btn btn-primary btn-block"
>
{{ "logIn" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { Component } from "@angular/core";
import { Params } from "@angular/router";
import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component";
@Component({
selector: "app-setup-provider",
templateUrl: "setup-provider.component.html",
})
export class SetupProviderComponent extends BaseAcceptComponent {
failedShortMessage = "inviteAcceptFailedShort";
failedMessage = "inviteAcceptFailed";
requiredParameters = ["providerId", "email", "token"];
async authedHandler(qParams: Params) {
this.router.navigate(["/providers/setup"], { queryParams: qParams });
}
async unauthedHandler(qParams: Params) {
// Empty
}
}

View File

@@ -0,0 +1,36 @@
<app-navbar></app-navbar>
<div class="container page-content">
<div class="page-header">
<h1>{{ "setupProvider" | i18n }}</h1>
</div>
<p>{{ "setupProviderDesc" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
<h2 class="mt-5">{{ "generalInformation" | i18n }}</h2>
<div class="row">
<div class="form-group col-6">
<label for="name">{{ "providerName" | i18n }}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required />
</div>
<div class="form-group col-6">
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
<input
id="billingEmail"
class="form-control"
type="text"
name="BillingEmail"
[(ngModel)]="billingEmail"
required
/>
</div>
</div>
<div class="mt-4">
<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>{{ "submit" | i18n }}</span>
</button>
</div>
</form>
</div>
<app-footer></app-footer>

View File

@@ -0,0 +1,99 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@Component({
selector: "provider-setup",
templateUrl: "setup.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SetupComponent implements OnInit {
loading = true;
authed = false;
email: string;
formPromise: Promise<any>;
providerId: string;
token: string;
name: string;
billingEmail: string;
constructor(
private router: Router,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private route: ActivatedRoute,
private cryptoService: CryptoService,
private apiService: ApiService,
private syncService: SyncService,
private validationService: ValidationService
) {}
ngOnInit() {
document.body.classList.remove("layout_frontend");
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
if (error) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("emergencyInviteAcceptFailed"),
{ timeout: 10000 }
);
this.router.navigate(["/"]);
return;
}
this.providerId = qParams.providerId;
this.token = qParams.token;
// Check if provider exists, redirect if it does
try {
const provider = await this.apiService.getProvider(this.providerId);
if (provider.name != null) {
this.router.navigate(["/providers", provider.id], { replaceUrl: true });
}
} catch (e) {
this.validationService.showError(e);
this.router.navigate(["/"]);
}
});
}
async submit() {
this.formPromise = this.doSubmit();
await this.formPromise;
this.formPromise = null;
}
async doSubmit() {
try {
const shareKey = await this.cryptoService.makeShareKey();
const key = shareKey[0].encryptedString;
const request = new ProviderSetupRequest();
request.name = this.name;
request.billingEmail = this.billingEmail;
request.token = this.token;
request.key = key;
const provider = await this.apiService.postProviderSetup(this.providerId, request);
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
await this.syncService.fullSync(true);
this.router.navigate(["/providers", provider.id]);
} catch (e) {
this.validationService.showError(e);
}
}
}