1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-08 03:23:50 +00:00

[PM-11901] Refactoring self-hosting license file uploader (#11083)

This commit is contained in:
Jonas Hendrickx
2024-09-26 11:23:23 +02:00
committed by GitHub
parent 8fb97e7b60
commit d2e5af7fb5
11 changed files with 374 additions and 53 deletions

View File

@@ -13,6 +13,8 @@ import { OffboardingSurveyComponent } from "./offboarding-survey.component";
import { PaymentV2Component } from "./payment/payment-v2.component";
import { PaymentComponent } from "./payment/payment.component";
import { PaymentMethodComponent } from "./payment-method.component";
import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component";
import { OrganizationSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/organization-self-hosting-license-uploader.component";
import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component";
import { TaxInfoComponent } from "./tax-info.component";
import { UpdateLicenseDialogComponent } from "./update-license-dialog.component";
@@ -40,6 +42,8 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
OffboardingSurveyComponent,
AdjustPaymentDialogV2Component,
AdjustStorageDialogV2Component,
IndividualSelfHostingLicenseUploaderComponent,
OrganizationSelfHostingLicenseUploaderComponent,
],
exports: [
SharedModule,
@@ -53,6 +57,8 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
OffboardingSurveyComponent,
VerifyBankAccountComponent,
PaymentV2Component,
IndividualSelfHostingLicenseUploaderComponent,
OrganizationSelfHostingLicenseUploaderComponent,
],
})
export class BillingSharedModule {}

View File

@@ -0,0 +1,81 @@
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
import { LicenseUploaderFormModel } from "./license-uploader-form-model";
/**
* Shared implementation for processing license file uploads.
* @remarks Requires self-hosting.
*/
export abstract class AbstractSelfHostingLicenseUploaderComponent {
protected form: FormGroup;
protected constructor(
protected readonly formBuilder: FormBuilder,
protected readonly i18nService: I18nService,
protected readonly platformUtilsService: PlatformUtilsService,
protected readonly toastService: ToastService,
protected readonly tokenService: TokenService,
) {
const isSelfHosted = this.platformUtilsService.isSelfHost();
if (!isSelfHosted) {
throw new Error("This component should only be used in self-hosted environments");
}
this.form = this.formBuilder.group({
file: [null, [Validators.required]],
});
this.submit = this.submit.bind(this);
}
/**
* Gets the submitted license upload form model.
* @protected
*/
protected get formValue(): LicenseUploaderFormModel {
return this.form.value as LicenseUploaderFormModel;
}
/**
* Triggered when a different license file is selected.
* @param event
*/
onLicenseFileSelectedChanged(event: Event): void {
const element = event.target as HTMLInputElement;
this.form.value.file = element.files.length > 0 ? element.files[0] : null;
}
/**
* Submits the license upload form.
* @protected
*/
protected async submit(): Promise<void> {
this.form.markAllAsTouched();
if (this.form.invalid) {
return this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("selectFile"),
});
}
const emailVerified = await this.tokenService.getEmailVerified();
if (!emailVerified) {
return this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("verifyEmailFirst"),
});
}
}
abstract get description(): string;
abstract get hintFileName(): string;
}

View File

@@ -0,0 +1,60 @@
import { Component, EventEmitter, Output } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { ToastService } from "@bitwarden/components";
import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-hosting-license-uploader/abstract-self-hosting-license-uploader.component";
/**
* Processes license file uploads for individual plans.
* @remarks Requires self-hosting.
*/
@Component({
selector: "individual-self-hosting-license-uploader",
templateUrl: "./self-hosting-license-uploader.component.html",
})
export class IndividualSelfHostingLicenseUploaderComponent extends AbstractSelfHostingLicenseUploaderComponent {
/**
* Emitted when a license file has been successfully uploaded & processed.
*/
@Output() onLicenseFileUploaded: EventEmitter<void> = new EventEmitter<void>();
constructor(
protected readonly apiService: ApiService,
protected readonly formBuilder: FormBuilder,
protected readonly i18nService: I18nService,
protected readonly platformUtilsService: PlatformUtilsService,
protected readonly syncService: SyncService,
protected readonly toastService: ToastService,
protected readonly tokenService: TokenService,
) {
super(formBuilder, i18nService, platformUtilsService, toastService, tokenService);
}
protected async submit(): Promise<void> {
await super.submit();
const formData = new FormData();
formData.append("license", this.formValue.file);
await this.apiService.postAccountLicense(formData);
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
this.onLicenseFileUploaded.emit();
}
get description(): string {
return "uploadLicenseFilePremium";
}
get hintFileName(): string {
return "bitwarden_premium_license.json";
}
}

View File

@@ -0,0 +1,3 @@
export interface LicenseUploaderFormModel {
file: File;
}

View File

@@ -0,0 +1,85 @@
import { Component, EventEmitter, Output } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { OrgKey } from "@bitwarden/common/types/key";
import { ToastService } from "@bitwarden/components";
import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-hosting-license-uploader/abstract-self-hosting-license-uploader.component";
/**
* Processes license file uploads for organizations.
* @remarks Requires self-hosting.
*/
@Component({
selector: "organization-self-hosting-license-uploader",
templateUrl: "./self-hosting-license-uploader.component.html",
})
export class OrganizationSelfHostingLicenseUploaderComponent extends AbstractSelfHostingLicenseUploaderComponent {
/**
* Notifies the parent component of the `organizationId` the license was created for.
*/
@Output() onLicenseFileUploaded: EventEmitter<string> = new EventEmitter<string>();
constructor(
protected readonly formBuilder: FormBuilder,
protected readonly i18nService: I18nService,
protected readonly platformUtilsService: PlatformUtilsService,
protected readonly toastService: ToastService,
protected readonly tokenService: TokenService,
private readonly apiService: ApiService,
private readonly encryptService: EncryptService,
private readonly cryptoService: CryptoService,
private readonly organizationApiService: OrganizationApiServiceAbstraction,
private readonly syncService: SyncService,
) {
super(formBuilder, i18nService, platformUtilsService, toastService, tokenService);
}
protected async submit(): Promise<void> {
await super.submit();
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
const key = orgKey[0].encryptedString;
const collection = await this.encryptService.encrypt(
this.i18nService.t("defaultCollection"),
orgKey[1],
);
const collectionCt = collection.encryptedString;
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
const fd = new FormData();
fd.append("license", this.formValue.file);
fd.append("key", key);
fd.append("collectionName", collectionCt);
const response = await this.organizationApiService.createLicense(fd);
const orgId = response.id;
await this.apiService.refreshIdentityToken();
// Org Keys live outside of the OrganizationLicense - add the keys to the org here
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
await this.organizationApiService.updateKeys(orgId, request);
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
this.onLicenseFileUploaded.emit(orgId);
}
get description(): string {
return "uploadLicenseFileOrg";
}
get hintFileName(): string {
return "bitwarden_organization_license.json";
}
}

View File

@@ -0,0 +1,26 @@
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
<form [formGroup]="form" [bitSubmit]="submit">
<bit-form-field>
<bit-label>{{ description | i18n }}</bit-label>
<div>
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{ form.value.file ? form.value.file.name : ("noFileChosen" | i18n) }}
</div>
<input
#fileSelector
bitInput
type="file"
formControlName="file"
(change)="onLicenseFileSelectedChanged($event)"
accept="application/JSON"
hidden
class="tw-hidden"
/>
<bit-hint>{{ "licenseFileDesc" | i18n: hintFileName }}</bit-hint>
</bit-form-field>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "submit" | i18n }}
</button>
</form>