mirror of
https://github.com/bitwarden/browser
synced 2026-01-05 18:13:26 +00:00
Merge branch 'main' into auth/pm-8111/browser-refresh-login-component
This commit is contained in:
@@ -21,7 +21,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -30,6 +29,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
|
||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { DialogService, ToastOptions, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { PolicyListService } from "./admin-console/core/policy-list.service";
|
||||
import {
|
||||
|
||||
@@ -66,33 +66,39 @@
|
||||
</bit-callout>
|
||||
</bit-section>
|
||||
<bit-section *ngIf="isSelfHost">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||
<form [formGroup]="licenseFormGroup" [bitSubmit]="submitPremiumLicense">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{
|
||||
licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n)
|
||||
}}
|
||||
</div>
|
||||
<input
|
||||
bitInput
|
||||
#fileSelector
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="onLicenseFileSelected($event)"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<ng-container *ngIf="!(useLicenseUploaderComponent$ | async)">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||
<form [formGroup]="licenseFormGroup" [bitSubmit]="submitPremiumLicense">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{
|
||||
licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n)
|
||||
}}
|
||||
</div>
|
||||
<input
|
||||
bitInput
|
||||
#fileSelector
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="onLicenseFileSelected($event)"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
<individual-self-hosting-license-uploader
|
||||
*ngIf="useLicenseUploaderComponent$ | async"
|
||||
(onLicenseFileUploaded)="onLicenseFileSelectedChanged()"
|
||||
/>
|
||||
</bit-section>
|
||||
<form *ngIf="!isSelfHost" [formGroup]="addOnFormGroup" [bitSubmit]="submitPayment">
|
||||
<bit-section>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { combineLatest, concatMap, from, Observable, of } from "rxjs";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -36,6 +38,10 @@ export class PremiumV2Component {
|
||||
protected cloudWebVaultURL: string;
|
||||
protected isSelfHost = false;
|
||||
|
||||
protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
|
||||
);
|
||||
|
||||
protected readonly familyPlanMaxUserCount = 6;
|
||||
protected readonly premiumPrice = 10;
|
||||
protected readonly storageGBPrice = 4;
|
||||
@@ -44,6 +50,7 @@ export class PremiumV2Component {
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
private environmentService: EnvironmentService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@@ -78,6 +85,9 @@ export class PremiumV2Component {
|
||||
finalizeUpgrade = async () => {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
};
|
||||
|
||||
postFinalizeUpgrade = async () => {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
@@ -119,6 +129,7 @@ export class PremiumV2Component {
|
||||
|
||||
await this.apiService.postAccountLicense(formData);
|
||||
await this.finalizeUpgrade();
|
||||
await this.postFinalizeUpgrade();
|
||||
};
|
||||
|
||||
submitPayment = async (): Promise<void> => {
|
||||
@@ -138,6 +149,7 @@ export class PremiumV2Component {
|
||||
|
||||
await this.apiService.postPremium(formData);
|
||||
await this.finalizeUpgrade();
|
||||
await this.postFinalizeUpgrade();
|
||||
};
|
||||
|
||||
protected get additionalStorageCost(): number {
|
||||
@@ -161,4 +173,8 @@ export class PremiumV2Component {
|
||||
protected get total(): number {
|
||||
return this.subtotal + this.estimatedTax;
|
||||
}
|
||||
|
||||
protected async onLicenseFileSelectedChanged(): Promise<void> {
|
||||
await this.postFinalizeUpgrade();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,7 +757,7 @@
|
||||
{{ "serviceAccounts" | i18n }}
|
||||
×
|
||||
{{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }}
|
||||
/{{ "month" | i18n }}
|
||||
/{{ "year" | i18n }}
|
||||
</span>
|
||||
<span>{{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}</span>
|
||||
</p>
|
||||
|
||||
@@ -497,7 +497,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const result = plan.PasswordManager.seatPrice * Math.abs(this.organization.seats || 0);
|
||||
const result = plan.PasswordManager.seatPrice * Math.abs(this.sub?.seats || 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,32 +7,38 @@
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="createOrganization && selfHosted">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div class="tw-pt-2 tw-pb-1">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
|
||||
</div>
|
||||
<input
|
||||
#fileSelector
|
||||
bitInput
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="setSelectedFile($event)"
|
||||
accept="application/JSON"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
<ng-container *ngIf="!(useLicenseUploaderComponent$ | async)">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div class="tw-pt-2 tw-pb-1">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
|
||||
</div>
|
||||
<input
|
||||
#fileSelector
|
||||
bitInput
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="setSelectedFile($event)"
|
||||
accept="application/JSON"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
<organization-self-hosting-license-uploader
|
||||
*ngIf="useLicenseUploaderComponent$ | async"
|
||||
(onLicenseFileUploaded)="onLicenseFileUploaded($event)"
|
||||
/>
|
||||
</ng-container>
|
||||
<form
|
||||
[formGroup]="formGroup"
|
||||
|
||||
@@ -117,6 +117,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
discount = 0;
|
||||
deprecateStripeSourcesAPI: boolean;
|
||||
|
||||
protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
|
||||
);
|
||||
|
||||
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
|
||||
|
||||
selfHostedForm = this.formBuilder.group({
|
||||
@@ -855,4 +859,30 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
private planIsEnabled(plan: PlanResponse) {
|
||||
return !plan.disabled && !plan.legacyYear;
|
||||
}
|
||||
|
||||
protected async onLicenseFileUploaded(organizationId: string): Promise<void> {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("organizationCreated"),
|
||||
message: this.i18nService.t("organizationReadyToGo"),
|
||||
});
|
||||
|
||||
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/organizations/" + organizationId]);
|
||||
}
|
||||
|
||||
if (this.isInTrialFlow) {
|
||||
this.onTrialBillingSuccess.emit({
|
||||
orgId: organizationId,
|
||||
subLabelText: this.billingSubLabelText(),
|
||||
});
|
||||
}
|
||||
|
||||
this.onSuccess.emit({ organizationId: organizationId });
|
||||
|
||||
// TODO: No one actually listening to this message?
|
||||
this.messagingService.send("organizationCreated", { organizationId: organizationId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface LicenseUploaderFormModel {
|
||||
file: 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";
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -4,24 +4,24 @@ import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core";
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import {
|
||||
SECURE_STORAGE,
|
||||
LOCALES_DIRECTORY,
|
||||
SYSTEM_LANGUAGE,
|
||||
MEMORY_STORAGE,
|
||||
OBSERVABLE_MEMORY_STORAGE,
|
||||
OBSERVABLE_DISK_STORAGE,
|
||||
OBSERVABLE_DISK_LOCAL_STORAGE,
|
||||
WINDOW,
|
||||
SafeInjectionToken,
|
||||
DEFAULT_VAULT_TIMEOUT,
|
||||
CLIENT_TYPE,
|
||||
DEFAULT_VAULT_TIMEOUT,
|
||||
LOCALES_DIRECTORY,
|
||||
MEMORY_STORAGE,
|
||||
OBSERVABLE_DISK_LOCAL_STORAGE,
|
||||
OBSERVABLE_DISK_STORAGE,
|
||||
OBSERVABLE_MEMORY_STORAGE,
|
||||
SECURE_STORAGE,
|
||||
SYSTEM_LANGUAGE,
|
||||
SafeInjectionToken,
|
||||
WINDOW,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
|
||||
import {
|
||||
SetPasswordJitService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
LoginComponentService,
|
||||
SetPasswordJitService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
@@ -48,7 +48,6 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
@@ -67,6 +66,7 @@ import {
|
||||
} from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||
import {
|
||||
@@ -77,7 +77,7 @@ import {
|
||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||
import { HtmlStorageService } from "../core/html-storage.service";
|
||||
import { I18nService } from "../core/i18n.service";
|
||||
import { WebBiometricsService } from "../platform/web-biometric.service";
|
||||
import { WebBiometricsService } from "../key-management/web-biometric.service";
|
||||
import { WebEnvironmentService } from "../platform/web-environment.service";
|
||||
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
||||
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
export class WebBiometricsService extends BiometricsService {
|
||||
async supportsBiometric(): Promise<boolean> {
|
||||
@@ -5,6 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
@@ -81,7 +82,7 @@ export class CipherReportComponent implements OnDestroy {
|
||||
if (filterId === 0) {
|
||||
cipherCount = this.allCiphers.length;
|
||||
} else if (filterId === 1) {
|
||||
cipherCount = this.allCiphers.filter((c: any) => c.orgFilterStatus === null).length;
|
||||
cipherCount = this.allCiphers.filter((c) => c.organizationId === null).length;
|
||||
} else {
|
||||
this.organizations.filter((org: Organization) => {
|
||||
if (org.id === filterId) {
|
||||
@@ -89,22 +90,20 @@ export class CipherReportComponent implements OnDestroy {
|
||||
return org;
|
||||
}
|
||||
});
|
||||
cipherCount = this.allCiphers.filter(
|
||||
(c: any) => c.orgFilterStatus === orgFilterStatus,
|
||||
).length;
|
||||
cipherCount = this.allCiphers.filter((c) => c.organizationId === orgFilterStatus).length;
|
||||
}
|
||||
return cipherCount;
|
||||
}
|
||||
|
||||
async filterOrgToggle(status: any) {
|
||||
this.currentFilterStatus = status;
|
||||
if (status === 0) {
|
||||
this.dataSource.filter = null;
|
||||
} else if (status === 1) {
|
||||
this.dataSource.filter = (c: any) => c.orgFilterStatus == null;
|
||||
} else {
|
||||
this.dataSource.filter = (c: any) => c.orgFilterStatus === status;
|
||||
let filter = null;
|
||||
if (typeof status === "number" && status === 1) {
|
||||
filter = (c: CipherView) => c.organizationId == null;
|
||||
} else if (typeof status === "string") {
|
||||
const orgId = status as OrganizationId;
|
||||
filter = (c: CipherView) => c.organizationId === orgId;
|
||||
}
|
||||
this.dataSource.filter = filter;
|
||||
}
|
||||
|
||||
async load() {
|
||||
@@ -183,9 +182,7 @@ export class CipherReportComponent implements OnDestroy {
|
||||
protected filterCiphersByOrg(ciphersList: CipherView[]) {
|
||||
this.allCiphers = [...ciphersList];
|
||||
|
||||
this.ciphers = ciphersList.map((ciph: any) => {
|
||||
ciph.orgFilterStatus = ciph.organizationId;
|
||||
|
||||
this.ciphers = ciphersList.map((ciph) => {
|
||||
if (this.filterStatus.indexOf(ciph.organizationId) === -1 && ciph.organizationId != null) {
|
||||
this.filterStatus.push(ciph.organizationId);
|
||||
} else if (this.filterStatus.indexOf(1) === -1 && ciph.organizationId == null) {
|
||||
@@ -193,7 +190,6 @@ export class CipherReportComponent implements OnDestroy {
|
||||
}
|
||||
return ciph;
|
||||
});
|
||||
|
||||
this.dataSource.data = this.ciphers;
|
||||
|
||||
if (this.filterStatus.length > 2) {
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
<th bitCell bitSortable="organizationId" *ngIf="!isAdminConsoleActive">
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-text-right" bitSortable="exposedXTimes"></th>
|
||||
<th bitCell class="tw-text-right" bitSortable="exposedXTimes">
|
||||
{{ "timesExposed" | i18n }}
|
||||
</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<th bitCell></th>
|
||||
<th bitCell>{{ "name" | i18n }}</th>
|
||||
<th bitCell>{{ "owner" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
|
||||
@@ -32,12 +32,16 @@
|
||||
</ng-container>
|
||||
</bit-toggle-group>
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header *ngIf="!isAdminConsoleActive">
|
||||
<ng-container header>
|
||||
<tr bitRow>
|
||||
<th bitCell></th>
|
||||
<th bitCell>{{ "name" | i18n }}</th>
|
||||
<th bitCell>{{ "owner" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
<th bitCell bitSortable="organizationId" *ngIf="!isAdminConsoleActive">
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-text-right" bitSortable="reportValue" default>
|
||||
{{ "weakness" | i18n }}
|
||||
</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
@@ -80,7 +84,7 @@
|
||||
<br />
|
||||
<small>{{ r.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<td bitCell *ngIf="!isAdminConsoleActive">
|
||||
<app-org-badge
|
||||
*ngIf="!organization"
|
||||
[disabled]="disabled"
|
||||
@@ -91,8 +95,8 @@
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge [variant]="passwordStrengthMap.get(r.id)[1]">
|
||||
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
|
||||
<span bitBadge [variant]="r.reportValue.badgeVariant">
|
||||
{{ r.reportValue.label | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -14,16 +14,17 @@ import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { CipherReportComponent } from "./cipher-report.component";
|
||||
|
||||
type ReportScore = { label: string; badgeVariant: BadgeVariant };
|
||||
type ReportResult = CipherView & { reportValue: ReportScore };
|
||||
|
||||
@Component({
|
||||
selector: "app-weak-passwords-report",
|
||||
templateUrl: "weak-passwords-report.component.html",
|
||||
})
|
||||
export class WeakPasswordsReportComponent extends CipherReportComponent implements OnInit {
|
||||
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
|
||||
disabled = true;
|
||||
|
||||
private passwordStrengthCache = new Map<string, number>();
|
||||
weakPasswordCiphers: CipherView[] = [];
|
||||
weakPasswordCiphers: ReportResult[] = [];
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
@@ -49,16 +50,15 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
const allCiphers: any = await this.getAllCiphers();
|
||||
this.passwordStrengthCache = new Map<string, number>();
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
this.weakPasswordCiphers = [];
|
||||
this.filterStatus = [0];
|
||||
this.findWeakPasswords(allCiphers);
|
||||
}
|
||||
|
||||
protected findWeakPasswords(ciphers: any[]): void {
|
||||
protected findWeakPasswords(ciphers: CipherView[]): void {
|
||||
ciphers.forEach((ciph) => {
|
||||
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
|
||||
const { type, login, isDeleted, edit, viewPassword } = ciph;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
@@ -71,50 +71,39 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
||||
}
|
||||
|
||||
const hasUserName = this.isUserNameNotEmpty(ciph);
|
||||
const cacheKey = this.getCacheKey(ciph);
|
||||
if (!this.passwordStrengthCache.has(cacheKey)) {
|
||||
let userInput: string[] = [];
|
||||
if (hasUserName) {
|
||||
const atPosition = login.username.indexOf("@");
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput
|
||||
.concat(
|
||||
login.username
|
||||
.substr(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/),
|
||||
)
|
||||
.filter((i) => i.length >= 3);
|
||||
} else {
|
||||
userInput = login.username
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/)
|
||||
.filter((i: any) => i.length >= 3);
|
||||
}
|
||||
let userInput: string[] = [];
|
||||
if (hasUserName) {
|
||||
const atPosition = login.username.indexOf("@");
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput
|
||||
.concat(
|
||||
login.username
|
||||
.substr(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/),
|
||||
)
|
||||
.filter((i) => i.length >= 3);
|
||||
} else {
|
||||
userInput = login.username
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/)
|
||||
.filter((i) => i.length >= 3);
|
||||
}
|
||||
const result = this.passwordStrengthService.getPasswordStrength(
|
||||
login.password,
|
||||
null,
|
||||
userInput.length > 0 ? userInput : null,
|
||||
);
|
||||
this.passwordStrengthCache.set(cacheKey, result.score);
|
||||
}
|
||||
const score = this.passwordStrengthCache.get(cacheKey);
|
||||
|
||||
if (score != null && score <= 2) {
|
||||
this.passwordStrengthMap.set(id, this.scoreKey(score));
|
||||
this.weakPasswordCiphers.push(ciph);
|
||||
}
|
||||
});
|
||||
this.weakPasswordCiphers.sort((a, b) => {
|
||||
return (
|
||||
this.passwordStrengthCache.get(this.getCacheKey(a)) -
|
||||
this.passwordStrengthCache.get(this.getCacheKey(b))
|
||||
const result = this.passwordStrengthService.getPasswordStrength(
|
||||
login.password,
|
||||
null,
|
||||
userInput.length > 0 ? userInput : null,
|
||||
);
|
||||
});
|
||||
|
||||
if (result.score != null && result.score <= 2) {
|
||||
const scoreValue = this.scoreKey(result.score);
|
||||
const row = { ...ciph, reportValue: scoreValue } as ReportResult;
|
||||
this.weakPasswordCiphers.push(row);
|
||||
}
|
||||
});
|
||||
this.filterCiphersByOrg(this.weakPasswordCiphers);
|
||||
}
|
||||
|
||||
@@ -127,20 +116,16 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
||||
return !Utils.isNullOrWhitespace(c.login.username);
|
||||
}
|
||||
|
||||
private getCacheKey(c: CipherView): string {
|
||||
return c.login.password + "_____" + (this.isUserNameNotEmpty(c) ? c.login.username : "");
|
||||
}
|
||||
|
||||
private scoreKey(score: number): [string, BadgeVariant] {
|
||||
private scoreKey(score: number): ReportScore {
|
||||
switch (score) {
|
||||
case 4:
|
||||
return ["strong", "success"];
|
||||
return { label: "strong", badgeVariant: "success" };
|
||||
case 3:
|
||||
return ["good", "primary"];
|
||||
return { label: "good", badgeVariant: "primary" };
|
||||
case 2:
|
||||
return ["weak", "warning"];
|
||||
return { label: "weak", badgeVariant: "warning" };
|
||||
default:
|
||||
return ["veryWeak", "danger"];
|
||||
return { label: "veryWeak", badgeVariant: "danger" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,10 @@
|
||||
>
|
||||
<bit-item slot="attachment-button">
|
||||
<button bit-item-content type="button" (click)="openAttachmentsDialog()">
|
||||
<p class="tw-m-0">
|
||||
{{ "attachments" | i18n }}
|
||||
<span *ngIf="!canAccessAttachments" bitBadge variant="success" class="tw-ml-2">
|
||||
{{ "premium" | i18n }}
|
||||
</span>
|
||||
</p>
|
||||
{{ "attachments" | i18n }}
|
||||
<span *ngIf="!canAccessAttachments" bitBadge variant="success" slot="default-trailing">
|
||||
{{ "premium" | i18n }}
|
||||
</span>
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</bit-item>
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
@@ -476,15 +477,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
firstSetup$
|
||||
.pipe(
|
||||
switchMap(() =>
|
||||
combineLatest([this.route.queryParams, allCipherMap$, allCollections$, organization$]),
|
||||
),
|
||||
switchMap(() => this.route.queryParams),
|
||||
withLatestFrom(allCipherMap$, allCollections$, organization$),
|
||||
switchMap(async ([qParams, allCiphersMap, allCollections]) => {
|
||||
const cipherId = getCipherIdFromParams(qParams);
|
||||
if (!cipherId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cipher = allCiphersMap[cipherId];
|
||||
const cipherCollections = allCollections.filter((c) =>
|
||||
cipher.collectionIds.includes(c.id),
|
||||
|
||||
Reference in New Issue
Block a user