mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[PM-5718] Fix free organization generating TOTP (#11918)
* [PM-5718] Fix totp generation for free orgs in old add-edit component * [PM-5718] Fix totp generation for free orgs in view cipher view component * [PM-5718] Cleanup merge conflicts * Don't generate totp code for premium users or free orgs * Added redirect to organization helper page * Changed text to learn more * Only show upgrade message to premium users * Show upgrade message to free users with free orgs as well --------- Co-authored-by: Matt Bishop <mbishop@bitwarden.com> Co-authored-by: gbubemismith <gsmithwalter@gmail.com>
This commit is contained in:
@@ -330,6 +330,20 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "upgradeOrganization": {
|
||||||
|
const upgradeConfirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "upgradeOrganization" },
|
||||||
|
content: { key: "upgradeOrganizationDesc" },
|
||||||
|
acceptButtonText: { key: "learnMore" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
if (upgradeConfirmed) {
|
||||||
|
this.platformUtilsService.launchUri(
|
||||||
|
"https://bitwarden.com/help/upgrade-from-individual-to-org/",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "emailVerificationRequired": {
|
case "emailVerificationRequired": {
|
||||||
const emailVerificationConfirmed = await this.dialogService.openSimpleDialog({
|
const emailVerificationConfirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "emailVerificationRequired" },
|
title: { key: "emailVerificationRequired" },
|
||||||
|
|||||||
@@ -3474,5 +3474,14 @@
|
|||||||
},
|
},
|
||||||
"changeAcctEmail": {
|
"changeAcctEmail": {
|
||||||
"message": "Change account email"
|
"message": "Change account email"
|
||||||
|
},
|
||||||
|
"organizationUpgradeRequired": {
|
||||||
|
"message": "Organization upgrade required"
|
||||||
|
},
|
||||||
|
"upgradeOrganization": {
|
||||||
|
"message": "Upgrade organization"
|
||||||
|
},
|
||||||
|
"upgradeOrganizationDesc": {
|
||||||
|
"message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,16 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="box-content-row box-content-row-flex totp" *ngIf="showUpgradeRequiredTotp">
|
||||||
|
<div class="row-main">
|
||||||
|
<span class="row-label">{{ "verificationCodeTotp" | i18n }}</span>
|
||||||
|
<span class="row-label">
|
||||||
|
<a [routerLink]="" (click)="upgradeOrganization()"
|
||||||
|
>{{ "organizationUpgradeRequired" | i18n }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Card -->
|
<!-- Card -->
|
||||||
<div *ngIf="cipher.card">
|
<div *ngIf="cipher.card">
|
||||||
|
|||||||
@@ -157,4 +157,10 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
|||||||
this.messagingService.send("premiumRequired");
|
this.messagingService.send("premiumRequired");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upgradeOrganization() {
|
||||||
|
this.messagingService.send("upgradeOrganization", {
|
||||||
|
organizationId: this.cipher.organizationId,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
|||||||
protected messagingService: MessagingService,
|
protected messagingService: MessagingService,
|
||||||
eventCollectionService: EventCollectionService,
|
eventCollectionService: EventCollectionService,
|
||||||
protected policyService: PolicyService,
|
protected policyService: PolicyService,
|
||||||
organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
@@ -307,7 +307,8 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
|||||||
this.cipher.type === CipherType.Login &&
|
this.cipher.type === CipherType.Login &&
|
||||||
this.cipher.login.totp &&
|
this.cipher.login.totp &&
|
||||||
this.organization?.productTierType != ProductTierType.Free &&
|
this.organization?.productTierType != ProductTierType.Free &&
|
||||||
(this.cipher.organizationUseTotp || this.canAccessPremium)
|
((this.canAccessPremium && this.cipher.organizationId == null) ||
|
||||||
|
this.cipher.organizationUseTotp)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
protected policyService: PolicyService,
|
protected policyService: PolicyService,
|
||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
protected passwordRepromptService: PasswordRepromptService,
|
protected passwordRepromptService: PasswordRepromptService,
|
||||||
private organizationService: OrganizationService,
|
protected organizationService: OrganizationService,
|
||||||
protected dialogService: DialogService,
|
protected dialogService: DialogService,
|
||||||
protected win: Window,
|
protected win: Window,
|
||||||
protected datePipe: DatePipe,
|
protected datePipe: DatePipe,
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
|||||||
showPrivateKey: boolean;
|
showPrivateKey: boolean;
|
||||||
canAccessPremium: boolean;
|
canAccessPremium: boolean;
|
||||||
showPremiumRequiredTotp: boolean;
|
showPremiumRequiredTotp: boolean;
|
||||||
|
showUpgradeRequiredTotp: boolean;
|
||||||
totpCode: string;
|
totpCode: string;
|
||||||
totpCodeFormatted: string;
|
totpCodeFormatted: string;
|
||||||
totpDash: number;
|
totpDash: number;
|
||||||
@@ -151,22 +152,25 @@ export class ViewComponent implements OnDestroy, OnInit {
|
|||||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
|
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
|
||||||
);
|
);
|
||||||
this.showPremiumRequiredTotp =
|
this.showPremiumRequiredTotp =
|
||||||
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;
|
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationId;
|
||||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
|
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
|
||||||
this.collectionId as CollectionId,
|
this.collectionId as CollectionId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
this.showUpgradeRequiredTotp =
|
||||||
|
this.cipher.login.totp && this.cipher.organizationId && !this.cipher.organizationUseTotp;
|
||||||
|
|
||||||
if (this.cipher.folderId) {
|
if (this.cipher.folderId) {
|
||||||
this.folder = await (
|
this.folder = await (
|
||||||
await firstValueFrom(this.folderService.folderViews$(activeUserId))
|
await firstValueFrom(this.folderService.folderViews$(activeUserId))
|
||||||
).find((f) => f.id == this.cipher.folderId);
|
).find((f) => f.id == this.cipher.folderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const canGenerateTotp = this.cipher.organizationId
|
||||||
this.cipher.type === CipherType.Login &&
|
? this.cipher.organizationUseTotp
|
||||||
this.cipher.login.totp &&
|
: this.canAccessPremium;
|
||||||
(cipher.organizationUseTotp || this.canAccessPremium)
|
|
||||||
) {
|
if (this.cipher.type === CipherType.Login && this.cipher.login.totp && canGenerateTotp) {
|
||||||
await this.totpUpdateCode();
|
await this.totpUpdateCode();
|
||||||
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
|
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
|
||||||
await this.totpTick(interval);
|
await this.totpTick(interval);
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
<bit-label [appTextDrag]="totpCodeCopyObj?.totpCode"
|
<bit-label [appTextDrag]="totpCodeCopyObj?.totpCode"
|
||||||
>{{ "verificationCodeTotp" | i18n }}
|
>{{ "verificationCodeTotp" | i18n }}
|
||||||
<span
|
<span
|
||||||
*ngIf="!(isPremium$ | async)"
|
*ngIf="!(allowTotpGeneration$ | async)"
|
||||||
bitBadge
|
bitBadge
|
||||||
variant="success"
|
variant="success"
|
||||||
class="tw-ml-2 tw-cursor-pointer"
|
class="tw-ml-2 tw-cursor-pointer"
|
||||||
@@ -130,14 +130,14 @@
|
|||||||
id="totp"
|
id="totp"
|
||||||
readonly
|
readonly
|
||||||
bitInput
|
bitInput
|
||||||
[type]="!(isPremium$ | async) ? 'password' : 'text'"
|
[type]="!(allowTotpGeneration$ | async) ? 'password' : 'text'"
|
||||||
[value]="totpCodeCopyObj?.totpCodeFormatted || '*** ***'"
|
[value]="totpCodeCopyObj?.totpCodeFormatted || '*** ***'"
|
||||||
aria-readonly="true"
|
aria-readonly="true"
|
||||||
data-testid="login-totp"
|
data-testid="login-totp"
|
||||||
class="tw-font-mono"
|
class="tw-font-mono"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
*ngIf="isPremium$ | async"
|
*ngIf="allowTotpGeneration$ | async"
|
||||||
bitTotpCountdown
|
bitTotpCountdown
|
||||||
[cipher]="cipher"
|
[cipher]="cipher"
|
||||||
bitSuffix
|
bitSuffix
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
showToast
|
showToast
|
||||||
[appA11yTitle]="'copyVerificationCode' | i18n"
|
[appA11yTitle]="'copyVerificationCode' | i18n"
|
||||||
data-testid="copy-totp"
|
data-testid="copy-totp"
|
||||||
[disabled]="!(isPremium$ | async)"
|
[disabled]="!(allowTotpGeneration$ | async)"
|
||||||
class="disabled:tw-cursor-default"
|
class="disabled:tw-cursor-default"
|
||||||
></button>
|
></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|||||||
@@ -2,7 +2,15 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { CommonModule, DatePipe } from "@angular/common";
|
import { CommonModule, DatePipe } from "@angular/common";
|
||||||
import { Component, inject, Input } from "@angular/core";
|
import { Component, inject, Input } from "@angular/core";
|
||||||
import { Observable, switchMap } from "rxjs";
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest,
|
||||||
|
filter,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
shareReplay,
|
||||||
|
switchMap,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
@@ -12,13 +20,13 @@ import { EventType } from "@bitwarden/common/enums";
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import {
|
import {
|
||||||
|
BadgeModule,
|
||||||
|
ColorPasswordModule,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
|
IconButtonModule,
|
||||||
SectionComponent,
|
SectionComponent,
|
||||||
SectionHeaderComponent,
|
SectionHeaderComponent,
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
IconButtonModule,
|
|
||||||
BadgeModule,
|
|
||||||
ColorPasswordModule,
|
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
// FIXME: remove `src` and fix import
|
// FIXME: remove `src` and fix import
|
||||||
@@ -51,13 +59,31 @@ type TotpCodeValues = {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class LoginCredentialsViewComponent {
|
export class LoginCredentialsViewComponent {
|
||||||
@Input() cipher: CipherView;
|
@Input()
|
||||||
|
get cipher(): CipherView {
|
||||||
|
return this._cipher$.value;
|
||||||
|
}
|
||||||
|
set cipher(value: CipherView) {
|
||||||
|
this._cipher$.next(value);
|
||||||
|
}
|
||||||
|
private _cipher$ = new BehaviorSubject<CipherView>(null);
|
||||||
|
|
||||||
isPremium$: Observable<boolean> = this.accountService.activeAccount$.pipe(
|
private _userHasPremium$: Observable<boolean> = this.accountService.activeAccount$.pipe(
|
||||||
switchMap((account) =>
|
switchMap((account) =>
|
||||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
allowTotpGeneration$: Observable<boolean> = combineLatest([
|
||||||
|
this._userHasPremium$,
|
||||||
|
this._cipher$.pipe(filter((c) => c != null)),
|
||||||
|
]).pipe(
|
||||||
|
map(([userHasPremium, cipher]) => {
|
||||||
|
// User premium status only applies to personal ciphers, organizationUseTotp applies to organization ciphers
|
||||||
|
return (userHasPremium && cipher.organizationId == null) || cipher.organizationUseTotp;
|
||||||
|
}),
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
|
);
|
||||||
showPasswordCount: boolean = false;
|
showPasswordCount: boolean = false;
|
||||||
passwordRevealed: boolean = false;
|
passwordRevealed: boolean = false;
|
||||||
totpCodeCopyObj: TotpCodeValues;
|
totpCodeCopyObj: TotpCodeValues;
|
||||||
|
|||||||
Reference in New Issue
Block a user