mirror of
https://github.com/bitwarden/browser
synced 2026-02-17 18:09:17 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -100,7 +100,7 @@ export class CollectionsComponent implements OnInit {
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("editedItem"));
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { PasswordColorText } from "../../tools/password-strength/password-strength.component";
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
|
||||
@@ -8,15 +8,14 @@ import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
PasswordLoginCredentials,
|
||||
RegisterRouteService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -24,7 +23,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import {
|
||||
AllValidationErrors,
|
||||
@@ -63,7 +62,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
protected twoFactorRoute = "2fa";
|
||||
protected successRoute = "vault";
|
||||
// TODO: remove when email verification flag is removed
|
||||
protected registerRoute = "/register";
|
||||
protected registerRoute$ = this.registerRouteService.registerRoute$();
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
|
||||
protected destroy$ = new Subject<void>();
|
||||
@@ -91,21 +90,12 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
protected loginEmailService: LoginEmailServiceAbstraction,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected webAuthnLoginService: WebAuthnLoginServiceAbstraction,
|
||||
protected configService: ConfigService,
|
||||
protected registerRouteService: RegisterRouteService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// TODO: remove when email verification flag is removed
|
||||
const emailVerification = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.EmailVerification,
|
||||
);
|
||||
|
||||
if (emailVerification) {
|
||||
this.registerRoute = "/signup";
|
||||
}
|
||||
|
||||
this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
||||
if (!params) {
|
||||
return;
|
||||
@@ -120,17 +110,8 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
});
|
||||
|
||||
if (!this.paramEmailSet) {
|
||||
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
|
||||
this.formGroup.controls.email.setValue(storedEmail ?? "");
|
||||
await this.loadEmailSettings();
|
||||
}
|
||||
|
||||
let rememberEmail = this.loginEmailService.getRememberEmail();
|
||||
|
||||
if (rememberEmail == null) {
|
||||
rememberEmail = (await firstValueFrom(this.loginEmailService.storedEmail$)) != null;
|
||||
}
|
||||
|
||||
this.formGroup.controls.rememberEmail.setValue(rememberEmail);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -168,8 +149,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
this.formPromise = this.loginStrategyService.logIn(credentials);
|
||||
const response = await this.formPromise;
|
||||
|
||||
this.setLoginEmailValues();
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
await this.saveEmailSettings();
|
||||
|
||||
if (this.handleCaptchaRequired(response)) {
|
||||
return;
|
||||
@@ -191,6 +171,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulLoginForceResetNavigate();
|
||||
} else {
|
||||
this.loginEmailService.clearValues();
|
||||
// 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([this.forcePasswordResetRoute]);
|
||||
@@ -206,6 +187,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulLoginNavigate();
|
||||
} else {
|
||||
this.loginEmailService.clearValues();
|
||||
// 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([this.successRoute]);
|
||||
@@ -235,12 +217,14 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
return;
|
||||
}
|
||||
|
||||
this.setLoginEmailValues();
|
||||
await this.saveEmailSettings();
|
||||
await this.router.navigate(["/login-with-device"]);
|
||||
}
|
||||
|
||||
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
||||
await this.saveEmailSettings();
|
||||
// Save off email for SSO
|
||||
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
||||
|
||||
// Generate necessary sso params
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
@@ -311,17 +295,28 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
|
||||
}
|
||||
}
|
||||
|
||||
setLoginEmailValues() {
|
||||
this.loginEmailService.setEmail(this.formGroup.value.email);
|
||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
||||
private async loadEmailSettings() {
|
||||
// Try to load from memory first
|
||||
const email = this.loginEmailService.getEmail();
|
||||
const rememberEmail = this.loginEmailService.getRememberEmail();
|
||||
if (email) {
|
||||
this.formGroup.controls.email.setValue(email);
|
||||
this.formGroup.controls.rememberEmail.setValue(rememberEmail);
|
||||
} else {
|
||||
// If not in memory, check email on disk
|
||||
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
|
||||
if (storedEmail) {
|
||||
// If we have a stored email, rememberEmail should default to true
|
||||
this.formGroup.controls.email.setValue(storedEmail);
|
||||
this.formGroup.controls.rememberEmail.setValue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async saveEmailSettings() {
|
||||
this.setLoginEmailValues();
|
||||
protected async saveEmailSettings() {
|
||||
this.loginEmailService.setEmail(this.formGroup.value.email);
|
||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
|
||||
// Save off email for SSO
|
||||
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
||||
}
|
||||
|
||||
// Legacy accounts used the master key to encrypt data. Migration is required
|
||||
|
||||
@@ -17,8 +17,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import {
|
||||
AllValidationErrors,
|
||||
|
||||
@@ -28,11 +28,11 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { SsoComponent } from "./sso.component";
|
||||
// test component that extends the SsoComponent
|
||||
@@ -201,7 +201,10 @@ describe("SsoComponent", () => {
|
||||
{ provide: ApiService, useValue: mockApiService },
|
||||
{ provide: CryptoFunctionService, useValue: mockCryptoFunctionService },
|
||||
{ provide: EnvironmentService, useValue: mockEnvironmentService },
|
||||
{ provide: PasswordGenerationServiceAbstraction, useValue: mockPasswordGenerationService },
|
||||
{
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
useValue: mockPasswordGenerationService,
|
||||
},
|
||||
|
||||
{
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
|
||||
@@ -25,7 +25,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@Directive()
|
||||
export class SsoComponent {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<ng-container>
|
||||
<p bitTypography="body1">
|
||||
{{ "enterVerificationCodeApp" | i18n }}
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "verificationCode" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
[(ngModel)]="tokenValue"
|
||||
(input)="token.emit(tokenValue)"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DialogModule } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import {
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-two-factor-auth-authenticator",
|
||||
templateUrl: "two-factor-auth-authenticator.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
DialogModule,
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
FormsModule,
|
||||
],
|
||||
providers: [I18nPipe],
|
||||
})
|
||||
export class TwoFactorAuthAuthenticatorComponent {
|
||||
tokenValue: string;
|
||||
@Output() token = new EventEmitter<string>();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "insertYubiKey" | i18n }}</p>
|
||||
<picture>
|
||||
<source srcset="../../images/yubikey.avif" type="image/avif" />
|
||||
<source srcset="../../images/yubikey.webp" type="image/webp" />
|
||||
<img src="../../images/yubikey.jpg" class="tw-rounded img-fluid tw-mb-3" alt="" />
|
||||
</picture>
|
||||
<bit-form-field>
|
||||
<bit-label class="tw-sr-only">{{ "verificationCode" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
bitInput
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
[(ngModel)]="tokenValue"
|
||||
(input)="token.emit(tokenValue)"
|
||||
/>
|
||||
</bit-form-field>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DialogModule } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import {
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-two-factor-auth-yubikey",
|
||||
templateUrl: "two-factor-auth-yubikey.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
DialogModule,
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
FormsModule,
|
||||
],
|
||||
providers: [I18nPipe],
|
||||
})
|
||||
export class TwoFactorAuthYubikeyComponent {
|
||||
tokenValue: string = "";
|
||||
@Output() token = new EventEmitter<string>();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<form [bitSubmit]="submitForm" [formGroup]="formGroup" autocomplete="off">
|
||||
<div class="tw-min-w-96">
|
||||
<app-two-factor-auth-authenticator
|
||||
(token)="token = $event"
|
||||
*ngIf="selectedProviderType === providerType.Authenticator"
|
||||
/>
|
||||
<app-two-factor-auth-yubikey
|
||||
(token)="token = $event"
|
||||
*ngIf="selectedProviderType === providerType.Yubikey"
|
||||
/>
|
||||
<bit-form-control *ngIf="selectedProviderType != null">
|
||||
<bit-label>{{ "rememberMe" | i18n }}</bit-label>
|
||||
<input type="checkbox" bitCheckbox formControlName="remember" />
|
||||
</bit-form-control>
|
||||
<ng-container *ngIf="selectedProviderType == null">
|
||||
<p bitTypography="body1">{{ "noTwoStepProviders" | i18n }}</p>
|
||||
<p bitTypography="body1">{{ "noTwoStepProviders2" | i18n }}</p>
|
||||
</ng-container>
|
||||
<hr />
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
<!-- <!-- Buttons -->
|
||||
<div class="tw-flex tw-flex-col tw-space-y-2.5 tw-mb-3">
|
||||
<button
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
*ngIf="selectedProviderType != null"
|
||||
>
|
||||
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ actionButtonText }} </span>
|
||||
</button>
|
||||
<a routerLink="/login" bitButton buttonType="secondary">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<a bitLink href="#" appStopClick (click)="selectOtherTwofactorMethod()">{{
|
||||
"useAnotherTwoStepMethod" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,502 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, convertToParamMap, Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
|
||||
FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption,
|
||||
FakeUserDecryptionOptions as UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { TwoFactorAuthComponent } from "./two-factor-auth.component";
|
||||
|
||||
// test component that extends the TwoFactorAuthComponent
|
||||
@Component({})
|
||||
class TestTwoFactorComponent extends TwoFactorAuthComponent {}
|
||||
|
||||
interface TwoFactorComponentProtected {
|
||||
trustedDeviceEncRoute: string;
|
||||
changePasswordRoute: string;
|
||||
forcePasswordResetRoute: string;
|
||||
successRoute: string;
|
||||
}
|
||||
|
||||
describe("TwoFactorComponent", () => {
|
||||
let component: TestTwoFactorComponent;
|
||||
let _component: TwoFactorComponentProtected;
|
||||
|
||||
let fixture: ComponentFixture<TestTwoFactorComponent>;
|
||||
const userId = "userId" as UserId;
|
||||
|
||||
// Mock Services
|
||||
let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
|
||||
let mockRouter: MockProxy<Router>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let mockApiService: MockProxy<ApiService>;
|
||||
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let mockWin: MockProxy<Window>;
|
||||
let mockEnvironmentService: MockProxy<EnvironmentService>;
|
||||
let mockStateService: MockProxy<StateService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockTwoFactorService: MockProxy<TwoFactorService>;
|
||||
let mockAppIdService: MockProxy<AppIdService>;
|
||||
let mockLoginEmailService: MockProxy<LoginEmailServiceAbstraction>;
|
||||
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
let mockMasterPasswordService: FakeMasterPasswordService;
|
||||
let mockAccountService: FakeAccountService;
|
||||
let mockDialogService: MockProxy<DialogService>;
|
||||
|
||||
let mockUserDecryptionOpts: {
|
||||
noMasterPassword: UserDecryptionOptions;
|
||||
withMasterPassword: UserDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDevice: UserDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
|
||||
withMasterPasswordAndKeyConnector: UserDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDevice: UserDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
|
||||
noMasterPasswordWithKeyConnector: UserDecryptionOptions;
|
||||
};
|
||||
|
||||
let selectedUserDecryptionOptions: BehaviorSubject<UserDecryptionOptions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
|
||||
mockRouter = mock<Router>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
mockApiService = mock<ApiService>();
|
||||
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
mockWin = mock<Window>();
|
||||
const mockEnvironment = mock<Environment>();
|
||||
mockEnvironment.getWebVaultUrl.mockReturnValue("http://example.com");
|
||||
mockEnvironmentService = mock<EnvironmentService>();
|
||||
mockEnvironmentService.environment$ = new BehaviorSubject(mockEnvironment);
|
||||
|
||||
mockStateService = mock<StateService>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockTwoFactorService = mock<TwoFactorService>();
|
||||
mockAppIdService = mock<AppIdService>();
|
||||
mockLoginEmailService = mock<LoginEmailServiceAbstraction>();
|
||||
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
mockConfigService = mock<ConfigService>();
|
||||
mockAccountService = mockAccountServiceWith(userId);
|
||||
mockMasterPasswordService = new FakeMasterPasswordService();
|
||||
mockDialogService = mock<DialogService>();
|
||||
|
||||
mockUserDecryptionOpts = {
|
||||
noMasterPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||
}),
|
||||
noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
noMasterPasswordWithKeyConnector: new UserDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||
}),
|
||||
};
|
||||
|
||||
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
|
||||
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestTwoFactorComponent],
|
||||
providers: [
|
||||
{ provide: LoginStrategyServiceAbstraction, useValue: mockLoginStrategyService },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: ApiService, useValue: mockApiService },
|
||||
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||
{ provide: WINDOW, useValue: mockWin },
|
||||
{ provide: EnvironmentService, useValue: mockEnvironmentService },
|
||||
{ provide: StateService, useValue: mockStateService },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
snapshot: {
|
||||
// Default to standard 2FA flow - not SSO + 2FA
|
||||
queryParamMap: convertToParamMap({ sso: "false" }),
|
||||
},
|
||||
},
|
||||
},
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: TwoFactorService, useValue: mockTwoFactorService },
|
||||
{ provide: AppIdService, useValue: mockAppIdService },
|
||||
{ provide: LoginEmailServiceAbstraction, useValue: mockLoginEmailService },
|
||||
{
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
useValue: mockUserDecryptionOptionsService,
|
||||
},
|
||||
{ provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: DialogService, useValue: mockDialogService },
|
||||
],
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(TestTwoFactorComponent);
|
||||
component = fixture.componentInstance;
|
||||
_component = component as any;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset all mocks after each test
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
// Shared tests
|
||||
const testChangePasswordOnSuccessfulLogin = () => {
|
||||
it("navigates to the component's defined change password route when user doesn't have a MP and key connector isn't enabled", async () => {
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
// Assert
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.changePasswordRoute], {
|
||||
queryParams: {
|
||||
identifier: component.orgIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const testForceResetOnSuccessfulLogin = (reasonString: string) => {
|
||||
it(`navigates to the component's defined forcePasswordResetRoute route when response.forcePasswordReset is ${reasonString}`, async () => {
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
// expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.forcePasswordResetRoute], {
|
||||
queryParams: {
|
||||
identifier: component.orgIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe("Standard 2FA scenarios", () => {
|
||||
describe("submit", () => {
|
||||
const token = "testToken";
|
||||
const remember = false;
|
||||
const captchaToken = "testCaptchaToken";
|
||||
|
||||
beforeEach(() => {
|
||||
component.token = token;
|
||||
component.remember = remember;
|
||||
component.captchaToken = captchaToken;
|
||||
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
||||
});
|
||||
|
||||
it("calls authService.logInTwoFactor with correct parameters when form is submitted", async () => {
|
||||
// Arrange
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
// Assert
|
||||
expect(mockLoginStrategyService.logInTwoFactor).toHaveBeenCalledWith(
|
||||
new TokenTwoFactorRequest(component.selectedProviderType, token, remember),
|
||||
captchaToken,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return when handleCaptchaRequired returns true", async () => {
|
||||
// Arrange
|
||||
const captchaSiteKey = "testCaptchaSiteKey";
|
||||
const authResult = new AuthResult();
|
||||
authResult.captchaSiteKey = captchaSiteKey;
|
||||
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
|
||||
// Note: the any casts are required b/c typescript cant recognize that
|
||||
// handleCaptureRequired is a method on TwoFactorComponent b/c it is inherited
|
||||
// from the CaptchaProtectedComponent
|
||||
const handleCaptchaRequiredSpy = jest
|
||||
.spyOn<any, any>(component, "handleCaptchaRequired")
|
||||
.mockReturnValue(true);
|
||||
|
||||
// Act
|
||||
const result = await component.submit();
|
||||
|
||||
// Assert
|
||||
expect(handleCaptchaRequiredSpy).toHaveBeenCalled();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLogin when defined", async () => {
|
||||
// Arrange
|
||||
component.onSuccessfulLogin = jest.fn().mockResolvedValue(undefined);
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
// Assert
|
||||
expect(component.onSuccessfulLogin).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls loginEmailService.clearValues() when login is successful", async () => {
|
||||
// Arrange
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
// spy on loginEmailService.clearValues
|
||||
const clearValuesSpy = jest.spyOn(mockLoginEmailService, "clearValues");
|
||||
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
// Assert
|
||||
expect(clearValuesSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("Set Master Password scenarios", () => {
|
||||
beforeEach(() => {
|
||||
const authResult = new AuthResult();
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
describe("Given user needs to set a master password", () => {
|
||||
beforeEach(() => {
|
||||
// Only need to test the case where the user has no master password to test the primary change mp flow here
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
|
||||
});
|
||||
|
||||
testChangePasswordOnSuccessfulLogin();
|
||||
});
|
||||
|
||||
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
|
||||
);
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalledWith([_component.changePasswordRoute], {
|
||||
queryParams: {
|
||||
identifier: component.orgIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Force Master Password Reset scenarios", () => {
|
||||
[
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
].forEach((forceResetPasswordReason) => {
|
||||
const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
|
||||
|
||||
beforeEach(() => {
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = forceResetPasswordReason;
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
testForceResetOnSuccessfulLogin(reasonString);
|
||||
});
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLoginNavigate when the callback is defined", async () => {
|
||||
// Arrange
|
||||
component.onSuccessfulLoginNavigate = jest.fn().mockResolvedValue(undefined);
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
// Assert
|
||||
expect(component.onSuccessfulLoginNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("navigates to the component's defined success route when the login is successful and onSuccessfulLoginNavigate is undefined", async () => {
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
// Assert
|
||||
expect(component.onSuccessfulLoginNavigate).not.toBeDefined();
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.successRoute], undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("SSO > 2FA scenarios", () => {
|
||||
beforeEach(() => {
|
||||
const mockActivatedRoute = TestBed.inject(ActivatedRoute);
|
||||
mockActivatedRoute.snapshot.queryParamMap.get = jest.fn().mockReturnValue("true");
|
||||
});
|
||||
|
||||
describe("submit", () => {
|
||||
const token = "testToken";
|
||||
const remember = false;
|
||||
const captchaToken = "testCaptchaToken";
|
||||
|
||||
beforeEach(() => {
|
||||
component.token = token;
|
||||
component.remember = remember;
|
||||
component.captchaToken = captchaToken;
|
||||
});
|
||||
|
||||
describe("Trusted Device Encryption scenarios", () => {
|
||||
beforeEach(() => {
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
|
||||
beforeEach(() => {
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
|
||||
);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
it("navigates to the component's defined trusted device encryption route and sets correct flag when user doesn't have a MP and key connector isn't enabled", async () => {
|
||||
// Act
|
||||
await component.submit();
|
||||
|
||||
// Assert
|
||||
expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
userId,
|
||||
);
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(
|
||||
[_component.trustedDeviceEncRoute],
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is required", () => {
|
||||
[
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
].forEach((forceResetPasswordReason) => {
|
||||
const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
|
||||
|
||||
beforeEach(() => {
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||
);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = forceResetPasswordReason;
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
testForceResetOnSuccessfulLogin(reasonString);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
|
||||
let authResult;
|
||||
beforeEach(() => {
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||
);
|
||||
|
||||
authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = ForceSetPasswordReason.None;
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
it("navigates to the component's defined trusted device encryption route when login is successful and onSuccessfulLoginTdeNavigate is undefined", async () => {
|
||||
await component.submit();
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(
|
||||
[_component.trustedDeviceEncRoute],
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLoginTdeNavigate instead of router.navigate when the callback is defined", async () => {
|
||||
component.onSuccessfulLoginTdeNavigate = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
expect(component.onSuccessfulLoginTdeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,396 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, NavigationExtras, Router, RouterLink } from "@angular/router";
|
||||
import { Subject, takeUntil, lastValueFrom, first, firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
TrustedDeviceUserDecryptionOption,
|
||||
UserDecryptionOptions,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { CaptchaProtectedComponent } from "../captcha-protected.component";
|
||||
|
||||
import { TwoFactorAuthAuthenticatorComponent } from "./two-factor-auth-authenticator.component";
|
||||
import { TwoFactorAuthYubikeyComponent } from "./two-factor-auth-yubikey.component";
|
||||
import {
|
||||
TwoFactorOptionsDialogResult,
|
||||
TwoFactorOptionsComponent,
|
||||
TwoFactorOptionsDialogResultType,
|
||||
} from "./two-factor-options.component";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-two-factor-auth",
|
||||
templateUrl: "two-factor-auth.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
RouterLink,
|
||||
ButtonModule,
|
||||
TwoFactorOptionsComponent,
|
||||
TwoFactorAuthAuthenticatorComponent,
|
||||
TwoFactorAuthYubikeyComponent,
|
||||
],
|
||||
providers: [I18nPipe],
|
||||
})
|
||||
export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements OnInit {
|
||||
token = "";
|
||||
remember = false;
|
||||
orgIdentifier: string = null;
|
||||
|
||||
providers = TwoFactorProviders;
|
||||
providerType = TwoFactorProviderType;
|
||||
selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator;
|
||||
providerData: any;
|
||||
|
||||
formGroup = this.formBuilder.group({
|
||||
token: [
|
||||
"",
|
||||
{
|
||||
validators: [Validators.required],
|
||||
updateOn: "submit",
|
||||
},
|
||||
],
|
||||
remember: [false],
|
||||
});
|
||||
actionButtonText = "";
|
||||
title = "";
|
||||
formPromise: Promise<any>;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
onSuccessfulLogin: () => Promise<void>;
|
||||
onSuccessfulLoginNavigate: () => Promise<void>;
|
||||
|
||||
onSuccessfulLoginTde: () => Promise<void>;
|
||||
onSuccessfulLoginTdeNavigate: () => Promise<void>;
|
||||
|
||||
submitForm = async () => {
|
||||
await this.submit();
|
||||
};
|
||||
goAfterLogIn = async () => {
|
||||
this.loginEmailService.clearValues();
|
||||
// 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([this.successRoute], {
|
||||
queryParams: {
|
||||
identifier: this.orgIdentifier,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
protected loginRoute = "login";
|
||||
|
||||
protected trustedDeviceEncRoute = "login-initiated";
|
||||
protected changePasswordRoute = "set-password";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
protected successRoute = "vault";
|
||||
|
||||
constructor(
|
||||
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
protected router: Router,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
environmentService: EnvironmentService,
|
||||
private dialogService: DialogService,
|
||||
protected route: ActivatedRoute,
|
||||
private logService: LogService,
|
||||
protected twoFactorService: TwoFactorService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected configService: ConfigService,
|
||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
private formBuilder: FormBuilder,
|
||||
@Inject(WINDOW) protected win: Window,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (!(await this.authing()) || (await this.twoFactorService.getProviders()) == null) {
|
||||
// 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([this.loginRoute]);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.route.queryParams.pipe(first()).subscribe((qParams) => {
|
||||
if (qParams.identifier != null) {
|
||||
this.orgIdentifier = qParams.identifier;
|
||||
}
|
||||
});
|
||||
|
||||
if (await this.needsLock()) {
|
||||
this.successRoute = "lock";
|
||||
}
|
||||
|
||||
const webAuthnSupported = this.platformUtilsService.supportsWebAuthn(this.win);
|
||||
this.selectedProviderType = await this.twoFactorService.getDefaultProvider(webAuthnSupported);
|
||||
const providerData = await this.twoFactorService.getProviders().then((providers) => {
|
||||
return providers.get(this.selectedProviderType);
|
||||
});
|
||||
this.providerData = providerData;
|
||||
await this.updateUIToProviderData();
|
||||
|
||||
this.actionButtonText = this.i18nService.t("continue");
|
||||
this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||
this.token = value.token;
|
||||
this.remember = value.remember;
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
await this.setupCaptcha();
|
||||
|
||||
if (this.token == null || this.token === "") {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("verificationCodeRequired"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.loginStrategyService.logInTwoFactor(
|
||||
new TokenTwoFactorRequest(this.selectedProviderType, this.token, this.remember),
|
||||
this.captchaToken,
|
||||
);
|
||||
const authResult: AuthResult = await this.formPromise;
|
||||
this.logService.info("Successfully submitted two factor token");
|
||||
await this.handleLoginResponse(authResult);
|
||||
} catch {
|
||||
this.logService.error("Error submitting two factor token");
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("invalidVerificationCode"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async selectOtherTwofactorMethod() {
|
||||
const dialogRef = TwoFactorOptionsComponent.open(this.dialogService);
|
||||
const response: TwoFactorOptionsDialogResultType = await lastValueFrom(dialogRef.closed);
|
||||
if (response.result === TwoFactorOptionsDialogResult.Provider) {
|
||||
const providerData = await this.twoFactorService.getProviders().then((providers) => {
|
||||
return providers.get(response.type);
|
||||
});
|
||||
this.providerData = providerData;
|
||||
this.selectedProviderType = response.type;
|
||||
await this.updateUIToProviderData();
|
||||
}
|
||||
}
|
||||
|
||||
protected handleMigrateEncryptionKey(result: AuthResult): boolean {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
// 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(["migrate-legacy-encryption"]);
|
||||
return true;
|
||||
}
|
||||
|
||||
async updateUIToProviderData() {
|
||||
if (this.selectedProviderType == null) {
|
||||
this.title = this.i18nService.t("loginUnavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
|
||||
}
|
||||
|
||||
private async handleLoginResponse(authResult: AuthResult) {
|
||||
if (this.handleCaptchaRequired(authResult)) {
|
||||
return;
|
||||
} else if (this.handleMigrateEncryptionKey(authResult)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save off the OrgSsoIdentifier for use in the TDE flows
|
||||
// - TDE login decryption options component
|
||||
// - Browser SSO on extension open
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier);
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
// note: this flow affects both TDE & standard users
|
||||
if (this.isForcePasswordResetRequired(authResult)) {
|
||||
return await this.handleForcePasswordReset(this.orgIdentifier);
|
||||
}
|
||||
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
|
||||
const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption);
|
||||
|
||||
if (tdeEnabled) {
|
||||
return await this.handleTrustedDeviceEncryptionEnabled(
|
||||
authResult,
|
||||
this.orgIdentifier,
|
||||
userDecryptionOpts,
|
||||
);
|
||||
}
|
||||
|
||||
// User must set password if they don't have one and they aren't using either TDE or key connector.
|
||||
const requireSetPassword =
|
||||
!userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined;
|
||||
|
||||
if (requireSetPassword || authResult.resetMasterPassword) {
|
||||
// Change implies going no password -> password in this case
|
||||
return await this.handleChangePasswordRequired(this.orgIdentifier);
|
||||
}
|
||||
|
||||
return await this.handleSuccessfulLogin();
|
||||
}
|
||||
|
||||
private async isTrustedDeviceEncEnabled(
|
||||
trustedDeviceOption: TrustedDeviceUserDecryptionOption,
|
||||
): Promise<boolean> {
|
||||
const ssoTo2faFlowActive = this.route.snapshot.queryParamMap.get("sso") === "true";
|
||||
|
||||
return ssoTo2faFlowActive && trustedDeviceOption !== undefined;
|
||||
}
|
||||
|
||||
private async handleTrustedDeviceEncryptionEnabled(
|
||||
authResult: AuthResult,
|
||||
orgIdentifier: string,
|
||||
userDecryptionOpts: UserDecryptionOptions,
|
||||
): Promise<void> {
|
||||
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||
if (
|
||||
!userDecryptionOpts.hasMasterPassword &&
|
||||
userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
|
||||
) {
|
||||
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
|
||||
// Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and
|
||||
// if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key.
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.onSuccessfulLoginTde != null) {
|
||||
// Note: awaiting this will currently cause a hang on desktop & browser as they will wait for a full sync to complete
|
||||
// before navigating to the success route.
|
||||
// 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.onSuccessfulLoginTde();
|
||||
}
|
||||
|
||||
// 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.navigateViaCallbackOrRoute(
|
||||
this.onSuccessfulLoginTdeNavigate,
|
||||
// Navigate to TDE page (if user was on trusted device and TDE has decrypted
|
||||
// their user key, the login-initiated guard will redirect them to the vault)
|
||||
[this.trustedDeviceEncRoute],
|
||||
);
|
||||
}
|
||||
|
||||
private async handleChangePasswordRequired(orgIdentifier: string) {
|
||||
await this.router.navigate([this.changePasswordRoute], {
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a user needs to reset their password based on certain conditions.
|
||||
* Users can be forced to reset their password via an admin or org policy disallowing weak passwords.
|
||||
* Note: this is different from the SSO component login flow as a user can
|
||||
* login with MP and then have to pass 2FA to finish login and we can actually
|
||||
* evaluate if they have a weak password at that time.
|
||||
*
|
||||
* @param {AuthResult} authResult - The authentication result.
|
||||
* @returns {boolean} Returns true if a password reset is required, false otherwise.
|
||||
*/
|
||||
private isForcePasswordResetRequired(authResult: AuthResult): boolean {
|
||||
const forceResetReasons = [
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
];
|
||||
|
||||
return forceResetReasons.includes(authResult.forcePasswordReset);
|
||||
}
|
||||
|
||||
private async handleForcePasswordReset(orgIdentifier: string) {
|
||||
// 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([this.forcePasswordResetRoute], {
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async handleSuccessfulLogin() {
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
// Note: awaiting this will currently cause a hang on desktop & browser as they will wait for a full sync to complete
|
||||
// before navigating to the success route.
|
||||
// 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.onSuccessfulLogin();
|
||||
}
|
||||
await this.navigateViaCallbackOrRoute(this.onSuccessfulLoginNavigate, [this.successRoute]);
|
||||
}
|
||||
|
||||
private async navigateViaCallbackOrRoute(
|
||||
callback: () => Promise<unknown>,
|
||||
commands: unknown[],
|
||||
extras?: NavigationExtras,
|
||||
): Promise<void> {
|
||||
if (callback) {
|
||||
await callback();
|
||||
} else {
|
||||
await this.router.navigate(commands, extras);
|
||||
}
|
||||
}
|
||||
|
||||
private async authing(): Promise<boolean> {
|
||||
return (await firstValueFrom(this.loginStrategyService.currentAuthType$)) !== null;
|
||||
}
|
||||
|
||||
private async needsLock(): Promise<boolean> {
|
||||
const authType = await firstValueFrom(this.loginStrategyService.currentAuthType$);
|
||||
return authType == AuthenticationType.Sso || authType == AuthenticationType.UserApiKey;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<ng-container>
|
||||
<!-- TOTP Authenticator -->
|
||||
<bit-icon [icon]="Icons.TOTPIcon" *ngIf="provider == 0"></bit-icon>
|
||||
<!-- Email -->
|
||||
<bit-icon [icon]="Icons.EmailIcon" *ngIf="provider == 1"></bit-icon>
|
||||
<!-- Webauthn -->
|
||||
<bit-icon [icon]="Icons.WebAuthnIcon" *ngIf="provider == 7"></bit-icon>
|
||||
<!-- Recovery Code -->
|
||||
<bit-icon [icon]="Icons.RecoveryCodeIcon" *ngIf="provider == 'rc'"></bit-icon>
|
||||
<!-- Other 2FA Types (Duo, Yubico, U2F as PNG) -->
|
||||
<img
|
||||
[class]="'mfaType' + provider"
|
||||
[alt]="name"
|
||||
*ngIf="provider == 2 || provider == 3 || provider == 4 || provider == 5 || provider == 6"
|
||||
/>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { EmailIcon } from "../icons/email.icon";
|
||||
import { RecoveryCodeIcon } from "../icons/recovery.icon";
|
||||
import { TOTPIcon } from "../icons/totp.icon";
|
||||
import { WebAuthnIcon } from "../icons/webauthn.icon";
|
||||
|
||||
@Component({
|
||||
selector: "auth-two-factor-icon",
|
||||
templateUrl: "./two-factor-icon.component.html",
|
||||
})
|
||||
export class TwoFactorIconComponent {
|
||||
@Input() provider: any;
|
||||
@Input() name: string;
|
||||
|
||||
protected readonly Icons = {
|
||||
TOTPIcon,
|
||||
EmailIcon,
|
||||
WebAuthnIcon,
|
||||
RecoveryCodeIcon,
|
||||
};
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
@@ -220,12 +220,9 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
this.token = this.token.replace(" ", "").trim();
|
||||
}
|
||||
|
||||
try {
|
||||
await this.doSubmit();
|
||||
} catch {
|
||||
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && this.webAuthn != null) {
|
||||
this.webAuthn.start();
|
||||
}
|
||||
await this.doSubmit();
|
||||
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && this.webAuthn != null) {
|
||||
this.webAuthn.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
|
||||
44
libs/angular/src/auth/icons/email.icon.ts
Normal file
44
libs/angular/src/auth/icons/email.icon.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const EmailIcon = svgIcon`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="67"
|
||||
height="64"
|
||||
fill="none"
|
||||
class="tw-text-primary-600"
|
||||
role="img"
|
||||
aria-label="[title]"
|
||||
>
|
||||
<title>Email</title>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
d="m9.28 19.155-5.848 4.363a4 4 0 0 0-1.608 3.206V59a4 4 0 0 0 4 4h56.044a4 4 0 0 0 4-4V26.742a4 4 0 0 0-1.63-3.223L58.3 19.155M42.438 7.49l-6.442-4.736a4 4 0 0 0-4.762.017l-6.324 4.72"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="M58.373 30.978V9.473a2 2 0 0 0-2-2H11.318a2 2 0 0 0-2 2v21.505"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
d="M64.504 61.637 43.35 42.107a6 6 0 0 0-4.07-1.59H27.421a6 6 0 0 0-4.175 1.69l-20.06 19.43"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-width="2"
|
||||
d="m65.181 27.239-22.81 13.623M2.51 27.24l22.81 13.622"
|
||||
/>
|
||||
<rect width="35" height="12" x="16.324" y="17.5" stroke="currentColor" rx="6" />
|
||||
<circle cx="21.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="31.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="41.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="26.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="36.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="46.324" cy="23.5" r="1.5" fill="currentColor" />
|
||||
</svg>
|
||||
`;
|
||||
51
libs/angular/src/auth/icons/recovery.icon.ts
Normal file
51
libs/angular/src/auth/icons/recovery.icon.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const RecoveryCodeIcon = svgIcon`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
alt="rc logo"
|
||||
width="76"
|
||||
height="63"
|
||||
fill="none"
|
||||
class="tw-text-primary-600"
|
||||
role="img"
|
||||
aria-label="[title]"
|
||||
>
|
||||
<title>Recovery Code</title>
|
||||
<rect
|
||||
width="49.459"
|
||||
height="17.255"
|
||||
x="1"
|
||||
y="-1"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
rx="8.627"
|
||||
transform="matrix(-1 0 0 1 52.15 38.12)"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7.742 19.337a1 1 0 0 1 1.635-1.152l-1.635 1.152Zm7.499 8.895.576.817a1 1 0 0 1-1.393-.24l.817-.577Zm8.895-7.498a1 1 0 1 1 1.152 1.634l-1.152-1.634Zm44.129 29.953-.776-.631.776.631ZM63.942 8.483l.631-.775-.631.775Zm-42.205 4.323.776.631-.776-.631Zm22.266 49.925a1 1 0 1 1 .063-1.999l-.063 2ZM16.358 27.183a1 1 0 1 1-1.975-.313l1.975.313Zm-6.981-8.998 6.681 9.47-1.634 1.153-6.682-9.47 1.635-1.154Zm5.287 9.23 9.472-6.681 1.152 1.634-9.47 6.681-1.154-1.634ZM67.49 50.056c10.112-12.42 8.241-30.685-4.179-40.797l1.263-1.551C77.85 18.517 79.85 38.042 69.04 51.318l-1.55-1.262ZM63.31 9.259C50.89-.853 32.625 1.018 22.513 13.437l-1.551-1.262C31.772-1.102 51.297-3.102 64.573 7.708l-1.263 1.55ZM44.066 60.732c8.732.275 17.484-3.381 23.423-10.676l1.551 1.263c-6.35 7.799-15.708 11.706-25.037 11.412l.063-1.999ZM22.513 13.437a28.861 28.861 0 0 0-6.155 13.746l-1.975-.313a30.861 30.861 0 0 1 6.579-14.695l1.551 1.262Z"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M36.59 25.167v-5.445c0-4.021 3.806-7.267 8.475-7.267s8.475 3.264 8.475 7.267v5.445"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
d="M32.353 37.345v-6.098a6 6 0 0 1 6-6h13.424a6 6 0 0 1 6 6v9.187a6 6 0 0 1-6 6h-.601"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M45.886 35.63c.491-.285.822-.82.822-1.432v-.5c0-.913-.736-1.653-1.643-1.653-.908 0-1.643.74-1.643 1.654v.499c0 .612.33 1.146.82 1.432v1.283c0 .457.368.827.822.827.454 0 .822-.37.822-.827v-1.282Z"
|
||||
/>
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 46.001 44.148)" />
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 32.001 44.148)" />
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 18.001 44.148)" />
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 39.001 44.148)" />
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 25.001 44.148)" />
|
||||
<circle cx="2" cy="2" r="2" fill="currentColor" transform="matrix(-1 0 0 1 11.001 44.148)" />
|
||||
</svg>
|
||||
`;
|
||||
61
libs/angular/src/auth/icons/totp.icon.ts
Normal file
61
libs/angular/src/auth/icons/totp.icon.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const TOTPIcon = svgIcon`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="120"
|
||||
height="62"
|
||||
fill="none"
|
||||
class="tw-text-primary-600"
|
||||
role="img"
|
||||
aria-label="[title]"
|
||||
>
|
||||
<title>TOTP Authenticator</title>
|
||||
<rect width="35" height="12" x="50.324" y="21.5" stroke="currentColor" rx="6" />
|
||||
<circle cx="55.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="65.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="75.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="60.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="70.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<circle cx="80.324" cy="27.5" r="1.5" fill="currentColor" />
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M52.703 61h34.706"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M63.932 51.166V61M75.16 51.166V61"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
d="M38.952 51.166h64.894a6 6 0 0 0 6-6V7a6 6 0 0 0-6-6h-68a6 6 0 0 0-6 6v7.655"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
d="M38.692 46.517h62.999a4 4 0 0 0 4-4V9.552a4 4 0 0 0-4-4H38.537a4 4 0 0 0-4 4v5.103"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M32.284 27.382h11.064l-2.912-2.895M43.348 30.276H32.284l2.912 2.895"
|
||||
/>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
d="M37.846 27.75V19a4 4 0 0 0-4-4h-18a4 4 0 0 0-4 4v38a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V30.5"
|
||||
/>
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M24.806 18.5h.08" />
|
||||
<path stroke="currentColor" d="M24.846 36a8 8 0 1 0 7.858 6.5" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M21.547 45.36h1.02v-3.64l-1.221.584v-.626l1.215-.575h.657v4.258h1.007v.545h-2.678v-.545ZM25.793 45.358h2.23v.548h-2.948v-.548c.405-.424.76-.799 1.063-1.124a27 27 0 0 0 .627-.687c.216-.263.363-.475.438-.636.076-.164.114-.33.114-.5 0-.27-.08-.48-.24-.633-.158-.153-.376-.23-.653-.23a1.99 1.99 0 0 0-.621.107 3.493 3.493 0 0 0-.69.323v-.658c.224-.106.443-.185.657-.24.217-.053.43-.08.64-.08.475 0 .856.126 1.145.378.29.25.435.578.435.984 0 .207-.049.413-.146.62a3.005 3.005 0 0 1-.468.684c-.122.14-.298.334-.53.581-.23.248-.58.618-1.053 1.11Z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
42
libs/angular/src/auth/icons/webauthn.icon.ts
Normal file
42
libs/angular/src/auth/icons/webauthn.icon.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const WebAuthnIcon = svgIcon`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="62.176998"
|
||||
height="60.152"
|
||||
fill="none"
|
||||
class="tw-text-primary-600"
|
||||
role="img"
|
||||
aria-label="[title]"
|
||||
>
|
||||
<title>Webauthn</title>
|
||||
<g stroke="currentColor" clip-path="url(#a)" transform="translate(-6.081,-1.143)">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-miterlimit="10"
|
||||
stroke-width="2"
|
||||
d="M20.418 33.19c5.614 0 10.42 3.56 12.396 8.533h29.012l5.434 5.522-4.491 4.563h-6.602l-3.413-3.468-3.279 3.24-1.167-1.187-1.303 1.323-3.188-3.24-3.1 3.149H32.86c-1.932 5.065-6.782 8.67-12.44 8.67-7.366 0-13.339-6.069-13.339-13.553 0-7.483 5.928-13.552 13.338-13.552Zm-6.152 16.975c1.841 0 3.368-1.506 3.368-3.422 0-1.871-1.482-3.423-3.368-3.423-1.887 0-3.369 1.506-3.369 3.422 0 1.917 1.527 3.423 3.369 3.423Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
d="M59.78 44.722H36.32M19.99 36.722c4.036 0 7.569 2.405 9.51 6"
|
||||
/>
|
||||
<circle
|
||||
cx="12.24"
|
||||
cy="12.24"
|
||||
r="12.24"
|
||||
stroke-width="2"
|
||||
transform="matrix(-1 0 0 1 50.6 2.143)"
|
||||
/>
|
||||
<path
|
||||
stroke-width="2"
|
||||
d="M22.753 33.423c3.836-4.174 9.41-6.8 15.615-6.8 9.698 0 17.857 6.417 20.243 15.13"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a"><path fill="#fff" d="M68.5.62H.5v61h68z" /></clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./add-account-credit-dialog/add-account-credit-dialog.component";
|
||||
export * from "./invoices/invoices.component";
|
||||
export * from "./invoices/no-invoices.component";
|
||||
export * from "./manage-tax-information/manage-tax-information.component";
|
||||
export * from "./select-payment-method/select-payment-method.component";
|
||||
export * from "./verify-bank-account/verify-bank-account.component";
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<th bitCell>{{ "invoice" | i18n }}</th>
|
||||
<th bitCell>{{ "total" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell>{{ "clientDetails" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
@@ -47,36 +48,13 @@
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
[bitMenuTriggerFor]="rowMenu"
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
size="default"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #rowMenu>
|
||||
<a
|
||||
bitMenuItem
|
||||
href="{{ invoice.pdfUrl }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="tw-mr-2"
|
||||
appA11yTitle="{{ 'viewInvoice' | i18n }}"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-file-pdf"></i>
|
||||
{{ "viewInvoice" | i18n }}
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
*ngIf="getClientInvoiceReport"
|
||||
(click)="runExport(invoice.id)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-sign-in"></i>
|
||||
{{ "exportClientReport" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
<button type="button" bitLink (click)="runExport(invoice.id)">
|
||||
<span class="tw-font-normal">{{ "downloadCSV" | i18n }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<div *ngIf="!invoices || invoices.length === 0" class="tw-mt-10">
|
||||
<app-no-invoices></app-no-invoices>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
const partnerTrustIcon = svgIcon`
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M69 43.6511C70.1025 44.1328 71.32 44.4 72.6 44.4C77.5706 44.4 81.6 40.3706 81.6 35.4C81.6 30.4294 77.5706 26.4 72.6 26.4C67.6295 26.4 63.6 30.4294 63.6 35.4C63.6 36.0164 63.662 36.6184 63.7801 37.2" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M69 44.8416C70.1522 44.553 71.3559 44.4 72.5943 44.4C80.1533 44.4 86.4194 50.0998 87.5598 57.5529C87.7996 59.1204 86.9541 60 85.3451 60C79.7231 60 75.6474 60 71.4 60" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M51 43.6511C49.8976 44.1328 48.68 44.4 47.4 44.4C42.4295 44.4 38.4 40.3706 38.4 35.4C38.4 30.4294 42.4295 26.4 47.4 26.4C52.3706 26.4 56.4 30.4294 56.4 35.4C56.4 36.0164 56.338 36.6184 56.22 37.2" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M51 44.8416C49.8478 44.553 48.6441 44.4 47.4057 44.4C39.8467 44.4 33.5806 50.0998 32.4402 57.5529C32.2004 59.1204 33.0459 60 34.6549 60C40.2769 60 44.3526 60 48.6 60" stroke="#CED4DC" stroke-width="2"/>
|
||||
<circle cx="60" cy="45.6" r="9" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M72.7451 70.2C62.3773 70.2 57.2682 70.2 46.6437 70.2C45.3864 70.2 44.8665 68.8141 45.0289 67.7529C46.1693 60.2998 52.4354 54.6 59.9943 54.6C67.5533 54.6 73.8194 60.2998 74.9598 67.7529C75.1996 69.3204 74.3541 70.2 72.7451 70.2Z" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M73.557 103.195C74.3197 104.319 74.3197 105.443 73.557 106.272C73.1462 106.745 72.3835 107.041 71.738 107.041C71.6242 107.041 71.5013 107.032 71.3801 107.011C71.3345 108.505 70.5896 109.217 70.0948 109.467C69.7427 109.703 69.3318 109.822 68.9212 109.822C68.8703 109.822 68.8204 109.82 68.7714 109.816C68.7035 109.811 68.6373 109.803 68.5729 109.792C68.5559 110.332 68.3591 110.827 67.9823 111.242C67.6302 111.833 66.926 112.129 66.2808 112.129C66.1045 112.129 65.9874 112.129 65.8112 112.07C65.7753 112.058 65.7381 112.044 65.6997 112.028C65.5215 112.624 65.0169 113.105 64.227 113.431C63.9924 113.49 63.8162 113.49 63.5815 113.49C62.4417 113.49 60.9477 112.633 60.0352 112.028M55.5 88.7567C53.6867 88.9991 51.6246 89.9258 50.4378 90.4744C50.2985 90.5095 50.1797 90.5655 50.0693 90.6176L50.0687 90.6179C49.9937 90.6534 49.9223 90.6871 49.851 90.711C49.5281 90.5373 49.0708 90.3955 48.4995 90.2184C47.5139 89.9127 46.1886 89.5016 44.6287 88.6402C44.3941 88.5219 44.0419 88.581 43.8657 88.8769L37.6461 99.5268C37.4699 99.8226 37.5287 100.178 37.822 100.355L46.8 105.019" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M73.7624 103.371C73.6448 103.371 73.4683 103.313 73.3507 103.195L67.7625 97.9108C63.1743 96.7365 61.8803 93.9181 61.4097 92.8024C61.0567 92.8612 60.5273 92.9199 59.645 92.9199C59.2332 93.2135 58.1744 94.0355 56.7039 94.7988C55.3509 95.5034 53.998 95.2098 53.4098 94.2117C53.0568 93.5658 52.998 92.8024 53.351 92.1566C55.7039 87.5766 58.4685 86.6372 60.3509 86.6372C61.4685 86.6372 62.1744 86.9895 62.2332 86.9895L69.2919 90.1015L75.7624 87.1656C75.8801 87.1069 76.0565 87.1069 76.233 87.1656C76.4095 87.2243 76.5271 87.3418 76.5859 87.4592L82.6447 97.8521C82.7623 98.1457 82.6447 98.4393 82.4094 98.6154L74.0565 103.313C73.9389 103.371 73.8801 103.371 73.7624 103.371Z" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M46.6097 107.206C47.0221 108.443 48.394 109.005 49.044 109.195C49.217 109.923 49.5794 110.391 49.9178 110.692C50.5086 111.164 51.1584 111.342 51.7491 111.342C51.9248 111.342 52.0796 111.321 52.2135 111.291C52.2516 111.368 52.2936 111.444 52.3399 111.519C52.8716 112.346 53.8168 112.818 55.2346 112.937C55.3143 112.937 55.3897 112.919 55.4573 112.887C56.0276 113.588 56.9845 114 57.893 114C58.7791 114 59.488 113.527 59.7243 112.759C60.0787 111.223 59.9606 109.628 59.7243 108.742C59.5602 108.031 59.1935 106.763 57.6392 106.673C57.4017 106.121 56.803 105.205 55.4709 105.08C55.1879 105.053 54.9235 105.086 54.6774 105.164C54.4496 104.62 54.0132 103.917 53.2851 103.662C52.7534 103.485 51.9264 103.662 51.1584 104.134C51.1181 104.161 51.0782 104.187 51.0387 104.215C50.6899 103.795 50.1003 103.263 49.3271 103.189C48.6182 103.13 48.0275 103.485 47.4367 104.193C46.5506 105.316 46.3143 106.32 46.6097 107.206Z" stroke="#CED4DC" stroke-width="2"/>
|
||||
<path d="M51.9 104.4C51.5709 104.603 49.9229 105.531 49.5 106.8C49.2 107.7 49.5 108.9 49.8 109.5" stroke="#CED4DC" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M54.6703 105.317C54.1767 105.571 53.0709 106.485 52.597 108.109C52.1231 109.734 52.3995 110.816 52.597 111.155" stroke="#CED4DC" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M57.2 107.263C56.7063 107.517 56.051 108.431 55.7104 109.598C55.3697 110.766 55.5129 112.179 55.7104 112.517" stroke="#CED4DC" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M65.5675 111.739C64.0108 110.96 61.2866 108.431 61.2866 108.431" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M68.2914 109.598C66.1686 108.591 62.4538 105.317 62.4538 105.317" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M71.4049 106.874C68.9991 105.775 64.789 102.204 64.789 102.204" stroke="#CED4DC" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M86.9971 91.8C95.5145 86.0616 101.962 77.753 105.392 68.094C108.822 58.435 109.055 47.9345 106.056 38.134C103.057 28.3336 96.9838 19.7494 88.7289 13.6419C80.474 7.53438 70.4717 4.22518 60.1907 4.20014C49.9096 4.1751 39.8913 7.43554 31.6065 13.5028C23.3217 19.5701 17.2069 28.1245 14.1599 37.9102C11.1128 47.696 11.294 58.1975 14.6768 67.8731C18.0596 77.5486 24.4658 85.8885 32.955 91.6684" stroke="#CED4DC" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M84.6771 88.2C92.4374 82.9725 98.3114 75.4037 101.437 66.6048C104.562 57.8059 104.774 48.2403 102.042 39.3125C99.3091 30.3847 93.7761 22.5649 86.255 17.0012C78.7338 11.4375 69.6207 8.42293 60.2535 8.40012C50.8863 8.37731 41.7585 11.3474 34.2101 16.8745C26.6618 22.4015 21.0905 30.1942 18.3143 39.1086C15.5381 48.023 15.7032 57.5895 18.7853 66.4035C21.8674 75.2176 27.7042 82.8149 35.4388 88.0801" stroke="#CED4DC" stroke-linecap="round"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
@Component({
|
||||
selector: "app-no-invoices",
|
||||
template: `<div class="tw-flex tw-flex-col tw-items-center tw-text-info">
|
||||
<bit-icon [icon]="icon"></bit-icon>
|
||||
<p class="tw-mt-4">{{ "noInvoicesToList" | i18n }}</p>
|
||||
</div>`,
|
||||
})
|
||||
export class NoInvoicesComponent {
|
||||
icon = partnerTrustIcon;
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
import { Directive, Input, OnChanges, OnDestroy, OnInit } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
ControlValueAccessor,
|
||||
FormBuilder,
|
||||
ValidationErrors,
|
||||
Validator,
|
||||
} from "@angular/forms";
|
||||
import { filter, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { VaultTimeout, VaultTimeoutOption } from "@bitwarden/common/types/vault-timeout.type";
|
||||
|
||||
interface VaultTimeoutFormValue {
|
||||
vaultTimeout: VaultTimeout | null;
|
||||
custom: {
|
||||
hours: number | null;
|
||||
minutes: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export class VaultTimeoutInputComponent
|
||||
implements ControlValueAccessor, Validator, OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
get showCustom() {
|
||||
return this.form.get("vaultTimeout").value === VaultTimeoutInputComponent.CUSTOM_VALUE;
|
||||
}
|
||||
|
||||
get exceedsMinimumTimout(): boolean {
|
||||
return (
|
||||
!this.showCustom || this.customTimeInMinutes() > VaultTimeoutInputComponent.MIN_CUSTOM_MINUTES
|
||||
);
|
||||
}
|
||||
|
||||
static CUSTOM_VALUE = -100;
|
||||
static MIN_CUSTOM_MINUTES = 0;
|
||||
|
||||
form = this.formBuilder.group({
|
||||
vaultTimeout: [null],
|
||||
custom: this.formBuilder.group({
|
||||
hours: [null],
|
||||
minutes: [null],
|
||||
}),
|
||||
});
|
||||
|
||||
@Input() vaultTimeoutOptions: VaultTimeoutOption[];
|
||||
vaultTimeoutPolicy: Policy;
|
||||
vaultTimeoutPolicyHours: number;
|
||||
vaultTimeoutPolicyMinutes: number;
|
||||
|
||||
protected canLockVault$: Observable<boolean>;
|
||||
|
||||
private onChange: (vaultTimeout: VaultTimeout) => void;
|
||||
private validatorChange: () => void;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private policyService: PolicyService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.policyService
|
||||
.get$(PolicyType.MaximumVaultTimeout)
|
||||
.pipe(
|
||||
filter((policy) => policy != null),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((policy) => {
|
||||
this.vaultTimeoutPolicy = policy;
|
||||
this.applyVaultTimeoutPolicy();
|
||||
});
|
||||
|
||||
this.form.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((value: VaultTimeoutFormValue) => {
|
||||
if (this.onChange) {
|
||||
this.onChange(this.getVaultTimeout(value));
|
||||
}
|
||||
});
|
||||
|
||||
// Assign the current value to the custom fields
|
||||
// so that if the user goes from a numeric value to custom
|
||||
// we can initialize the custom fields with the current value
|
||||
// ex: user picks 5 min, goes to custom, we want to show 0 hr, 5 min in the custom fields
|
||||
this.form.controls.vaultTimeout.valueChanges
|
||||
.pipe(
|
||||
filter((value) => value !== VaultTimeoutInputComponent.CUSTOM_VALUE),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((value) => {
|
||||
const current = Math.max(value, 0);
|
||||
|
||||
// This cannot emit an event b/c it would cause form.valueChanges to fire again
|
||||
// and we are already handling that above so just silently update
|
||||
// custom fields when vaultTimeout changes to a non-custom value
|
||||
this.form.patchValue(
|
||||
{
|
||||
custom: {
|
||||
hours: Math.floor(current / 60),
|
||||
minutes: current % 60,
|
||||
},
|
||||
},
|
||||
{ emitEvent: false },
|
||||
);
|
||||
});
|
||||
|
||||
this.canLockVault$ = this.vaultTimeoutSettingsService
|
||||
.availableVaultTimeoutActions$()
|
||||
.pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock)));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (
|
||||
!this.vaultTimeoutOptions.find((p) => p.value === VaultTimeoutInputComponent.CUSTOM_VALUE)
|
||||
) {
|
||||
this.vaultTimeoutOptions.push({
|
||||
name: this.i18nService.t("custom"),
|
||||
value: VaultTimeoutInputComponent.CUSTOM_VALUE,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getVaultTimeout(value: VaultTimeoutFormValue) {
|
||||
if (value.vaultTimeout !== VaultTimeoutInputComponent.CUSTOM_VALUE) {
|
||||
return value.vaultTimeout;
|
||||
}
|
||||
|
||||
return value.custom.hours * 60 + value.custom.minutes;
|
||||
}
|
||||
|
||||
writeValue(value: number): void {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.vaultTimeoutOptions.every((p) => p.value !== value)) {
|
||||
this.form.setValue({
|
||||
vaultTimeout: VaultTimeoutInputComponent.CUSTOM_VALUE,
|
||||
custom: {
|
||||
hours: Math.floor(value / 60),
|
||||
minutes: value % 60,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.patchValue({
|
||||
vaultTimeout: value,
|
||||
});
|
||||
}
|
||||
|
||||
registerOnChange(onChange: any): void {
|
||||
this.onChange = onChange;
|
||||
}
|
||||
|
||||
registerOnTouched(onTouched: any): void {
|
||||
// Empty
|
||||
}
|
||||
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
// Empty
|
||||
}
|
||||
|
||||
validate(control: AbstractControl): ValidationErrors {
|
||||
if (this.vaultTimeoutPolicy && this.vaultTimeoutPolicy?.data?.minutes < control.value) {
|
||||
return { policyError: true };
|
||||
}
|
||||
|
||||
if (!this.exceedsMinimumTimout) {
|
||||
return { minTimeoutError: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
registerOnValidatorChange(fn: () => void): void {
|
||||
this.validatorChange = fn;
|
||||
}
|
||||
|
||||
private customTimeInMinutes() {
|
||||
return this.form.value.custom.hours * 60 + this.form.value.custom.minutes;
|
||||
}
|
||||
|
||||
private applyVaultTimeoutPolicy() {
|
||||
this.vaultTimeoutPolicyHours = Math.floor(this.vaultTimeoutPolicy.data.minutes / 60);
|
||||
this.vaultTimeoutPolicyMinutes = this.vaultTimeoutPolicy.data.minutes % 60;
|
||||
|
||||
this.vaultTimeoutOptions = this.vaultTimeoutOptions.filter((vaultTimeoutOption) => {
|
||||
// Always include the custom option
|
||||
if (vaultTimeoutOption.value === VaultTimeoutInputComponent.CUSTOM_VALUE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof vaultTimeoutOption.value === "number") {
|
||||
// Include numeric values that are less than or equal to the policy minutes
|
||||
return vaultTimeoutOption.value <= this.vaultTimeoutPolicy.data.minutes;
|
||||
}
|
||||
|
||||
// Exclude all string cases when there's a numeric policy defined
|
||||
return false;
|
||||
});
|
||||
|
||||
// Only call validator change if it's been set
|
||||
if (this.validatorChange) {
|
||||
this.validatorChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid
|
||||
* Attach it to a <form> element and provide the name of the class property that will hold the api call promise.
|
||||
* e.g. <form [appApiAction]="this.formPromise">
|
||||
* Any errors/rejections that occur will be intercepted and displayed as error toasts.
|
||||
*
|
||||
* @deprecated Use the CL's {@link BitSubmitDirective} instead
|
||||
*/
|
||||
@Directive({
|
||||
selector: "[appApiAction]",
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { Directive, HostListener, Input } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Directive({
|
||||
selector: "[appCopyClick]",
|
||||
})
|
||||
export class CopyClickDirective {
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
@Input("appCopyClick") valueToCopy = "";
|
||||
@Input({ transform: coerceBooleanProperty }) showToast?: boolean;
|
||||
|
||||
@HostListener("click") onClick() {
|
||||
this.platformUtilsService.copyToClipboard(this.valueToCopy);
|
||||
|
||||
if (this.showToast) {
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: null,
|
||||
message: this.i18nService.t("copySuccessful"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Directive, ElementRef, HostListener } from "@angular/core";
|
||||
import { Directive, ElementRef, HostListener, Self } from "@angular/core";
|
||||
import { NgControl } from "@angular/forms";
|
||||
|
||||
@Directive({
|
||||
selector: "input[appInputStripSpaces]",
|
||||
})
|
||||
export class InputStripSpacesDirective {
|
||||
constructor(private el: ElementRef<HTMLInputElement>) {}
|
||||
constructor(
|
||||
private el: ElementRef<HTMLInputElement>,
|
||||
@Self() private ngControl: NgControl,
|
||||
) {}
|
||||
|
||||
@HostListener("input") onInput() {
|
||||
this.el.nativeElement.value = this.el.nativeElement.value.replace(/ /g, "");
|
||||
const value = this.el.nativeElement.value.replace(/\s+/g, "");
|
||||
this.ngControl.control.setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import {
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
SelectPaymentMethodComponent,
|
||||
VerifyBankAccountComponent,
|
||||
@@ -17,6 +18,8 @@ import {
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
MenuModule,
|
||||
RadioButtonModule,
|
||||
SelectModule,
|
||||
@@ -25,6 +28,7 @@ import {
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component";
|
||||
import { CalloutComponent } from "./components/callout.component";
|
||||
import { A11yInvalidDirective } from "./directives/a11y-invalid.directive";
|
||||
import { A11yTitleDirective } from "./directives/a11y-title.directive";
|
||||
@@ -73,6 +77,9 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
TableModule,
|
||||
MenuModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
IconModule,
|
||||
],
|
||||
declarations: [
|
||||
A11yInvalidDirective,
|
||||
@@ -104,9 +111,11 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
FingerprintPipe,
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
SelectPaymentMethodComponent,
|
||||
VerifyBankAccountComponent,
|
||||
TwoFactorIconComponent,
|
||||
],
|
||||
exports: [
|
||||
A11yInvalidDirective,
|
||||
@@ -139,9 +148,11 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
FingerprintPipe,
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
SelectPaymentMethodComponent,
|
||||
VerifyBankAccountComponent,
|
||||
TwoFactorIconComponent,
|
||||
],
|
||||
providers: [
|
||||
CreditCardNumberPipe,
|
||||
|
||||
@@ -18,11 +18,7 @@ export class AngularThemingService implements AbstractThemingService {
|
||||
static createSystemThemeFromWindow(window: Window): Observable<ThemeType> {
|
||||
return merge(
|
||||
// This observable should always emit at least once, so go and get the current system theme designation
|
||||
of(
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? ThemeType.Dark
|
||||
: ThemeType.Light,
|
||||
),
|
||||
of(AngularThemingService.getSystemThemeFromWindow(window)),
|
||||
// Start listening to changes
|
||||
fromEvent<MediaQueryListEvent>(
|
||||
window.matchMedia("(prefers-color-scheme: dark)"),
|
||||
@@ -31,6 +27,17 @@ export class AngularThemingService implements AbstractThemingService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently active system theme based on the given window.
|
||||
* @param window The window to query for the current theme.
|
||||
* @returns The active system theme.
|
||||
*/
|
||||
static getSystemThemeFromWindow(window: Window): ThemeType {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? ThemeType.Dark
|
||||
: ThemeType.Light;
|
||||
}
|
||||
|
||||
readonly theme$ = this.themeStateService.selectedTheme$.pipe(
|
||||
switchMap((configuredTheme) => {
|
||||
if (configuredTheme === ThemeType.System) {
|
||||
|
||||
53
libs/angular/src/platform/utils/feature-flagged-route.ts
Normal file
53
libs/angular/src/platform/utils/feature-flagged-route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Type, inject } from "@angular/core";
|
||||
import { Route, Routes } from "@angular/router";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { componentRouteSwap } from "../../utils/component-route-swap";
|
||||
|
||||
/**
|
||||
* @param defaultComponent The component to be used when the feature flag is off.
|
||||
* @param flaggedComponent The component to be used when the feature flag is on.
|
||||
* @param featureFlag The feature flag to evaluate
|
||||
* @param routeOptions The shared route options to apply to both components.
|
||||
*/
|
||||
type FeatureFlaggedRouteConfig = {
|
||||
defaultComponent: Type<any>;
|
||||
flaggedComponent: Type<any>;
|
||||
featureFlag: FeatureFlag;
|
||||
routeOptions: Omit<Route, "component">;
|
||||
};
|
||||
|
||||
/**
|
||||
* Swap between two routes at runtime based on the value of a feature flag.
|
||||
* The routes share a common path and configuration but load different components.
|
||||
* @param config See {@link FeatureFlaggedRouteConfig}
|
||||
* @returns A tuple containing the conditional configuration for the two routes. This should be unpacked into your existing Routes array.
|
||||
* @example
|
||||
* const routes: Routes = [
|
||||
* ...featureFlaggedRoute({
|
||||
* defaultComponent: GroupsComponent,
|
||||
* flaggedComponent: GroupsNewComponent,
|
||||
* featureFlag: FeatureFlag.GroupsComponentRefactor,
|
||||
* routeOptions: {
|
||||
* path: "groups",
|
||||
* canActivate: [OrganizationPermissionsGuard],
|
||||
* },
|
||||
* }),
|
||||
* ]
|
||||
*/
|
||||
export function featureFlaggedRoute(config: FeatureFlaggedRouteConfig): Routes {
|
||||
const canMatch$ = () =>
|
||||
inject(ConfigService)
|
||||
.getFeatureFlag$(config.featureFlag)
|
||||
.pipe(map((flagValue) => flagValue === true));
|
||||
|
||||
return componentRouteSwap(
|
||||
config.defaultComponent,
|
||||
config.flaggedComponent,
|
||||
canMatch$,
|
||||
config.routeOptions,
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import {
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
DefaultRegistrationFinishService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
AuthRequestService,
|
||||
@@ -14,6 +18,7 @@ import {
|
||||
UserDecryptionOptionsService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
LogoutReason,
|
||||
RegisterRouteService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||
@@ -149,7 +154,7 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { devFlagEnabled, flagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
@@ -157,7 +162,6 @@ import { ConfigApiService } from "@bitwarden/common/platform/services/config/con
|
||||
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||
import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service";
|
||||
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
@@ -202,12 +206,6 @@ import { NotificationsService } from "@bitwarden/common/services/notifications.s
|
||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||
import {
|
||||
legacyPasswordGenerationServiceFactory,
|
||||
legacyUsernameGenerationServiceFactory,
|
||||
} from "@bitwarden/common/tools/generator";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
@@ -239,6 +237,12 @@ import { FolderService } from "@bitwarden/common/vault/services/folder/folder.se
|
||||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
legacyPasswordGenerationServiceFactory,
|
||||
legacyUsernameGenerationServiceFactory,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
ImportApiService,
|
||||
ImportApiServiceAbstraction,
|
||||
@@ -723,6 +727,10 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: SsoLoginService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: STATE_FACTORY,
|
||||
useValue: new StateFactory(GlobalState, Account),
|
||||
}),
|
||||
safeProvider({
|
||||
provide: StateServiceAbstraction,
|
||||
useClass: StateService,
|
||||
@@ -813,7 +821,7 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EncryptService,
|
||||
useFactory: encryptServiceFactory,
|
||||
useClass: MultithreadEncryptServiceImplementation,
|
||||
deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -830,6 +838,7 @@ const safeProviders: SafeProvider[] = [
|
||||
OrganizationServiceAbstraction,
|
||||
EventUploadServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -969,7 +978,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: LoginEmailServiceAbstraction,
|
||||
useClass: LoginEmailService,
|
||||
deps: [StateProvider],
|
||||
deps: [AccountServiceAbstraction, AuthServiceAbstraction, StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: OrgDomainInternalServiceAbstraction,
|
||||
@@ -1220,18 +1229,18 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: StripeService,
|
||||
deps: [LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RegisterRouteService,
|
||||
useClass: RegisterRouteService,
|
||||
deps: [ConfigService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: RegistrationFinishServiceAbstraction,
|
||||
useClass: DefaultRegistrationFinishService,
|
||||
deps: [CryptoServiceAbstraction, AccountApiServiceAbstraction],
|
||||
}),
|
||||
];
|
||||
|
||||
function encryptServiceFactory(
|
||||
cryptoFunctionservice: CryptoFunctionServiceAbstraction,
|
||||
logService: LogService,
|
||||
logMacFailures: boolean,
|
||||
): EncryptService {
|
||||
return flagEnabled("multithreadDecryption")
|
||||
? new MultithreadEncryptServiceImplementation(cryptoFunctionservice, logService, logMacFailures)
|
||||
: new EncryptServiceImplementation(cryptoFunctionservice, logService, logMacFailures);
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function
|
||||
|
||||
@@ -8,18 +8,23 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
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 { GeneratorType } from "@bitwarden/common/tools/generator/generator-type";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
GeneratorType,
|
||||
DefaultPasswordBoundaries as DefaultBoundaries,
|
||||
} from "@bitwarden/generator-core";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PasswordGeneratorOptions,
|
||||
} from "@bitwarden/common/tools/generator/password";
|
||||
import { DefaultBoundaries } from "@bitwarden/common/tools/generator/password/password-generator-options-evaluator";
|
||||
import {
|
||||
UsernameGenerationServiceAbstraction,
|
||||
UsernameGeneratorOptions,
|
||||
} from "@bitwarden/common/tools/generator/username";
|
||||
import { EmailForwarderOptions } from "@bitwarden/common/tools/models/domain/email-forwarder-options";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
PasswordGeneratorOptions,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
|
||||
export class EmailForwarderOptions {
|
||||
name: string;
|
||||
value: string;
|
||||
validForSelfHosted: boolean;
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export class GeneratorComponent implements OnInit, OnDestroy {
|
||||
|
||||
@@ -2,11 +2,9 @@ import { Directive, OnInit } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
GeneratedPasswordHistory,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/password";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { GeneratedPasswordHistory } from "@bitwarden/generator-history";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@Directive()
|
||||
export class PasswordGeneratorHistoryComponent implements OnInit {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<bit-progress
|
||||
[size]="size"
|
||||
[text]="text"
|
||||
[bgColor]="color"
|
||||
[showText]="showText"
|
||||
[barWidth]="scoreWidth"
|
||||
></bit-progress>
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
|
||||
import {
|
||||
PasswordColorText,
|
||||
PasswordStrengthScore,
|
||||
PasswordStrengthV2Component,
|
||||
} from "./password-strength-v2.component";
|
||||
|
||||
describe("PasswordStrengthV2Component", () => {
|
||||
let component: PasswordStrengthV2Component;
|
||||
let fixture: ComponentFixture<PasswordStrengthV2Component>;
|
||||
|
||||
const mockPasswordStrengthService = mock<PasswordStrengthServiceAbstraction>();
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: PasswordStrengthServiceAbstraction, useValue: mockPasswordStrengthService },
|
||||
],
|
||||
});
|
||||
fixture = TestBed.createComponent(PasswordStrengthV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create the component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should update password strength when password changes", () => {
|
||||
const password = "testPassword";
|
||||
jest.spyOn(component, "updatePasswordStrength");
|
||||
component.password = password;
|
||||
expect(component.updatePasswordStrength).toHaveBeenCalledWith(password);
|
||||
expect(mockPasswordStrengthService.getPasswordStrength).toHaveBeenCalledWith(
|
||||
password,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("should emit password strength result when password changes", () => {
|
||||
const password = "testPassword";
|
||||
jest.spyOn(component.passwordStrengthScore, "emit");
|
||||
component.password = password;
|
||||
expect(component.passwordStrengthScore.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should emit password score text and color when ngOnChanges executes", () => {
|
||||
jest.spyOn(component.passwordScoreTextWithColor, "emit");
|
||||
jest.useFakeTimers();
|
||||
component.ngOnChanges();
|
||||
jest.runAllTimers();
|
||||
expect(component.passwordScoreTextWithColor.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const table = [
|
||||
[4, { color: "success", text: "strong" }],
|
||||
[3, { color: "primary", text: "good" }],
|
||||
[2, { color: "warning", text: "weak" }],
|
||||
[1, { color: "danger", text: "weak" }],
|
||||
[null, { color: "danger", text: null }],
|
||||
];
|
||||
|
||||
test.each(table)(
|
||||
"should passwordScore be %d then emit passwordScoreTextWithColor = %s",
|
||||
(score: PasswordStrengthScore, expected: PasswordColorText) => {
|
||||
jest.useFakeTimers();
|
||||
jest.spyOn(component.passwordScoreTextWithColor, "emit");
|
||||
component.passwordScore = score;
|
||||
component.ngOnChanges();
|
||||
jest.runAllTimers();
|
||||
expect(component.passwordScoreTextWithColor.emit).toHaveBeenCalledWith(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, OnChanges, Output } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { ProgressModule } from "@bitwarden/components";
|
||||
|
||||
export interface PasswordColorText {
|
||||
color: BackgroundTypes;
|
||||
text: string;
|
||||
}
|
||||
export type PasswordStrengthScore = 0 | 1 | 2 | 3 | 4;
|
||||
|
||||
type SizeTypes = "small" | "default" | "large";
|
||||
type BackgroundTypes = "danger" | "primary" | "success" | "warning";
|
||||
|
||||
@Component({
|
||||
selector: "tools-password-strength",
|
||||
templateUrl: "password-strength-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, JslibModule, ProgressModule],
|
||||
})
|
||||
export class PasswordStrengthV2Component implements OnChanges {
|
||||
/**
|
||||
* The size (height) of the password strength component.
|
||||
* Possible values are "default", "small" and "large".
|
||||
*/
|
||||
@Input() size: SizeTypes = "default";
|
||||
/**
|
||||
* Determines whether to show the password strength score text on the progress bar or not.
|
||||
*/
|
||||
@Input() showText = false;
|
||||
/**
|
||||
* Optional email address which can be used as input for the password strength calculation
|
||||
*/
|
||||
@Input() email: string;
|
||||
/**
|
||||
* Optional name which can be used as input for the password strength calculation
|
||||
*/
|
||||
@Input() name: string;
|
||||
/**
|
||||
* Sets the password value and updates the password strength.
|
||||
*
|
||||
* @param value - password provided by the hosting component
|
||||
*/
|
||||
@Input() set password(value: string) {
|
||||
this.updatePasswordStrength(value);
|
||||
}
|
||||
/**
|
||||
* Emits the password strength score.
|
||||
*
|
||||
* @remarks
|
||||
* The password strength score represents the strength of a password.
|
||||
* It is emitted as an event when the password strength changes.
|
||||
*/
|
||||
@Output() passwordStrengthScore = new EventEmitter<PasswordStrengthScore>();
|
||||
|
||||
/**
|
||||
* Emits an event with the password score text and color.
|
||||
*/
|
||||
@Output() passwordScoreTextWithColor = new EventEmitter<PasswordColorText>();
|
||||
|
||||
passwordScore: PasswordStrengthScore;
|
||||
scoreWidth = 0;
|
||||
color: BackgroundTypes = "danger";
|
||||
text: string;
|
||||
|
||||
private passwordStrengthTimeout: number | NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
) {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.passwordStrengthTimeout = setTimeout(() => {
|
||||
this.scoreWidth = this.passwordScore == null ? 0 : (this.passwordScore + 1) * 20;
|
||||
|
||||
switch (this.passwordScore) {
|
||||
case 4:
|
||||
this.color = "success";
|
||||
this.text = this.i18nService.t("strong");
|
||||
break;
|
||||
case 3:
|
||||
this.color = "primary";
|
||||
this.text = this.i18nService.t("good");
|
||||
break;
|
||||
case 2:
|
||||
this.color = "warning";
|
||||
this.text = this.i18nService.t("weak");
|
||||
break;
|
||||
default:
|
||||
this.color = "danger";
|
||||
this.text = this.passwordScore != null ? this.i18nService.t("weak") : null;
|
||||
break;
|
||||
}
|
||||
|
||||
this.passwordScoreTextWithColor.emit({
|
||||
color: this.color,
|
||||
text: this.text,
|
||||
} as PasswordColorText);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
updatePasswordStrength(password: string) {
|
||||
if (this.passwordStrengthTimeout != null) {
|
||||
clearTimeout(this.passwordStrengthTimeout);
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordStrengthService.getPasswordStrength(
|
||||
password,
|
||||
this.email,
|
||||
this.name?.trim().toLowerCase().split(" "),
|
||||
);
|
||||
this.passwordScore = strengthResult == null ? null : strengthResult.score;
|
||||
this.passwordStrengthScore.emit(this.passwordScore);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@ export interface PasswordColorText {
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated July 2024: Use new PasswordStrengthV2Component instead
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-password-strength",
|
||||
templateUrl: "password-strength.component.html",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Type } from "@angular/core";
|
||||
import { Route, Routes } from "@angular/router";
|
||||
import { CanMatchFn, Route, Routes } from "@angular/router";
|
||||
|
||||
/**
|
||||
* Helper function to swap between two components based on an async condition. The async condition is evaluated
|
||||
@@ -26,28 +26,27 @@ import { Route, Routes } from "@angular/router";
|
||||
* @param defaultComponent - The default component to render.
|
||||
* @param altComponent - The alternate component to render when the condition is met.
|
||||
* @param shouldSwapFn - The async function to determine if the alternate component should be rendered.
|
||||
* @param options - The shared route options to apply to both components.
|
||||
* @param options - The shared route options to apply to the default component, and to the alt component if altOptions is not provided.
|
||||
* @param altOptions - The alt route options to apply to the alt component.
|
||||
*/
|
||||
export function componentRouteSwap(
|
||||
defaultComponent: Type<any>,
|
||||
altComponent: Type<any>,
|
||||
shouldSwapFn: () => Promise<boolean>,
|
||||
shouldSwapFn: CanMatchFn,
|
||||
options: Route,
|
||||
altOptions?: Route,
|
||||
): Routes {
|
||||
const defaultRoute = {
|
||||
...options,
|
||||
component: defaultComponent,
|
||||
};
|
||||
|
||||
const selectedAltOptions = altOptions ?? options;
|
||||
|
||||
const altRoute: Route = {
|
||||
...options,
|
||||
...selectedAltOptions,
|
||||
component: altComponent,
|
||||
canMatch: [
|
||||
async () => {
|
||||
return await shouldSwapFn();
|
||||
},
|
||||
...(options.canMatch ?? []),
|
||||
],
|
||||
canMatch: [shouldSwapFn, ...(selectedAltOptions.canMatch ?? [])],
|
||||
};
|
||||
|
||||
// Return the alternate route first, so it is evaluated first.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Type, inject } from "@angular/core";
|
||||
import { Route, Routes } from "@angular/router";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { componentRouteSwap } from "./component-route-swap";
|
||||
/**
|
||||
* Helper function to swap between two components based on the TwoFactorComponentRefactor feature flag.
|
||||
* @param defaultComponent - The current non-refactored component to render.
|
||||
* @param refreshedComponent - The new refactored component to render.
|
||||
* @param defaultOptions - The options to apply to the default component and the refactored component, if alt options are not provided.
|
||||
* @param altOptions - The options to apply to the refactored component.
|
||||
*/
|
||||
export function twofactorRefactorSwap(
|
||||
defaultComponent: Type<any>,
|
||||
refreshedComponent: Type<any>,
|
||||
defaultOptions: Route,
|
||||
altOptions?: Route,
|
||||
): Routes {
|
||||
return componentRouteSwap(
|
||||
defaultComponent,
|
||||
refreshedComponent,
|
||||
async () => {
|
||||
const configService = inject(ConfigService);
|
||||
return configService.getFeatureFlag(FeatureFlag.TwoFactorComponentRefactor);
|
||||
},
|
||||
defaultOptions,
|
||||
altOptions,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user