mirror of
https://github.com/bitwarden/browser
synced 2026-02-08 12:40:26 +00:00
Merge branch 'main' into km/pm-18576/fix-missing-userid-on-remove-password
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||
import { Component, EventEmitter, Output, Input, OnInit, OnDestroy } from "@angular/core";
|
||||
@@ -7,8 +5,6 @@ import { ActivatedRoute } from "@angular/router";
|
||||
import { Observable, map, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { SelfHostedEnvConfigDialogComponent } from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
@@ -88,7 +84,6 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
protected environmentService: EnvironmentService,
|
||||
private route: ActivatedRoute,
|
||||
private dialogService: DialogService,
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
@@ -113,24 +108,18 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the self-hosted settings dialog.
|
||||
*
|
||||
* If the `UnauthenticatedExtensionUIRefresh` feature flag is enabled,
|
||||
* the self-hosted settings dialog is opened directly. Otherwise, the
|
||||
* `onOpenSelfHostedSettings` event is emitted.
|
||||
* Opens the self-hosted settings dialog when the self-hosted option is selected.
|
||||
*/
|
||||
if (option === Region.SelfHosted) {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.UnauthenticatedExtensionUIRefresh)) {
|
||||
if (await SelfHostedEnvConfigDialogComponent.open(this.dialogService)) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.onOpenSelfHostedSettings.emit();
|
||||
}
|
||||
if (
|
||||
option === Region.SelfHosted &&
|
||||
(await SelfHostedEnvConfigDialogComponent.open(this.dialogService))
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, NavigationSkipped, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, of } from "rxjs";
|
||||
import { switchMap, take, takeUntil } from "rxjs/operators";
|
||||
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
PasswordLoginCredentials,
|
||||
} 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 { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.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";
|
||||
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 { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import {
|
||||
AllValidationErrors,
|
||||
FormValidationErrorsService,
|
||||
} from "../../platform/abstractions/form-validation-errors.service";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
@Directive()
|
||||
export class LoginComponentV1 extends CaptchaProtectedComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("masterPasswordInput", { static: true }) masterPasswordInput: ElementRef;
|
||||
|
||||
showPassword = false;
|
||||
formPromise: Promise<AuthResult>;
|
||||
|
||||
onSuccessfulLogin: () => Promise<any>;
|
||||
onSuccessfulLoginNavigate: (userId: UserId) => Promise<any>;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||
|
||||
showLoginWithDevice: boolean;
|
||||
validatedEmail = false;
|
||||
paramEmailSet = false;
|
||||
|
||||
get emailFormControl() {
|
||||
return this.formGroup.controls.email;
|
||||
}
|
||||
|
||||
formGroup = this.formBuilder.nonNullable.group({
|
||||
email: ["", [Validators.required, Validators.email]],
|
||||
masterPassword: [
|
||||
"",
|
||||
[Validators.required, Validators.minLength(Utils.originalMinimumPasswordLength)],
|
||||
],
|
||||
rememberEmail: [false],
|
||||
});
|
||||
|
||||
protected twoFactorRoute = "2fa";
|
||||
protected successRoute = "vault";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
|
||||
protected destroy$ = new Subject<void>();
|
||||
|
||||
get loggedEmail() {
|
||||
return this.formGroup.controls.email.value;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected devicesApiService: DevicesApiServiceAbstraction,
|
||||
protected appIdService: AppIdService,
|
||||
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
protected router: Router,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
protected stateService: StateService,
|
||||
environmentService: EnvironmentService,
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected logService: LogService,
|
||||
protected ngZone: NgZone,
|
||||
protected formBuilder: FormBuilder,
|
||||
protected formValidationErrorService: FormValidationErrorsService,
|
||||
protected route: ActivatedRoute,
|
||||
protected loginEmailService: LoginEmailServiceAbstraction,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService, toastService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route?.queryParams
|
||||
.pipe(
|
||||
switchMap((params) => {
|
||||
if (!params) {
|
||||
// If no params,loadEmailSettings from state
|
||||
return this.loadEmailSettings();
|
||||
}
|
||||
|
||||
const queryParamsEmail = params.email;
|
||||
|
||||
if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) {
|
||||
this.formGroup.controls.email.setValue(queryParamsEmail);
|
||||
this.paramEmailSet = true;
|
||||
}
|
||||
|
||||
// If paramEmailSet is false, loadEmailSettings from state
|
||||
return this.paramEmailSet ? of(null) : this.loadEmailSettings();
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// If the user navigates to /login from /login, reset the validatedEmail flag
|
||||
// This should bring the user back to the login screen with the email field
|
||||
this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => {
|
||||
if (event instanceof NavigationSkipped && event.url === "/login") {
|
||||
this.validatedEmail = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Backup check to handle unknown case where activatedRoute is not available
|
||||
// This shouldn't happen under normal circumstances
|
||||
if (!this.route) {
|
||||
await this.loadEmailSettings();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async submit(showToast = true) {
|
||||
await this.setupCaptcha();
|
||||
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
//web
|
||||
if (this.formGroup.invalid && !showToast) {
|
||||
return;
|
||||
}
|
||||
|
||||
//desktop, browser; This should be removed once all clients use reactive forms
|
||||
if (this.formGroup.invalid && showToast) {
|
||||
const errorText = this.getErrorToastMessage();
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: errorText,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
this.formGroup.controls.email.value,
|
||||
this.formGroup.controls.masterPassword.value,
|
||||
this.captchaToken,
|
||||
undefined,
|
||||
);
|
||||
|
||||
this.formPromise = this.loginStrategyService.logIn(credentials);
|
||||
const response = await this.formPromise;
|
||||
|
||||
await this.saveEmailSettings();
|
||||
|
||||
if (this.handleCaptchaRequired(response)) {
|
||||
return;
|
||||
} else if (await this.handleMigrateEncryptionKey(response)) {
|
||||
return;
|
||||
} else if (response.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != 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.onSuccessfulLoginTwoFactorNavigate();
|
||||
} else {
|
||||
// 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.twoFactorRoute]);
|
||||
}
|
||||
} else if (response.forcePasswordReset != ForceSetPasswordReason.None) {
|
||||
if (this.onSuccessfulLoginForceResetNavigate != 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.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]);
|
||||
}
|
||||
} else {
|
||||
if (this.onSuccessfulLogin != 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.onSuccessfulLogin();
|
||||
}
|
||||
|
||||
if (this.onSuccessfulLoginNavigate != 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.onSuccessfulLoginNavigate(response.userId);
|
||||
} 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]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
if (this.ngZone.isStable) {
|
||||
document.getElementById("masterPassword").focus();
|
||||
} else {
|
||||
this.ngZone.onStable
|
||||
.pipe(take(1))
|
||||
.subscribe(() => document.getElementById("masterPassword").focus());
|
||||
}
|
||||
}
|
||||
|
||||
async startAuthRequestLogin() {
|
||||
this.formGroup.get("masterPassword")?.clearValidators();
|
||||
this.formGroup.get("masterPassword")?.updateValueAndValidity();
|
||||
|
||||
if (!this.formGroup.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.saveEmailSettings();
|
||||
await this.router.navigate(["/login-with-device"]);
|
||||
}
|
||||
|
||||
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
||||
// Save off email for SSO
|
||||
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
||||
|
||||
// Generate necessary sso params
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
|
||||
// Save sso params
|
||||
await this.ssoLoginService.setSsoState(state);
|
||||
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
|
||||
|
||||
// Build URI
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webUrl = env.getWebVaultUrl();
|
||||
|
||||
// Launch browser
|
||||
this.platformUtilsService.launchUri(
|
||||
webUrl +
|
||||
"/#/sso?clientId=" +
|
||||
clientId +
|
||||
"&redirectUri=" +
|
||||
encodeURIComponent(ssoRedirectUri) +
|
||||
"&state=" +
|
||||
state +
|
||||
"&codeChallenge=" +
|
||||
codeChallenge +
|
||||
"&email=" +
|
||||
encodeURIComponent(this.formGroup.controls.email.value),
|
||||
);
|
||||
}
|
||||
|
||||
async validateEmail() {
|
||||
this.formGroup.controls.email.markAsTouched();
|
||||
const emailValid = this.formGroup.get("email").valid;
|
||||
|
||||
if (emailValid) {
|
||||
this.toggleValidateEmail(true);
|
||||
await this.getLoginWithDevice(this.loggedEmail);
|
||||
}
|
||||
}
|
||||
|
||||
toggleValidateEmail(value: boolean) {
|
||||
this.validatedEmail = value;
|
||||
if (!this.validatedEmail) {
|
||||
// Reset master password only when going from validated to not validated
|
||||
// so that autofill can work properly
|
||||
this.formGroup.controls.masterPassword.reset();
|
||||
} else {
|
||||
// Mark MP as untouched so that, when users enter email and hit enter,
|
||||
// the MP field doesn't load with validation errors
|
||||
this.formGroup.controls.masterPassword.markAsUntouched();
|
||||
|
||||
// When email is validated, focus on master password after
|
||||
// waiting for input to be rendered
|
||||
if (this.ngZone.isStable) {
|
||||
this.masterPasswordInput?.nativeElement?.focus();
|
||||
} else {
|
||||
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
|
||||
this.masterPasswordInput?.nativeElement?.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadEmailSettings() {
|
||||
// Try to load from memory first
|
||||
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async saveEmailSettings() {
|
||||
// Save off email for SSO
|
||||
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
||||
|
||||
this.loginEmailService.setLoginEmail(this.formGroup.value.email);
|
||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
}
|
||||
|
||||
// Legacy accounts used the master key to encrypt data. Migration is required but only performed on web
|
||||
protected async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccured"),
|
||||
message: this.i18nService.t("encryptionKeyMigrationRequired"),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private getErrorToastMessage() {
|
||||
const error: AllValidationErrors = this.formValidationErrorService
|
||||
.getFormValidationErrors(this.formGroup.controls)
|
||||
.shift();
|
||||
|
||||
if (error) {
|
||||
switch (error.errorName) {
|
||||
case "email":
|
||||
return this.i18nService.t("invalidEmail");
|
||||
case "minlength":
|
||||
return this.i18nService.t("masterPasswordMinlength", Utils.originalMinimumPasswordLength);
|
||||
default:
|
||||
return this.i18nService.t(this.errorTag(error));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private errorTag(error: AllValidationErrors): string {
|
||||
const name = error.errorName.charAt(0).toUpperCase() + error.errorName.slice(1);
|
||||
return `${error.controlName}${name}`;
|
||||
}
|
||||
|
||||
async getLoginWithDevice(email: string) {
|
||||
try {
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
this.showLoginWithDevice = await this.devicesApiService.getKnownDevice(
|
||||
email,
|
||||
deviceIdentifier,
|
||||
);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
this.showLoginWithDevice = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,538 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { IsActiveMatchOptions, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, map, takeUntil } from "rxjs";
|
||||
|
||||
import {
|
||||
AuthRequestLoginCredentials,
|
||||
AuthRequestServiceAbstraction,
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums/http-status-code.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.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";
|
||||
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 { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
enum State {
|
||||
StandardAuthRequest,
|
||||
AdminAuthRequest,
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export class LoginViaAuthRequestComponentV1
|
||||
extends CaptchaProtectedComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private destroy$ = new Subject<void>();
|
||||
userAuthNStatus: AuthenticationStatus;
|
||||
email: string;
|
||||
showResendNotification = false;
|
||||
authRequest: AuthRequest;
|
||||
fingerprintPhrase: string;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||
onSuccessfulLogin: () => Promise<any>;
|
||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||
|
||||
protected adminApprovalRoute = "admin-approval-requested";
|
||||
|
||||
protected StateEnum = State;
|
||||
protected state = State.StandardAuthRequest;
|
||||
protected webVaultUrl: string;
|
||||
protected twoFactorRoute = "2fa";
|
||||
protected successRoute = "vault";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
private resendTimeout = 12000;
|
||||
protected deviceManagementUrl: string;
|
||||
|
||||
private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array };
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
private keyService: KeyService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private appIdService: AppIdService,
|
||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private apiService: ApiService,
|
||||
private authService: AuthService,
|
||||
private logService: LogService,
|
||||
environmentService: EnvironmentService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private anonymousHubService: AnonymousHubService,
|
||||
private validationService: ValidationService,
|
||||
private accountService: AccountService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService, toastService);
|
||||
|
||||
// Get the web vault URL from the environment service
|
||||
environmentService.environment$.pipe(takeUntil(this.destroy$)).subscribe((env) => {
|
||||
this.webVaultUrl = env.getWebVaultUrl();
|
||||
this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`;
|
||||
});
|
||||
|
||||
// Gets signalR push notification
|
||||
// Only fires on approval to prevent enumeration
|
||||
this.authRequestService.authRequestPushNotification$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((id) => {
|
||||
this.verifyAndHandleApprovedAuthReq(id).catch((e: Error) => {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("error"),
|
||||
message: e.message,
|
||||
});
|
||||
this.logService.error("Failed to use approved auth request: " + e.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
this.userAuthNStatus = await this.authService.getAuthStatus();
|
||||
|
||||
const matchOptions: IsActiveMatchOptions = {
|
||||
paths: "exact",
|
||||
queryParams: "ignored",
|
||||
fragment: "ignored",
|
||||
matrixParams: "ignored",
|
||||
};
|
||||
|
||||
if (this.router.isActive(this.adminApprovalRoute, matchOptions)) {
|
||||
this.state = State.AdminAuthRequest;
|
||||
}
|
||||
|
||||
if (this.state === State.AdminAuthRequest) {
|
||||
// Pull email from state for admin auth reqs b/c it is available
|
||||
// This also prevents it from being lost on refresh as the
|
||||
// login service email does not persist.
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
|
||||
if (!this.email) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("userEmailMissing"),
|
||||
});
|
||||
// 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(["/login-initiated"]);
|
||||
return;
|
||||
}
|
||||
|
||||
// We only allow a single admin approval request to be active at a time
|
||||
// so must check state to see if we have an existing one or not
|
||||
const adminAuthReqStorable = await this.authRequestService.getAdminAuthRequest(userId);
|
||||
|
||||
if (adminAuthReqStorable) {
|
||||
await this.handleExistingAdminAuthRequest(adminAuthReqStorable, userId);
|
||||
} else {
|
||||
// No existing admin auth request; so we need to create one
|
||||
await this.startAuthRequestLogin();
|
||||
}
|
||||
} else {
|
||||
// Standard auth request
|
||||
// TODO: evaluate if we can remove the setting of this.email in the constructor
|
||||
this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
|
||||
if (!this.email) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("userEmailMissing"),
|
||||
});
|
||||
// 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(["/login"]);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.startAuthRequestLogin();
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
await this.anonymousHubService.stopHubConnection();
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private async handleExistingAdminAuthRequest(
|
||||
adminAuthReqStorable: AdminAuthRequestStorable,
|
||||
userId: UserId,
|
||||
) {
|
||||
// Note: on login, the SSOLoginStrategy will also call to see an existing admin auth req
|
||||
// has been approved and handle it if so.
|
||||
|
||||
// Regardless, we always retrieve the auth request from the server verify and handle status changes here as well
|
||||
let adminAuthReqResponse: AuthRequestResponse;
|
||||
try {
|
||||
adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id);
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
||||
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
||||
}
|
||||
}
|
||||
|
||||
// Request doesn't exist anymore
|
||||
if (!adminAuthReqResponse) {
|
||||
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
||||
}
|
||||
|
||||
// Re-derive the user's fingerprint phrase
|
||||
// It is important to not use the server's public key here as it could have been compromised via MITM
|
||||
const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey(
|
||||
adminAuthReqStorable.privateKey,
|
||||
);
|
||||
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||
this.email,
|
||||
derivedPublicKeyArrayBuffer,
|
||||
);
|
||||
|
||||
// Request denied
|
||||
if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) {
|
||||
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
||||
}
|
||||
|
||||
// Request approved
|
||||
if (adminAuthReqResponse.requestApproved) {
|
||||
return await this.handleApprovedAdminAuthRequest(
|
||||
adminAuthReqResponse,
|
||||
adminAuthReqStorable.privateKey,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
// Request still pending response from admin
|
||||
// set keypair and create hub connection so that any approvals will be received via push notification
|
||||
this.authRequestKeyPair = { privateKey: adminAuthReqStorable.privateKey, publicKey: null };
|
||||
await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id);
|
||||
}
|
||||
|
||||
private async handleExistingAdminAuthReqDeletedOrDenied(userId: UserId) {
|
||||
// clear the admin auth request from state
|
||||
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||
|
||||
// start new auth request
|
||||
// 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.startAuthRequestLogin();
|
||||
}
|
||||
|
||||
private async buildAuthRequest(authRequestType: AuthRequestType) {
|
||||
const authRequestKeyPairArray = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
||||
|
||||
this.authRequestKeyPair = {
|
||||
publicKey: authRequestKeyPairArray[0],
|
||||
privateKey: authRequestKeyPairArray[1],
|
||||
};
|
||||
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey);
|
||||
const accessCode = await this.passwordGenerationService.generatePassword({
|
||||
type: "password",
|
||||
length: 25,
|
||||
});
|
||||
|
||||
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||
this.email,
|
||||
this.authRequestKeyPair.publicKey,
|
||||
);
|
||||
|
||||
this.authRequest = new AuthRequest(
|
||||
this.email,
|
||||
deviceIdentifier,
|
||||
publicKey,
|
||||
authRequestType,
|
||||
accessCode,
|
||||
);
|
||||
}
|
||||
|
||||
async startAuthRequestLogin() {
|
||||
this.showResendNotification = false;
|
||||
|
||||
try {
|
||||
let reqResponse: AuthRequestResponse;
|
||||
|
||||
if (this.state === State.AdminAuthRequest) {
|
||||
await this.buildAuthRequest(AuthRequestType.AdminApproval);
|
||||
reqResponse = await this.apiService.postAdminAuthRequest(this.authRequest);
|
||||
|
||||
const adminAuthReqStorable = new AdminAuthRequestStorable({
|
||||
id: reqResponse.id,
|
||||
privateKey: this.authRequestKeyPair.privateKey,
|
||||
});
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId);
|
||||
} else {
|
||||
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
|
||||
reqResponse = await this.apiService.postAuthRequest(this.authRequest);
|
||||
}
|
||||
|
||||
if (reqResponse.id) {
|
||||
await this.anonymousHubService.createHubConnection(reqResponse.id);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.showResendNotification = true;
|
||||
}, this.resendTimeout);
|
||||
}
|
||||
|
||||
private async verifyAndHandleApprovedAuthReq(requestId: string) {
|
||||
try {
|
||||
// Retrieve the auth request from server and verify it's approved
|
||||
let authReqResponse: AuthRequestResponse;
|
||||
|
||||
switch (this.state) {
|
||||
case State.StandardAuthRequest:
|
||||
// Unauthed - access code required for user verification
|
||||
authReqResponse = await this.apiService.getAuthResponse(
|
||||
requestId,
|
||||
this.authRequest.accessCode,
|
||||
);
|
||||
break;
|
||||
|
||||
case State.AdminAuthRequest:
|
||||
// Authed - no access code required
|
||||
authReqResponse = await this.apiService.getAuthRequest(requestId);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!authReqResponse.requestApproved) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Approved so proceed:
|
||||
|
||||
// 4 Scenarios to handle for approved auth requests:
|
||||
// Existing flow 1:
|
||||
// - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(masterKey)
|
||||
// > decrypt masterKey > must authenticate > gets masterKey(userKey) > decrypt userKey and proceed to vault
|
||||
|
||||
// 3 new flows from TDE:
|
||||
// Flow 2:
|
||||
// - Post SSO > User is AuthN > SSO login strategy success sets masterKey(userKey) > receives approval from device with pubKey(masterKey)
|
||||
// > decrypt masterKey > decrypt userKey > establish trust if required > proceed to vault
|
||||
// Flow 3:
|
||||
// - Post SSO > User is AuthN > Receives approval from device with pubKey(userKey) > decrypt userKey > establish trust if required > proceed to vault
|
||||
// Flow 4:
|
||||
// - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(userKey)
|
||||
// > decrypt userKey > must authenticate > set userKey > proceed to vault
|
||||
|
||||
// if user has authenticated via SSO
|
||||
if (this.userAuthNStatus === AuthenticationStatus.Locked) {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
return await this.handleApprovedAdminAuthRequest(
|
||||
authReqResponse,
|
||||
this.authRequestKeyPair.privateKey,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
// Flow 1 and 4:
|
||||
const loginAuthResult = await this.loginViaAuthRequestStrategy(requestId, authReqResponse);
|
||||
await this.handlePostLoginNavigation(loginAuthResult);
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse) {
|
||||
let errorRoute = "/login";
|
||||
if (this.state === State.AdminAuthRequest) {
|
||||
errorRoute = "/login-initiated";
|
||||
}
|
||||
|
||||
// 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([errorRoute]);
|
||||
this.validationService.showError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async handleApprovedAdminAuthRequest(
|
||||
adminAuthReqResponse: AuthRequestResponse,
|
||||
privateKey: ArrayBuffer,
|
||||
userId: UserId,
|
||||
) {
|
||||
// See verifyAndHandleApprovedAuthReq(...) for flow details
|
||||
// it's flow 2 or 3 based on presence of masterPasswordHash
|
||||
if (adminAuthReqResponse.masterPasswordHash) {
|
||||
// Flow 2: masterPasswordHash is not null
|
||||
// key is authRequestPublicKey(masterKey) + we have authRequestPublicKey(masterPasswordHash)
|
||||
await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash(
|
||||
adminAuthReqResponse,
|
||||
privateKey,
|
||||
userId,
|
||||
);
|
||||
} else {
|
||||
// Flow 3: masterPasswordHash is null
|
||||
// we can assume key is authRequestPublicKey(userKey) and we can just decrypt with userKey and proceed to vault
|
||||
await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey(
|
||||
adminAuthReqResponse,
|
||||
privateKey,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
// clear the admin auth request from state so it cannot be used again (it's a one time use)
|
||||
// TODO: this should eventually be enforced via deleting this on the server once it is used
|
||||
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("loginApproved"),
|
||||
});
|
||||
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
// need to establish trust on the current device
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);
|
||||
|
||||
// TODO: don't forget to use auto enrollment service everywhere we trust device
|
||||
|
||||
await this.handleSuccessfulLoginNavigation();
|
||||
}
|
||||
|
||||
// Authentication helper
|
||||
private async buildAuthRequestLoginCredentials(
|
||||
requestId: string,
|
||||
response: AuthRequestResponse,
|
||||
): Promise<AuthRequestLoginCredentials> {
|
||||
// if masterPasswordHash has a value, we will always receive key as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash)
|
||||
// if masterPasswordHash is null, we will always receive key as authRequestPublicKey(userKey)
|
||||
if (response.masterPasswordHash) {
|
||||
const { masterKey, masterKeyHash } =
|
||||
await this.authRequestService.decryptPubKeyEncryptedMasterKeyAndHash(
|
||||
response.key,
|
||||
response.masterPasswordHash,
|
||||
this.authRequestKeyPair.privateKey,
|
||||
);
|
||||
|
||||
return new AuthRequestLoginCredentials(
|
||||
this.email,
|
||||
this.authRequest.accessCode,
|
||||
requestId,
|
||||
null, // no userKey
|
||||
masterKey,
|
||||
masterKeyHash,
|
||||
);
|
||||
} else {
|
||||
const userKey = await this.authRequestService.decryptPubKeyEncryptedUserKey(
|
||||
response.key,
|
||||
this.authRequestKeyPair.privateKey,
|
||||
);
|
||||
return new AuthRequestLoginCredentials(
|
||||
this.email,
|
||||
this.authRequest.accessCode,
|
||||
requestId,
|
||||
userKey,
|
||||
null, // no masterKey
|
||||
null, // no masterKeyHash
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async loginViaAuthRequestStrategy(
|
||||
requestId: string,
|
||||
authReqResponse: AuthRequestResponse,
|
||||
): Promise<AuthResult> {
|
||||
// Note: credentials change based on if the authReqResponse.key is a encryptedMasterKey or UserKey
|
||||
const credentials = await this.buildAuthRequestLoginCredentials(requestId, authReqResponse);
|
||||
|
||||
// Note: keys are set by AuthRequestLoginStrategy success handling
|
||||
return await this.loginStrategyService.logIn(credentials);
|
||||
}
|
||||
|
||||
// Routing logic
|
||||
private async handlePostLoginNavigation(loginResponse: AuthResult) {
|
||||
if (loginResponse.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != 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.onSuccessfulLoginTwoFactorNavigate();
|
||||
} else {
|
||||
// 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.twoFactorRoute]);
|
||||
}
|
||||
} else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) {
|
||||
if (this.onSuccessfulLoginForceResetNavigate != 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.onSuccessfulLoginForceResetNavigate();
|
||||
} else {
|
||||
// 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]);
|
||||
}
|
||||
} else {
|
||||
await this.handleSuccessfulLoginNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSuccessfulLoginNavigation() {
|
||||
if (this.state === State.StandardAuthRequest) {
|
||||
// Only need to set remembered email on standard login with auth req flow
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
}
|
||||
|
||||
if (this.onSuccessfulLogin != 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.onSuccessfulLogin();
|
||||
}
|
||||
|
||||
if (this.onSuccessfulLoginNavigate != 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.onSuccessfulLoginNavigate();
|
||||
} else {
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
@@ -62,6 +63,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
policyService: PolicyService,
|
||||
protected router: Router,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private apiService: ApiService,
|
||||
private syncService: SyncService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -195,7 +197,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
);
|
||||
try {
|
||||
if (this.resetPasswordAutoEnroll) {
|
||||
this.formPromise = this.apiService
|
||||
this.formPromise = this.masterPasswordApiService
|
||||
.setPassword(request)
|
||||
.then(async () => {
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
@@ -222,7 +224,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.apiService.setPassword(request).then(async () => {
|
||||
this.formPromise = this.masterPasswordApiService.setPassword(request).then(async () => {
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import { Directive } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
@@ -40,7 +40,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||
policyService: PolicyService,
|
||||
keyService: KeyService,
|
||||
messagingService: MessagingService,
|
||||
private apiService: ApiService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private logService: LogService,
|
||||
dialogService: DialogService,
|
||||
@@ -117,9 +117,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
|
||||
// Update user's password
|
||||
// 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.apiService.postPassword(request);
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Directive, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
@@ -52,7 +52,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
|
||||
policyService: PolicyService,
|
||||
keyService: KeyService,
|
||||
messagingService: MessagingService,
|
||||
private apiService: ApiService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private syncService: SyncService,
|
||||
private logService: LogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
@@ -202,7 +202,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.masterPasswordHint = this.hint;
|
||||
|
||||
return this.apiService.putUpdateTempPassword(request);
|
||||
return this.masterPasswordApiService.putUpdateTempPassword(request);
|
||||
}
|
||||
|
||||
private async updatePassword(newMasterPasswordHash: string, userKey: [UserKey, EncString]) {
|
||||
@@ -214,7 +214,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
|
||||
request.newMasterPasswordHash = newMasterPasswordHash;
|
||||
request.key = userKey[1].encryptedString;
|
||||
|
||||
return this.apiService.postPassword(request);
|
||||
return this.masterPasswordApiService.postPassword(request);
|
||||
}
|
||||
|
||||
private async updateTdeOffboardingPassword(
|
||||
@@ -226,6 +226,6 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.masterPasswordHint = this.hint;
|
||||
|
||||
return this.apiService.putUpdateTdeOffboardingPassword(request);
|
||||
return this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Navigation, Router, UrlTree } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { unauthUiRefreshRedirect } from "./unauth-ui-refresh-redirect";
|
||||
|
||||
describe("unauthUiRefreshRedirect", () => {
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let router: MockProxy<Router>;
|
||||
|
||||
beforeEach(() => {
|
||||
configService = mock<ConfigService>();
|
||||
router = mock<Router>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns true when UnauthenticatedExtensionUIRefresh flag is disabled", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
const result = await TestBed.runInInjectionContext(() =>
|
||||
unauthUiRefreshRedirect("/redirect")(),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.UnauthenticatedExtensionUIRefresh,
|
||||
);
|
||||
expect(router.parseUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns UrlTree when UnauthenticatedExtensionUIRefresh flag is enabled and preserves query params", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
const urlTree = new UrlTree();
|
||||
urlTree.queryParams = { test: "test" };
|
||||
|
||||
const navigation: Navigation = {
|
||||
extras: {},
|
||||
id: 0,
|
||||
initialUrl: new UrlTree(),
|
||||
extractedUrl: urlTree,
|
||||
trigger: "imperative",
|
||||
previousNavigation: undefined,
|
||||
};
|
||||
|
||||
router.getCurrentNavigation.mockReturnValue(navigation);
|
||||
|
||||
await TestBed.runInInjectionContext(() => unauthUiRefreshRedirect("/redirect")());
|
||||
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.UnauthenticatedExtensionUIRefresh,
|
||||
);
|
||||
expect(router.createUrlTree).toHaveBeenCalledWith(["/redirect"], {
|
||||
queryParams: urlTree.queryParams,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { UrlTree, Router } from "@angular/router";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
/**
|
||||
* Helper function to redirect to a new URL based on the UnauthenticatedExtensionUIRefresh feature flag.
|
||||
* @param redirectUrl - The URL to redirect to if the UnauthenticatedExtensionUIRefresh flag is enabled.
|
||||
*/
|
||||
export function unauthUiRefreshRedirect(redirectUrl: string): () => Promise<boolean | UrlTree> {
|
||||
return async () => {
|
||||
const configService = inject(ConfigService);
|
||||
const router = inject(Router);
|
||||
const shouldRedirect = await configService.getFeatureFlag(
|
||||
FeatureFlag.UnauthenticatedExtensionUIRefresh,
|
||||
);
|
||||
if (shouldRedirect) {
|
||||
const currentNavigation = router.getCurrentNavigation();
|
||||
const queryParams = currentNavigation?.extractedUrl?.queryParams || {};
|
||||
|
||||
// Preserve query params when redirecting as it is likely that the refreshed component
|
||||
// will be consuming the same query params.
|
||||
return router.createUrlTree([redirectUrl], { queryParams });
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export abstract class DeviceTrustToastService {
|
||||
/**
|
||||
* An observable pipeline that observes any cross-application toast messages
|
||||
* that need to be shown as part of the trusted device encryption (TDE) process.
|
||||
*/
|
||||
abstract setupListeners$: Observable<void>;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { merge, Observable, tap } from "rxjs";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "./device-trust-toast.service.abstraction";
|
||||
|
||||
export class DeviceTrustToastService implements DeviceTrustToastServiceAbstraction {
|
||||
private adminLoginApproved$: Observable<void>;
|
||||
private deviceTrusted$: Observable<void>;
|
||||
|
||||
setupListeners$: Observable<void>;
|
||||
|
||||
constructor(
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
this.adminLoginApproved$ = this.authRequestService.adminLoginApproved$.pipe(
|
||||
tap(() => {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("loginApproved"),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
this.deviceTrusted$ = this.deviceTrustService.deviceTrusted$.pipe(
|
||||
tap(() => {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("deviceTrusted"),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
this.setupListeners$ = merge(this.adminLoginApproved$, this.deviceTrusted$);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { EMPTY, of } from "rxjs";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "./device-trust-toast.service.abstraction";
|
||||
import { DeviceTrustToastService } from "./device-trust-toast.service.implementation";
|
||||
|
||||
describe("DeviceTrustToastService", () => {
|
||||
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
|
||||
let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
|
||||
let sut: DeviceTrustToastServiceAbstraction;
|
||||
|
||||
beforeEach(() => {
|
||||
authRequestService = mock<AuthRequestServiceAbstraction>();
|
||||
deviceTrustService = mock<DeviceTrustServiceAbstraction>();
|
||||
i18nService = mock<I18nService>();
|
||||
toastService = mock<ToastService>();
|
||||
|
||||
i18nService.t.mockImplementation((key: string) => key); // just return the key that was given
|
||||
});
|
||||
|
||||
const initService = () => {
|
||||
return new DeviceTrustToastService(
|
||||
authRequestService,
|
||||
deviceTrustService,
|
||||
i18nService,
|
||||
toastService,
|
||||
);
|
||||
};
|
||||
|
||||
const loginApprovalToastOptions = {
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: "loginApproved",
|
||||
};
|
||||
|
||||
const deviceTrustedToastOptions = {
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: "deviceTrusted",
|
||||
};
|
||||
|
||||
describe("setupListeners$", () => {
|
||||
describe("given adminLoginApproved$ emits and deviceTrusted$ emits", () => {
|
||||
beforeEach(() => {
|
||||
// Arrange
|
||||
authRequestService.adminLoginApproved$ = of(undefined);
|
||||
deviceTrustService.deviceTrusted$ = of(undefined);
|
||||
sut = initService();
|
||||
});
|
||||
|
||||
it("should trigger a toast for login approval", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should trigger a toast for device trust", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given adminLoginApproved$ emits and deviceTrusted$ does not emit", () => {
|
||||
beforeEach(() => {
|
||||
// Arrange
|
||||
authRequestService.adminLoginApproved$ = of(undefined);
|
||||
deviceTrustService.deviceTrusted$ = EMPTY;
|
||||
sut = initService();
|
||||
});
|
||||
|
||||
it("should trigger a toast for login approval", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT trigger a toast for device trust", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).not.toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given adminLoginApproved$ does not emit and deviceTrusted$ emits", () => {
|
||||
beforeEach(() => {
|
||||
// Arrange
|
||||
authRequestService.adminLoginApproved$ = EMPTY;
|
||||
deviceTrustService.deviceTrusted$ = of(undefined);
|
||||
sut = initService();
|
||||
});
|
||||
|
||||
it("should NOT trigger a toast for login approval", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).not.toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should trigger a toast for device trust", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given adminLoginApproved$ does not emit and deviceTrusted$ does not emit", () => {
|
||||
beforeEach(() => {
|
||||
// Arrange
|
||||
authRequestService.adminLoginApproved$ = EMPTY;
|
||||
deviceTrustService.deviceTrusted$ = EMPTY;
|
||||
sut = initService();
|
||||
});
|
||||
|
||||
it("should NOT trigger a toast for login approval", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).not.toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT trigger a toast for device trust", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).not.toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -91,6 +91,7 @@ import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractio
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { MasterPasswordApiService as MasterPasswordApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import {
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
MasterPasswordServiceAbstraction,
|
||||
@@ -113,6 +114,7 @@ import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust
|
||||
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
|
||||
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
|
||||
import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation";
|
||||
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
|
||||
@@ -304,6 +306,8 @@ import {
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
import {
|
||||
DefaultTaskService,
|
||||
DefaultEndUserNotificationService,
|
||||
EndUserNotificationService,
|
||||
NewDeviceVerificationNoticeService,
|
||||
PasswordRepromptService,
|
||||
TaskService,
|
||||
@@ -317,6 +321,8 @@ import {
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
} from "@bitwarden/vault-export-core";
|
||||
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
||||
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { ViewCacheService } from "../platform/abstractions/view-cache.service";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
@@ -1281,7 +1287,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: BillingApiServiceAbstraction,
|
||||
useClass: BillingApiService,
|
||||
deps: [ApiServiceAbstraction, LogService, ToastService],
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TaxServiceAbstraction,
|
||||
@@ -1345,6 +1351,7 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultSetPasswordJitService,
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
MasterPasswordApiService,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
@@ -1462,6 +1469,26 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultTaskService,
|
||||
deps: [StateProvider, ApiServiceAbstraction, OrganizationServiceAbstraction, ConfigService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EndUserNotificationService,
|
||||
useClass: DefaultEndUserNotificationService,
|
||||
deps: [StateProvider, ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DeviceTrustToastServiceAbstraction,
|
||||
useClass: DeviceTrustToastService,
|
||||
deps: [
|
||||
AuthRequestServiceAbstraction,
|
||||
DeviceTrustServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
ToastService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MasterPasswordApiServiceAbstraction,
|
||||
useClass: MasterPasswordApiService,
|
||||
deps: [ApiServiceAbstraction, LogService],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -15,7 +15,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -41,7 +40,7 @@ import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { generate_ssh_key } from "@bitwarden/sdk-internal";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
@Directive()
|
||||
export class AddEditComponent implements OnInit, OnDestroy {
|
||||
@@ -131,7 +130,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
protected configService: ConfigService,
|
||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||
protected toastService: ToastService,
|
||||
private sdkService: SdkService,
|
||||
protected sdkService: SdkService,
|
||||
private sshImportPromptService: SshImportPromptService,
|
||||
) {
|
||||
this.typeOptions = [
|
||||
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
|
||||
@@ -207,10 +207,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
this.writeableCollections = await this.loadCollections();
|
||||
this.canUseReprompt = await this.passwordRepromptService.enabled();
|
||||
|
||||
const sshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem);
|
||||
if (sshKeysEnabled) {
|
||||
this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey });
|
||||
}
|
||||
this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey });
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -824,6 +821,15 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
|
||||
async importSshKeyFromClipboard() {
|
||||
const key = await this.sshImportPromptService.importSshKeyFromClipboard();
|
||||
if (key != null) {
|
||||
this.cipher.sshKey.privateKey = key.privateKey;
|
||||
this.cipher.sshKey.publicKey = key.publicKey;
|
||||
this.cipher.sshKey.keyFingerprint = key.keyFingerprint;
|
||||
}
|
||||
}
|
||||
|
||||
private async generateSshKey(showNotification: boolean = true) {
|
||||
await firstValueFrom(this.sdkService.client$);
|
||||
const sshKey = generate_ssh_key("Ed25519");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
@@ -10,7 +10,7 @@ import { TopLevelTreeNode } from "../models/top-level-tree-node.model";
|
||||
import { VaultFilter } from "../models/vault-filter.model";
|
||||
|
||||
@Directive()
|
||||
export class CollectionFilterComponent {
|
||||
export class CollectionFilterComponent implements OnInit {
|
||||
@Input() hide = false;
|
||||
@Input() collapsedFilterNodes: Set<string>;
|
||||
@Input() collectionNodes: DynamicTreeNode<CollectionView>;
|
||||
@@ -51,4 +51,13 @@ export class CollectionFilterComponent {
|
||||
async toggleCollapse(node: ITreeNodeObject) {
|
||||
this.onNodeCollapseStateChange.emit(node);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Populate the set with all node IDs so all nodes are collapsed initially.
|
||||
if (this.collectionNodes?.fullList) {
|
||||
this.collectionNodes.fullList.forEach((node) => {
|
||||
this.collapsedFilterNodes.add(node.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"@bitwarden/generator-history": ["../tools/generator/extensions/history/src"],
|
||||
"@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"],
|
||||
"@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"],
|
||||
"@bitwarden/importer/core": ["../importer/src"],
|
||||
"@bitwarden/importer-ui": ["../importer/src/components"],
|
||||
"@bitwarden/key-management": ["../key-management/src"],
|
||||
"@bitwarden/platform": ["../platform/src"],
|
||||
"@bitwarden/ui-common": ["../ui/common/src"],
|
||||
|
||||
Reference in New Issue
Block a user