Add shared two-factor-options component (#9767)
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.6 KiB |
|
@ -0,0 +1,31 @@
|
||||||
|
@import "variables.scss";
|
||||||
|
|
||||||
|
@each $mfaType in $mfaTypes {
|
||||||
|
.mfaType#{$mfaType} {
|
||||||
|
content: url("../images/two-factor/" + $mfaType + ".png");
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfaType1 {
|
||||||
|
@include themify($themes) {
|
||||||
|
content: url("../images/two-factor/1" + themed("mfaLogoSuffix"));
|
||||||
|
max-width: 100px;
|
||||||
|
max-height: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfaType7 {
|
||||||
|
@include themify($themes) {
|
||||||
|
content: url("../images/two-factor/7" + themed("mfaLogoSuffix"));
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-code-img {
|
||||||
|
@include themify($themes) {
|
||||||
|
content: url("../images/two-factor/rc" + themed("mfaLogoSuffix"));
|
||||||
|
max-width: 100px;
|
||||||
|
max-height: 45px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,5 +10,6 @@
|
||||||
@import "modal.scss";
|
@import "modal.scss";
|
||||||
@import "environment.scss";
|
@import "environment.scss";
|
||||||
@import "pages.scss";
|
@import "pages.scss";
|
||||||
|
@import "plugins.scss";
|
||||||
@import "@angular/cdk/overlay-prebuilt.css";
|
@import "@angular/cdk/overlay-prebuilt.css";
|
||||||
@import "../../../../../libs/components/src/multi-select/scss/bw.theme";
|
@import "../../../../../libs/components/src/multi-select/scss/bw.theme";
|
||||||
|
|
|
@ -19,6 +19,8 @@ $border-radius: 6px;
|
||||||
$line-height-base: 1.42857143;
|
$line-height-base: 1.42857143;
|
||||||
$icon-hover-color: lighten($text-color, 50%);
|
$icon-hover-color: lighten($text-color, 50%);
|
||||||
|
|
||||||
|
$mfaTypes: 0, 2, 3, 4, 6;
|
||||||
|
|
||||||
$gray: #555;
|
$gray: #555;
|
||||||
$gray-light: #777;
|
$gray-light: #777;
|
||||||
$text-muted: $gray-light;
|
$text-muted: $gray-light;
|
||||||
|
@ -111,6 +113,7 @@ $themes: (
|
||||||
infoColor: $brand-info,
|
infoColor: $brand-info,
|
||||||
warningColor: $brand-warning,
|
warningColor: $brand-warning,
|
||||||
logoSuffix: "dark",
|
logoSuffix: "dark",
|
||||||
|
mfaLogoSuffix: ".png",
|
||||||
passwordNumberColor: #007fde,
|
passwordNumberColor: #007fde,
|
||||||
passwordSpecialColor: #c40800,
|
passwordSpecialColor: #c40800,
|
||||||
passwordCountText: #212529,
|
passwordCountText: #212529,
|
||||||
|
@ -174,6 +177,7 @@ $themes: (
|
||||||
infoColor: #a4b0c6,
|
infoColor: #a4b0c6,
|
||||||
warningColor: #ffeb66,
|
warningColor: #ffeb66,
|
||||||
logoSuffix: "white",
|
logoSuffix: "white",
|
||||||
|
mfaLogoSuffix: "-w.png",
|
||||||
passwordNumberColor: #6f9df1,
|
passwordNumberColor: #6f9df1,
|
||||||
passwordSpecialColor: #ff8d85,
|
passwordSpecialColor: #ff8d85,
|
||||||
passwordCountText: #ffffff,
|
passwordCountText: #ffffff,
|
||||||
|
@ -236,6 +240,7 @@ $themes: (
|
||||||
infoColor: $nord9,
|
infoColor: $nord9,
|
||||||
warningColor: $nord12,
|
warningColor: $nord12,
|
||||||
logoSuffix: "white",
|
logoSuffix: "white",
|
||||||
|
mfaLogoSuffix: "-w.png",
|
||||||
passwordNumberColor: $nord8,
|
passwordNumberColor: $nord8,
|
||||||
passwordSpecialColor: $nord12,
|
passwordSpecialColor: $nord12,
|
||||||
passwordCountText: $nord5,
|
passwordCountText: $nord5,
|
||||||
|
@ -298,6 +303,7 @@ $themes: (
|
||||||
infoColor: $solarizedDarkGreen,
|
infoColor: $solarizedDarkGreen,
|
||||||
warningColor: $solarizedDarkYellow,
|
warningColor: $solarizedDarkYellow,
|
||||||
logoSuffix: "white",
|
logoSuffix: "white",
|
||||||
|
mfaLogoSuffix: "-w.png",
|
||||||
passwordNumberColor: $solarizedDarkCyan,
|
passwordNumberColor: $solarizedDarkCyan,
|
||||||
passwordSpecialColor: $solarizedDarkYellow,
|
passwordSpecialColor: $solarizedDarkYellow,
|
||||||
passwordCountText: $solarizedDarkBase2,
|
passwordCountText: $solarizedDarkBase2,
|
||||||
|
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.6 KiB |
|
@ -0,0 +1,31 @@
|
||||||
|
@import "variables.scss";
|
||||||
|
|
||||||
|
@each $mfaType in $mfaTypes {
|
||||||
|
.mfaType#{$mfaType} {
|
||||||
|
content: url("../images/two-factor/" + $mfaType + ".png");
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfaType1 {
|
||||||
|
@include themify($themes) {
|
||||||
|
content: url("../images/two-factor/1" + themed("mfaLogoSuffix"));
|
||||||
|
max-width: 100px;
|
||||||
|
max-height: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mfaType7 {
|
||||||
|
@include themify($themes) {
|
||||||
|
content: url("../images/two-factor/7" + themed("mfaLogoSuffix"));
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-code-img {
|
||||||
|
@include themify($themes) {
|
||||||
|
content: url("../images/two-factor/rc" + themed("mfaLogoSuffix"));
|
||||||
|
max-width: 100px;
|
||||||
|
max-height: 45px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,5 +15,6 @@
|
||||||
@import "header.scss";
|
@import "header.scss";
|
||||||
@import "left-nav.scss";
|
@import "left-nav.scss";
|
||||||
@import "loading.scss";
|
@import "loading.scss";
|
||||||
|
@import "plugins.scss";
|
||||||
@import "../../../../libs/angular/src/scss/icons.scss";
|
@import "../../../../libs/angular/src/scss/icons.scss";
|
||||||
@import "../../../../libs/components/src/multi-select/scss/bw.theme";
|
@import "../../../../libs/components/src/multi-select/scss/bw.theme";
|
||||||
|
|
|
@ -15,6 +15,8 @@ $list-icon-color: #c7c7cd;
|
||||||
$border-radius: 3px;
|
$border-radius: 3px;
|
||||||
$line-height-base: 1.42857143;
|
$line-height-base: 1.42857143;
|
||||||
|
|
||||||
|
$mfaTypes: 0, 2, 3, 4, 6;
|
||||||
|
|
||||||
$gray: #555;
|
$gray: #555;
|
||||||
$gray-light: #777;
|
$gray-light: #777;
|
||||||
$text-muted: $gray-light;
|
$text-muted: $gray-light;
|
||||||
|
@ -92,6 +94,7 @@ $themes: (
|
||||||
infoColor: $brand-info,
|
infoColor: $brand-info,
|
||||||
warningColor: $brand-warning,
|
warningColor: $brand-warning,
|
||||||
logoSuffix: "dark",
|
logoSuffix: "dark",
|
||||||
|
mfaLogoSuffix: ".png",
|
||||||
passwordNumberColor: #007fde,
|
passwordNumberColor: #007fde,
|
||||||
passwordSpecialColor: #c40800,
|
passwordSpecialColor: #c40800,
|
||||||
passwordCountText: #212529,
|
passwordCountText: #212529,
|
||||||
|
@ -150,6 +153,7 @@ $themes: (
|
||||||
infoColor: #a4b0c6,
|
infoColor: #a4b0c6,
|
||||||
warningColor: #ffeb66,
|
warningColor: #ffeb66,
|
||||||
logoSuffix: "white",
|
logoSuffix: "white",
|
||||||
|
mfaLogoSuffix: "-w.png",
|
||||||
passwordNumberColor: #6f9df1,
|
passwordNumberColor: #6f9df1,
|
||||||
passwordSpecialColor: #ff8d85,
|
passwordSpecialColor: #ff8d85,
|
||||||
passwordCountText: #ffffff,
|
passwordCountText: #ffffff,
|
||||||
|
@ -208,6 +212,7 @@ $themes: (
|
||||||
infoColor: $nord9,
|
infoColor: $nord9,
|
||||||
warningColor: $nord12,
|
warningColor: $nord12,
|
||||||
logoSuffix: "white",
|
logoSuffix: "white",
|
||||||
|
mfaLogoSuffix: "-w.png",
|
||||||
passwordNumberColor: $nord8,
|
passwordNumberColor: $nord8,
|
||||||
passwordSpecialColor: $nord12,
|
passwordSpecialColor: $nord12,
|
||||||
passwordCountText: $nord5,
|
passwordCountText: $nord5,
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
<bit-dialog dialogSize="large">
|
||||||
|
<span bitDialogTitle>
|
||||||
|
{{ "twoStepOptions" | i18n }}
|
||||||
|
</span>
|
||||||
|
<ng-container bitDialogContent>
|
||||||
|
<div *ngFor="let p of providers" class="tw-m-2">
|
||||||
|
<div class="tw-flex tw-items-center tw-justify-center tw-gap-4">
|
||||||
|
<div
|
||||||
|
class="tw-flex tw-items-center tw-justify-center tw-min-w-[100px]"
|
||||||
|
*ngIf="!areIconsDisabled"
|
||||||
|
>
|
||||||
|
<img [class]="'mfaType' + p.type" [alt]="p.name + ' logo'" />
|
||||||
|
</div>
|
||||||
|
<div class="tw-flex-1">
|
||||||
|
<h3 bitTypography="h3">{{ p.name }}</h3>
|
||||||
|
<p bitTypography="body1">{{ p.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="tw-min-w-20">
|
||||||
|
<button bitButton type="button" buttonType="secondary" (click)="choose(p)">
|
||||||
|
{{ "select" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
<div class="tw-m-2" (click)="recover()">
|
||||||
|
<div class="tw-flex tw-items-center tw-justify-center tw-gap-4">
|
||||||
|
<div
|
||||||
|
class="tw-flex tw-items-center tw-justify-center tw-min-w-[100px]"
|
||||||
|
*ngIf="!areIconsDisabled"
|
||||||
|
>
|
||||||
|
<img class="recovery-code-img" alt="rc logo" />
|
||||||
|
</div>
|
||||||
|
<div class="tw-flex-1">
|
||||||
|
<h3 bitTypography="h3">{{ "recoveryCodeTitle" | i18n }}</h3>
|
||||||
|
<p bitTypography="body1">{{ "recoveryCodeDesc" | i18n }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="tw-min-w-20">
|
||||||
|
<button bitButton type="button" buttonType="secondary" (click)="recover()">
|
||||||
|
{{ "select" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button bitButton type="button" buttonType="secondary" bitDialogClose>
|
||||||
|
{{ "close" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, EventEmitter, OnInit, Output } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
|
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||||
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
export enum TwoFactorOptionsDialogResult {
|
||||||
|
Provider = "Provider selected",
|
||||||
|
Recover = "Recover selected",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TwoFactorOptionsDialogResultType = {
|
||||||
|
result: TwoFactorOptionsDialogResult;
|
||||||
|
type: TwoFactorProviderType;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "app-two-factor-options",
|
||||||
|
templateUrl: "two-factor-options.component.html",
|
||||||
|
imports: [CommonModule, JslibModule, DialogModule, ButtonModule, TypographyModule],
|
||||||
|
providers: [I18nPipe],
|
||||||
|
})
|
||||||
|
export class TwoFactorOptionsComponent implements OnInit {
|
||||||
|
@Output() onProviderSelected = new EventEmitter<TwoFactorProviderType>();
|
||||||
|
@Output() onRecoverSelected = new EventEmitter();
|
||||||
|
|
||||||
|
providers: any[] = [];
|
||||||
|
|
||||||
|
// todo: remove after porting to two-factor-options-v2
|
||||||
|
// icons cause the layout to break on browser extensions
|
||||||
|
areIconsDisabled = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private twoFactorService: TwoFactorService,
|
||||||
|
private environmentService: EnvironmentService,
|
||||||
|
private dialogRef: DialogRef,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
) {
|
||||||
|
// todo: remove after porting to two-factor-options-v2
|
||||||
|
if (this.platformUtilsService.getClientType() == ClientType.Browser) {
|
||||||
|
this.areIconsDisabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.providers = await this.twoFactorService.getSupportedProviders(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
async choose(p: any) {
|
||||||
|
this.onProviderSelected.emit(p.type);
|
||||||
|
this.dialogRef.close({ result: TwoFactorOptionsDialogResult.Provider, type: p.type });
|
||||||
|
}
|
||||||
|
|
||||||
|
async recover() {
|
||||||
|
const env = await firstValueFrom(this.environmentService.environment$);
|
||||||
|
const webVault = env.getWebVaultUrl();
|
||||||
|
this.platformUtilsService.launchUri(webVault + "/#/recover-2fa");
|
||||||
|
this.onRecoverSelected.emit();
|
||||||
|
this.dialogRef.close({ result: TwoFactorOptionsDialogResult.Recover });
|
||||||
|
}
|
||||||
|
|
||||||
|
static open(dialogService: DialogService) {
|
||||||
|
return dialogService.open<TwoFactorOptionsDialogResultType>(TwoFactorOptionsComponent);
|
||||||
|
}
|
||||||
|
}
|