mirror of
https://github.com/bitwarden/browser
synced 2026-01-02 16:43:19 +00:00
* feat(change-password-component): Change Password Update [18720] - Very close to complete. * fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Removed temp code to force the state I need to verify correctness. * fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Recover account working with change password component. * fix(policy-enforcement): [PM-21085] Fix Bug with Policy Enforcement - Made code more dry. * fix(change-password-component): Change Password Update [18720] - Updates to routing and the extension. Extension is still a wip. * fix(change-password-component): Change Password Update [18720] - Extension routing changes. * feat(change-password-component): Change Password Update [18720] - More extension work * feat(change-password-component): Change Password Update [18720] - Pausing work for now while we wait for product to hear back. * feat(change-password-component): Change Password Update [18720] - Removed duplicated anon layouts. * feat(change-password-component): Change Password Update [18720] - Tidied up code. * feat(change-password-component): Change Password Update [18720] - Small fixes to the styling * feat(change-password-component): Change Password Update [18720] - Adding more content for the routing. * feat(change-password-component): Change Password Update [18720] - Removed circular loop for now. * feat(change-password-component): Change Password Update [18720] - Made comments regarding the change password routing complexities with change-password and auth guard. * feat(change-password-component): Change Password Update [18720] - Undid some changes because they will be conflicts later on. * feat(change-password-component): Change Password Update [18720] - Small directive change. * feat(change-password-component): Change Password Update [18720] - Small changes and added some clarification on where I'm blocked * feat(change-password-component): Change Password Update [18720] - Org invite is seemingly working, found one bug to iron out. * refactor(change-password-component): Change Password Update [18720] - Fixed up policy service to be made more clear. * docs(change-password-component): Change Password Update [18720] - Updated documentation. * refactor(change-password-component): Change Password Update [18720] - Routing changes and policy service changes. * fix(change-password-component): Change Password Update [18720] - Wrapping up changes. * feat(change-password-component): Change Password Update [18720] - Should be working fully * feat(change-password-component): Change Password Update [18720] - Found a bug, working on password policy being present on login. * feat(change-password-component): Change Password Update [18720] - Turned on auth guard on other clients for change-password route. * feat(change-password-component): Change Password Update [18720] - Committing intermediate changes. * feat(change-password-component): Change Password Update [18720] - The master password policy endpoint has been added! Should be working. Testing now. * feat(change-password-component): Change Password Update [18720] - Minor fixes. * feat(change-password-component): Change Password Update [18720] - Undid naming change. * feat(change-password-component): Change Password Update [18720] - Removed comment. * feat(change-password-component): Change Password Update [18720] - Removed unneeded code. * fix(change-password-component): Change Password Update [18720] - Took org invite state out of service and made it accessible. * fix(change-password-component): Change Password Update [18720] - Small changes. * fix(change-password-component): Change Password Update [18720] - Split up org invite service into client specific implementations and have them injected into clients properly * feat(change-password-component): Change Password Update [18720] - Stopping work and going to switch to a new branch to pare down some of the solutions that were made to get this over the finish line * feat(change-password-component): Change Password Update [18720] - Started to remove functionality in the login.component and the password login strategy. * feat(change-password-component): Change Password Update [18720] - Removed more unneded changes. * feat(change-password-component): Change Password Update [18720] - Change password clearing state working properly. * fix(change-password-component): Change Password Update [18720] - Added docs and moved web implementation. * comments(change-password-component): Change Password Update [18720] - Added more notes. * test(change-password-component): Change Password Update [18720] - Added in tests for policy service. * comment(change-password-component): Change Password Update [18720] - Updated doc with correct ticket number. * comment(change-password-component): Change Password Update [18720] - Fixed doc. * test(change-password-component): Change Password Update [18720] - Fixed tests. * test(change-password-component): Change Password Update [18720] - Fixed linting errors. Have more tests to fix. * test(change-password-component): Change Password Update [18720] - Added back in ignore for typesafety. * fix(change-password-component): Change Password Update [18720] - Fixed other type issues. * test(change-password-component): Change Password Update [18720] - Fixed tests. * test(change-password-component): Change Password Update [18720] - Fixed more tests. * test(change-password-component): Change Password Update [18720] - Fixed tiny duplicate code. * fix(change-password-component): Change Password Update [18720] - Fixed desktop component. * fix(change-password-component): Change Password Update [18720] - Removed unused code * fix(change-password-component): Change Password Update [18720] - Fixed locales. * fix(change-password-component): Change Password Update [18720] - Removed tracing. * fix(change-password-component): Change Password Update [18720] - Removed duplicative services module entry. * fix(change-password-component): Change Password Update [18720] - Added comment. * fix(change-password-component): Change Password Update [18720] - Fixed unneeded call in two factor to get user id. * fix(change-password-component): Change Password Update [18720] - Fixed a couple of tiny things. * fix(change-password-component): Change Password Update [18720] - Added comment for later fix. * fix(change-password-component): Change Password Update [18720] - Fixed linting error. * PM-18720 - AuthGuard - move call to get isChangePasswordFlagOn down after other conditions for efficiency. * PM-18720 - PasswordLoginStrategy tests - test new feature flagged combine org invite policies logic for weak password evaluation. * PM-18720 - CLI - fix dep issue * PM-18720 - ChangePasswordComp - extract change password warning up out of input password component * PM-18720 - InputPassword - remove unused dependency. * PM-18720 - ChangePasswordComp - add callout dep * PM-18720 - Revert all anon-layout changes * PM-18720 - Anon Layout - finish reverting changes. * PM-18720 - WIP move of change password out of libs/auth * PM-18720 - Clean up remaining imports from moving change password out of libs/auth * PM-18720 - Add change-password barrel file for better import grouping * PM-18720 - Change Password comp - restore maxWidth * PM-18720 - After merge, fix errors * PM-18720 - Desktop - fix api service import * PM-18720 - NDV - fix routing. * PM-18720 - Change Password Comp - add logout service todo * PM-18720 - PasswordSettings - per feedback, component is already feature flagged behind PM16117_ChangeExistingPasswordRefactor so we can just delete the replaced callout (new text is in change-password comp) * PM-18720 - Routing Modules - properly flag new component behind feature flag. * PM-18720 - SSO Login Strategy - fix config service import since it is now in shared deps from main merge. * PM-18720 - Fix SSO login strategy tests * PM-18720 - Default Policy Service - address AC PR feedback --------- Co-authored-by: Jared Snider <jsnider@bitwarden.com> Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
425 lines
14 KiB
TypeScript
425 lines
14 KiB
TypeScript
// FIXME: Update this file to be type safe and remove this and next line
|
|
// @ts-strict-ignore
|
|
import { StepperSelectionEvent } from "@angular/cdk/stepper";
|
|
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
|
import { FormBuilder, Validators } from "@angular/forms";
|
|
import { ActivatedRoute, Router } from "@angular/router";
|
|
import { combineLatest, firstValueFrom, map, Subject, switchMap, takeUntil } from "rxjs";
|
|
|
|
import {
|
|
InputPasswordFlow,
|
|
PasswordInputResult,
|
|
RegistrationFinishService,
|
|
} from "@bitwarden/auth/angular";
|
|
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
|
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
|
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
|
|
import {
|
|
OrganizationBillingServiceAbstraction as OrganizationBillingService,
|
|
OrganizationInformation,
|
|
PlanInformation,
|
|
} from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
|
import { PlanType, ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
|
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
|
import { ToastService } from "@bitwarden/components";
|
|
|
|
import {
|
|
OrganizationCreatedEvent,
|
|
SubscriptionProduct,
|
|
TrialOrganizationType,
|
|
} from "../../../billing/accounts/trial-initiation/trial-billing-step.component";
|
|
import { RouterService } from "../../../core/router.service";
|
|
import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component";
|
|
|
|
export type InitiationPath =
|
|
| "Password Manager trial from marketing website"
|
|
| "Secrets Manager trial from marketing website";
|
|
|
|
@Component({
|
|
selector: "app-complete-trial-initiation",
|
|
templateUrl: "complete-trial-initiation.component.html",
|
|
standalone: false,
|
|
})
|
|
export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
|
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
|
|
|
inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAccountRegistration;
|
|
initializing = true;
|
|
|
|
/** Password Manager or Secrets Manager */
|
|
product: ProductType;
|
|
/** The tier of product being subscribed to */
|
|
productTier: ProductTierType;
|
|
/** Product types that display steppers for Password Manager */
|
|
stepperProductTypes: ProductTierType[] = [
|
|
ProductTierType.Teams,
|
|
ProductTierType.Enterprise,
|
|
ProductTierType.Families,
|
|
];
|
|
|
|
/** Display multi-step trial flow when true */
|
|
useTrialStepper = false;
|
|
|
|
/** True, registering a password is in progress */
|
|
submitting = false;
|
|
|
|
/** Valid product types, used to filter out invalid query parameters */
|
|
validProducts = [ProductType.PasswordManager, ProductType.SecretsManager];
|
|
|
|
orgInfoSubLabel = "";
|
|
orgId = "";
|
|
orgLabel = "";
|
|
billingSubLabel = "";
|
|
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
|
|
|
/** User's email address associated with the trial */
|
|
email = "";
|
|
/** Token from the backend associated with the email verification */
|
|
emailVerificationToken: string;
|
|
loading = false;
|
|
productTierValue: number;
|
|
|
|
trialLength: number;
|
|
|
|
orgInfoFormGroup = this.formBuilder.group({
|
|
name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }],
|
|
billingEmail: [""],
|
|
});
|
|
|
|
private destroy$ = new Subject<void>();
|
|
protected readonly SubscriptionProduct = SubscriptionProduct;
|
|
protected readonly ProductType = ProductType;
|
|
protected trialPaymentOptional$ = this.configService.getFeatureFlag$(
|
|
FeatureFlag.TrialPaymentOptional,
|
|
);
|
|
protected allowTrialLengthZero$ = this.configService.getFeatureFlag$(
|
|
FeatureFlag.AllowTrialLengthZero,
|
|
);
|
|
|
|
constructor(
|
|
protected router: Router,
|
|
private route: ActivatedRoute,
|
|
private formBuilder: FormBuilder,
|
|
private logService: LogService,
|
|
private policyApiService: PolicyApiServiceAbstraction,
|
|
private policyService: PolicyService,
|
|
private i18nService: I18nService,
|
|
private routerService: RouterService,
|
|
private organizationBillingService: OrganizationBillingService,
|
|
private organizationInviteService: OrganizationInviteService,
|
|
private toastService: ToastService,
|
|
private registrationFinishService: RegistrationFinishService,
|
|
private validationService: ValidationService,
|
|
private loginStrategyService: LoginStrategyServiceAbstraction,
|
|
private configService: ConfigService,
|
|
private accountService: AccountService,
|
|
) {}
|
|
|
|
async ngOnInit(): Promise<void> {
|
|
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => {
|
|
// Retrieve email from query params
|
|
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
|
|
this.email = qParams.email;
|
|
this.orgInfoFormGroup.controls.billingEmail.setValue(qParams.email);
|
|
}
|
|
|
|
// Show email validation toast when coming from email
|
|
if (qParams.fromEmail && qParams.fromEmail === "true") {
|
|
this.toastService.showToast({
|
|
title: null,
|
|
message: this.i18nService.t("emailVerifiedV2"),
|
|
variant: "success",
|
|
});
|
|
}
|
|
|
|
if (qParams.token != null) {
|
|
this.emailVerificationToken = qParams.token;
|
|
}
|
|
|
|
const product = parseInt(qParams.product);
|
|
|
|
// Get product from query params, default to password manager
|
|
this.product = this.validProducts.includes(product) ? product : ProductType.PasswordManager;
|
|
|
|
const productTierParam = parseInt(qParams.productTier) as ProductTierType;
|
|
this.productTierValue = productTierParam;
|
|
|
|
/** Only show the trial stepper for a subset of types */
|
|
const showPasswordManagerStepper = this.stepperProductTypes.includes(productTierParam);
|
|
|
|
/** All types of secret manager should see the trial stepper */
|
|
const showSecretsManagerStepper = this.product === ProductType.SecretsManager;
|
|
|
|
if ((showPasswordManagerStepper || showSecretsManagerStepper) && !isNaN(productTierParam)) {
|
|
this.productTier = productTierParam;
|
|
|
|
this.orgLabel = this.planTypeDisplay;
|
|
|
|
this.useTrialStepper = true;
|
|
}
|
|
|
|
this.trialLength = qParams.trialLength ? parseInt(qParams.trialLength) : 7;
|
|
|
|
// Are they coming from an email for sponsoring a families organization
|
|
// After logging in redirect them to setup the families sponsorship
|
|
this.setupFamilySponsorship(qParams.sponsorshipToken);
|
|
});
|
|
|
|
const invite = await this.organizationInviteService.getOrganizationInvite();
|
|
let policies: Policy[] | null = null;
|
|
|
|
if (invite != null) {
|
|
try {
|
|
policies = await this.policyApiService.getPoliciesByToken(
|
|
invite.organizationId,
|
|
invite.token,
|
|
invite.email,
|
|
invite.organizationUserId,
|
|
);
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
}
|
|
}
|
|
|
|
if (policies !== null) {
|
|
this.accountService.activeAccount$
|
|
.pipe(
|
|
getUserId,
|
|
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)),
|
|
takeUntil(this.destroy$),
|
|
)
|
|
.subscribe((enforcedPasswordPolicyOptions) => {
|
|
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
|
|
});
|
|
}
|
|
|
|
this.orgInfoFormGroup.controls.name.valueChanges
|
|
.pipe(takeUntil(this.destroy$))
|
|
.subscribe(() => {
|
|
this.orgInfoFormGroup.controls.name.markAsTouched();
|
|
});
|
|
|
|
this.initializing = false;
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
}
|
|
|
|
/** Handle manual stepper change */
|
|
verticalStepChange(event: StepperSelectionEvent) {
|
|
if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") {
|
|
this.orgInfoSubLabel = this.planInfoLabel;
|
|
} else if (event.previouslySelectedIndex === 1) {
|
|
this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value;
|
|
}
|
|
}
|
|
|
|
async orgNameEntrySubmit(): Promise<void> {
|
|
const isTrialPaymentOptional = await firstValueFrom(this.trialPaymentOptional$);
|
|
|
|
/** Only skip payment if the flag is on AND trialLength > 0 */
|
|
if (isTrialPaymentOptional && this.trialLength > 0) {
|
|
await this.createOrganizationOnTrial();
|
|
} else {
|
|
await this.conditionallyCreateOrganization();
|
|
}
|
|
}
|
|
|
|
/** Update local details from organization created event */
|
|
createdOrganization(event: OrganizationCreatedEvent) {
|
|
this.orgId = event.organizationId;
|
|
this.billingSubLabel = event.planDescription;
|
|
this.verticalStepper.next();
|
|
}
|
|
|
|
/** create an organization on trial without payment method */
|
|
async createOrganizationOnTrial() {
|
|
this.loading = true;
|
|
let trialInitiationPath: InitiationPath = "Password Manager trial from marketing website";
|
|
let plan: PlanInformation = {
|
|
type: this.getPlanType(),
|
|
passwordManagerSeats: 1,
|
|
};
|
|
|
|
if (this.product === ProductType.SecretsManager) {
|
|
trialInitiationPath = "Secrets Manager trial from marketing website";
|
|
plan = {
|
|
...plan,
|
|
subscribeToSecretsManager: true,
|
|
isFromSecretsManagerTrial: true,
|
|
secretsManagerSeats: 1,
|
|
};
|
|
}
|
|
|
|
const organization: OrganizationInformation = {
|
|
name: this.orgInfoFormGroup.value.name,
|
|
billingEmail: this.orgInfoFormGroup.value.billingEmail,
|
|
initiationPath: trialInitiationPath,
|
|
};
|
|
|
|
const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({
|
|
organization,
|
|
plan,
|
|
});
|
|
|
|
this.orgId = response?.id;
|
|
this.billingSubLabel = response.name.toString();
|
|
this.loading = false;
|
|
this.verticalStepper.next();
|
|
}
|
|
|
|
/** Move the user to the previous step */
|
|
previousStep() {
|
|
this.verticalStepper.previous();
|
|
}
|
|
|
|
getPlanType() {
|
|
switch (this.productTier) {
|
|
case ProductTierType.Teams:
|
|
return PlanType.TeamsAnnually;
|
|
case ProductTierType.Enterprise:
|
|
return PlanType.EnterpriseAnnually;
|
|
case ProductTierType.Families:
|
|
return PlanType.FamiliesAnnually;
|
|
case ProductTierType.Free:
|
|
return PlanType.Free;
|
|
default:
|
|
return PlanType.EnterpriseAnnually;
|
|
}
|
|
}
|
|
|
|
get isSecretsManagerFree() {
|
|
return this.product === ProductType.SecretsManager && this.productTier === ProductTierType.Free;
|
|
}
|
|
|
|
get planTypeDisplay() {
|
|
switch (this.productTier) {
|
|
case ProductTierType.Teams:
|
|
return "Teams";
|
|
case ProductTierType.Enterprise:
|
|
return "Enterprise";
|
|
case ProductTierType.Families:
|
|
return "Families";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
get planInfoLabel() {
|
|
switch (this.productTier) {
|
|
case ProductTierType.Teams:
|
|
return this.i18nService.t("enterTeamsOrgInfo");
|
|
case ProductTierType.Enterprise:
|
|
return this.i18nService.t("enterEnterpriseOrgInfo");
|
|
case ProductTierType.Families:
|
|
return this.i18nService.t("enterFamiliesOrgInfo");
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
get trialOrganizationType(): TrialOrganizationType {
|
|
if (this.productTier === ProductTierType.Free) {
|
|
return null;
|
|
}
|
|
|
|
return this.productTier;
|
|
}
|
|
|
|
readonly showBillingStep$ = combineLatest([
|
|
this.trialPaymentOptional$,
|
|
this.allowTrialLengthZero$,
|
|
]).pipe(
|
|
map(([trialPaymentOptional, allowTrialLengthZero]) => {
|
|
return (
|
|
(!trialPaymentOptional && !this.isSecretsManagerFree) ||
|
|
(trialPaymentOptional && allowTrialLengthZero && this.trialLength === 0)
|
|
);
|
|
}),
|
|
);
|
|
|
|
/** Create an organization unless the trial is for secrets manager */
|
|
async conditionallyCreateOrganization(): Promise<void> {
|
|
if (!this.isSecretsManagerFree) {
|
|
this.verticalStepper.next();
|
|
return;
|
|
}
|
|
|
|
const response = await this.organizationBillingService.startFree({
|
|
organization: {
|
|
name: this.orgInfoFormGroup.value.name,
|
|
billingEmail: this.orgInfoFormGroup.value.billingEmail,
|
|
},
|
|
plan: {
|
|
type: 0,
|
|
subscribeToSecretsManager: true,
|
|
isFromSecretsManagerTrial: true,
|
|
},
|
|
});
|
|
|
|
this.orgId = response.id;
|
|
this.verticalStepper.next();
|
|
}
|
|
|
|
/**
|
|
* Complete the users registration with their password.
|
|
*
|
|
* When a the trial stepper isn't used, redirect the user to the login page.
|
|
*/
|
|
async handlePasswordSubmit(passwordInputResult: PasswordInputResult) {
|
|
if (!this.useTrialStepper) {
|
|
await this.finishRegistration(passwordInputResult);
|
|
this.submitting = false;
|
|
|
|
await this.router.navigate(["/login"], { queryParams: { email: this.email } });
|
|
return;
|
|
}
|
|
|
|
await this.finishRegistration(passwordInputResult);
|
|
|
|
await this.logIn(passwordInputResult.newPassword);
|
|
|
|
this.submitting = false;
|
|
|
|
this.verticalStepper.next();
|
|
}
|
|
|
|
private setupFamilySponsorship(sponsorshipToken: string) {
|
|
if (sponsorshipToken != null) {
|
|
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
|
queryParams: { plan: sponsorshipToken },
|
|
});
|
|
this.routerService.setPreviousUrl(route.toString());
|
|
}
|
|
}
|
|
|
|
/** Logs the user in */
|
|
private async logIn(masterPassword: string): Promise<void> {
|
|
const credentials = new PasswordLoginCredentials(this.email, masterPassword);
|
|
|
|
await this.loginStrategyService.logIn(credentials);
|
|
}
|
|
|
|
finishRegistration(passwordInputResult: PasswordInputResult) {
|
|
this.submitting = true;
|
|
return this.registrationFinishService
|
|
.finishRegistration(this.email, passwordInputResult, this.emailVerificationToken)
|
|
.catch((e) => {
|
|
this.validationService.showError(e);
|
|
this.submitting = false;
|
|
return null;
|
|
});
|
|
}
|
|
}
|