mirror of
https://github.com/bitwarden/browser
synced 2026-03-02 11:31:44 +00:00
Merge branch 'main' into km/beeep/fido2-rust-v2
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { WebSsoComponentService } from "./web-sso-component.service";
|
||||
|
||||
describe("WebSsoComponentService", () => {
|
||||
let service: WebSsoComponentService;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
|
||||
beforeEach(() => {
|
||||
i18nService = mock<I18nService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [WebSsoComponentService, { provide: I18nService, useValue: i18nService }],
|
||||
});
|
||||
service = TestBed.inject(WebSsoComponentService);
|
||||
});
|
||||
|
||||
it("creates the service", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("setDocumentCookies", () => {
|
||||
it("sets ssoHandOffMessage cookie with translated message", () => {
|
||||
const mockMessage = "Test SSO Message";
|
||||
i18nService.t.mockReturnValue(mockMessage);
|
||||
|
||||
service.setDocumentCookies?.();
|
||||
|
||||
expect(document.cookie).toContain(`ssoHandOffMessage=${mockMessage}`);
|
||||
expect(i18nService.t).toHaveBeenCalledWith("ssoHandOff");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { DefaultSsoComponentService, SsoComponentService } from "@bitwarden/auth/angular";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
/**
|
||||
* This service is used to handle the SSO login process for the web client.
|
||||
*/
|
||||
@Injectable()
|
||||
export class WebSsoComponentService
|
||||
extends DefaultSsoComponentService
|
||||
implements SsoComponentService
|
||||
{
|
||||
constructor(private i18nService: I18nService) {
|
||||
super();
|
||||
}
|
||||
|
||||
setDocumentCookies() {
|
||||
document.cookie = `ssoHandOffMessage=${this.i18nService.t("ssoHandOff")};SameSite=strict`;
|
||||
}
|
||||
}
|
||||
@@ -35,10 +35,10 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
|
||||
|
||||
@Component({
|
||||
selector: "app-sso",
|
||||
templateUrl: "sso.component.html",
|
||||
templateUrl: "sso-v1.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class SsoComponent extends BaseSsoComponent implements OnInit {
|
||||
export class SsoComponentV1 extends BaseSsoComponent implements OnInit {
|
||||
protected formGroup = new FormGroup({
|
||||
identifier: new FormControl(null, [Validators.required]),
|
||||
});
|
||||
@@ -23,12 +23,17 @@
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="orgInfoFormGroup.controls.name.invalid"
|
||||
(click)="conditionallyCreateOrganization()"
|
||||
[loading]="loading && (trialPaymentOptional$ | async)"
|
||||
(click)="orgNameEntrySubmit()"
|
||||
>
|
||||
{{ "next" | i18n }}
|
||||
{{ (trialPaymentOptional$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }}
|
||||
</button>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Billing" [subLabel]="billingSubLabel" *ngIf="!isSecretsManagerFree">
|
||||
<app-vertical-step
|
||||
label="Billing"
|
||||
[subLabel]="billingSubLabel"
|
||||
*ngIf="!(trialPaymentOptional$ | async) && !isSecretsManagerFree"
|
||||
>
|
||||
<app-trial-billing-step
|
||||
*ngIf="stepper.selectedIndex === 2"
|
||||
[organizationInfo]="{
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { Subject, takeUntil } from "rxjs";
|
||||
import { firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { PasswordInputResult, RegistrationFinishService } from "@bitwarden/auth/angular";
|
||||
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
|
||||
@@ -12,8 +12,14 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs
|
||||
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 { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
||||
import { ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
|
||||
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";
|
||||
@@ -28,6 +34,10 @@ import { RouterService } from "../../../core/router.service";
|
||||
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.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",
|
||||
@@ -65,6 +75,8 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
email = "";
|
||||
/** Token from the backend associated with the email verification */
|
||||
emailVerificationToken: string;
|
||||
loading = false;
|
||||
productTierValue: number;
|
||||
|
||||
orgInfoFormGroup = this.formBuilder.group({
|
||||
name: ["", { validators: [Validators.required, Validators.maxLength(50)], updateOn: "change" }],
|
||||
@@ -74,6 +86,9 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
protected readonly SubscriptionProduct = SubscriptionProduct;
|
||||
protected readonly ProductType = ProductType;
|
||||
protected trialPaymentOptional$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.TrialPaymentOptional,
|
||||
);
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
@@ -90,6 +105,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
private registrationFinishService: RegistrationFinishService,
|
||||
private validationService: ValidationService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -119,6 +135,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
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);
|
||||
@@ -185,6 +202,16 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
async orgNameEntrySubmit(): Promise<void> {
|
||||
const isTrialPaymentOptional = await firstValueFrom(this.trialPaymentOptional$);
|
||||
|
||||
if (isTrialPaymentOptional) {
|
||||
await this.createOrganizationOnTrial();
|
||||
} else {
|
||||
await this.conditionallyCreateOrganization();
|
||||
}
|
||||
}
|
||||
|
||||
/** Update local details from organization created event */
|
||||
createdOrganization(event: OrganizationCreatedEvent) {
|
||||
this.orgId = event.organizationId;
|
||||
@@ -192,11 +219,62 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
@@ -35,7 +34,6 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private logService: LogService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private dialogService: DialogService,
|
||||
private toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
@@ -87,14 +85,21 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
get isSentAwaitingSync() {
|
||||
return this.isSelfHosted && !this.sponsoringOrg.familySponsorshipLastSyncDate;
|
||||
}
|
||||
|
||||
private async doRevokeSponsorship() {
|
||||
const content = this.sponsoringOrg.familySponsorshipValidUntil
|
||||
? this.i18nService.t(
|
||||
"updatedRevokeSponsorshipConfirmationForAcceptedSponsorship",
|
||||
this.sponsoringOrg.familySponsorshipFriendlyName,
|
||||
formatDate(this.sponsoringOrg.familySponsorshipValidUntil, "MM/dd/yyyy", this.locale),
|
||||
)
|
||||
: this.i18nService.t(
|
||||
"updatedRevokeSponsorshipConfirmationForSentSponsorship",
|
||||
this.sponsoringOrg.familySponsorshipFriendlyName,
|
||||
);
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: `${this.i18nService.t("remove")} ${this.sponsoringOrg.familySponsorshipFriendlyName}?`,
|
||||
content: { key: "revokeSponsorshipConfirmation" },
|
||||
title: `${this.i18nService.t("removeSponsorship")}?`,
|
||||
content,
|
||||
acceptButtonText: { key: "remove" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
LoginComponentService,
|
||||
LockComponentService,
|
||||
SetPasswordJitService,
|
||||
SsoComponentService,
|
||||
LoginDecryptionOptionsService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
@@ -101,6 +102,7 @@ import {
|
||||
WebLockComponentService,
|
||||
WebLoginDecryptionOptionsService,
|
||||
} from "../auth";
|
||||
import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service";
|
||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||
import { HtmlStorageService } from "../core/html-storage.service";
|
||||
import { I18nService } from "../core/i18n.service";
|
||||
@@ -301,6 +303,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: LoginEmailService,
|
||||
deps: [AccountService, AuthService, StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SsoComponentService,
|
||||
useClass: WebSsoComponentService,
|
||||
deps: [I18nServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginDecryptionOptionsService,
|
||||
useClass: WebLoginDecryptionOptionsService,
|
||||
|
||||
@@ -29,11 +29,13 @@ import {
|
||||
LockIcon,
|
||||
TwoFactorTimeoutIcon,
|
||||
UserLockIcon,
|
||||
SsoKeyIcon,
|
||||
LoginViaAuthRequestComponent,
|
||||
DevicesIcon,
|
||||
RegistrationUserAddIcon,
|
||||
RegistrationLockAltIcon,
|
||||
RegistrationExpiredLinkIcon,
|
||||
SsoComponent,
|
||||
VaultIcon,
|
||||
LoginDecryptionOptionsComponent,
|
||||
} from "@bitwarden/auth/angular";
|
||||
@@ -62,7 +64,7 @@ import { AccountComponent } from "./auth/settings/account/account.component";
|
||||
import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component";
|
||||
import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component";
|
||||
import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module";
|
||||
import { SsoComponent } from "./auth/sso.component";
|
||||
import { SsoComponentV1 } from "./auth/sso-v1.component";
|
||||
import { CompleteTrialInitiationComponent } from "./auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component";
|
||||
import { freeTrialTextResolver } from "./auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver";
|
||||
import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component";
|
||||
@@ -430,27 +432,57 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "sso",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "enterpriseSingleSignOn",
|
||||
},
|
||||
titleId: "enterpriseSingleSignOn",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: SsoComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
...unauthUiRefreshSwap(
|
||||
SsoComponentV1,
|
||||
SsoComponent,
|
||||
{
|
||||
path: "sso",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "enterpriseSingleSignOn",
|
||||
},
|
||||
titleId: "enterpriseSingleSignOn",
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: SsoComponentV1,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "sso",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "singleSignOn",
|
||||
},
|
||||
titleId: "enterpriseSingleSignOn",
|
||||
pageSubtitle: {
|
||||
key: "singleSignOnEnterOrgIdentifierText",
|
||||
},
|
||||
titleAreaMaxWidth: "md",
|
||||
pageIcon: SsoKeyIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: SsoComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn()],
|
||||
|
||||
@@ -50,7 +50,7 @@ import { TwoFactorSetupYubiKeyComponent } from "../auth/settings/two-factor/two-
|
||||
import { TwoFactorSetupComponent } from "../auth/settings/two-factor/two-factor-setup.component";
|
||||
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor/two-factor-verify.component";
|
||||
import { UserVerificationModule } from "../auth/shared/components/user-verification";
|
||||
import { SsoComponent } from "../auth/sso.component";
|
||||
import { SsoComponentV1 } from "../auth/sso-v1.component";
|
||||
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component";
|
||||
import { TwoFactorComponent } from "../auth/two-factor.component";
|
||||
import { UpdatePasswordComponent } from "../auth/update-password.component";
|
||||
@@ -158,7 +158,7 @@ import { SharedModule } from "./shared.module";
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
SsoComponent,
|
||||
SsoComponentV1,
|
||||
TwoFactorSetupAuthenticatorComponent,
|
||||
TwoFactorComponent,
|
||||
TwoFactorSetupDuoComponent,
|
||||
@@ -225,7 +225,7 @@ import { SharedModule } from "./shared.module";
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
SsoComponent,
|
||||
SsoComponentV1,
|
||||
TwoFactorSetupAuthenticatorComponent,
|
||||
TwoFactorComponent,
|
||||
TwoFactorSetupDuoComponent,
|
||||
|
||||
@@ -4739,6 +4739,12 @@
|
||||
"ssoLogInWithOrgIdentifier": {
|
||||
"message": "Log in using your organization's single sign-on portal. Please enter your organization's SSO identifier to begin."
|
||||
},
|
||||
"singleSignOnEnterOrgIdentifier": {
|
||||
"message": "Enter your organization's SSO identifier to begin"
|
||||
},
|
||||
"singleSignOnEnterOrgIdentifierText": {
|
||||
"message": "To log in with your SSO provider, enter your organization's SSO identifier to begin. You may need to enter this SSO identifier when you log in from a new device."
|
||||
},
|
||||
"enterpriseSingleSignOn": {
|
||||
"message": "Enterprise single sign-on"
|
||||
},
|
||||
@@ -6156,9 +6162,6 @@
|
||||
"emailSent": {
|
||||
"message": "Email sent"
|
||||
},
|
||||
"revokeSponsorshipConfirmation": {
|
||||
"message": "After removing this account, the Families plan sponsorship will expire at the end of the billing period. You will not be able to redeem a new sponsorship offer until the existing one expires. Are you sure you want to continue?"
|
||||
},
|
||||
"removeSponsorshipSuccess": {
|
||||
"message": "Sponsorship removed"
|
||||
},
|
||||
@@ -9959,5 +9962,27 @@
|
||||
"example": "bitwarden.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updatedRevokeSponsorshipConfirmationForSentSponsorship": {
|
||||
"message": "If you remove $EMAIL$, the sponsorship for this Family plan cannot be redeemed. Are you sure you want to continue?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "sponsored@organization.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"updatedRevokeSponsorshipConfirmationForAcceptedSponsorship": {
|
||||
"message": "If you remove $EMAIL$, the sponsorship for this Family plan will end and the saved payment method will be charged $40 + applicable tax on $DATE$. You will not be able to redeem a new sponsorship until $DATE$. Are you sure you want to continue?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "sponsored@organization.com"
|
||||
},
|
||||
"date": {
|
||||
"content": "$2",
|
||||
"example": "12/10/2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user