mirror of
https://github.com/bitwarden/browser
synced 2026-02-08 04:33:38 +00:00
Merge branch 'main' into km/pm-18576/fix-missing-userid-on-remove-password
This commit is contained in:
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -8,6 +8,8 @@
|
||||
apps/desktop/desktop_native @bitwarden/team-platform-dev
|
||||
apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev
|
||||
apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev
|
||||
## No ownership for Cargo.toml to allow dependency updates
|
||||
apps/desktop/desktop_native/Cargo.toml
|
||||
|
||||
## Auth team files ##
|
||||
apps/browser/src/auth @bitwarden/team-auth-dev
|
||||
@@ -119,6 +121,7 @@ apps/browser/src/autofill @bitwarden/team-autofill-dev
|
||||
apps/desktop/src/autofill @bitwarden/team-autofill-dev
|
||||
libs/common/src/autofill @bitwarden/team-autofill-dev
|
||||
apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
|
||||
apps/desktop/src/app/components/fido2placeholder.component.ts @bitwarden/team-autofill-dev
|
||||
apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev
|
||||
# DuckDuckGo integration
|
||||
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
|
||||
|
||||
54
.github/renovate.json5
vendored
54
.github/renovate.json5
vendored
@@ -168,15 +168,20 @@
|
||||
matchPackageNames: [
|
||||
"@emotion/css",
|
||||
"@webcomponents/custom-elements",
|
||||
"bitwarden-russh",
|
||||
"bytes",
|
||||
"concurrently",
|
||||
"cross-env",
|
||||
"del",
|
||||
"ed25519",
|
||||
"lit",
|
||||
"patch-package",
|
||||
"pkcs8",
|
||||
"prettier",
|
||||
"prettier-plugin-tailwindcss",
|
||||
"rimraf",
|
||||
"ssh-encoding",
|
||||
"ssh-key",
|
||||
"@storybook/web-components-webpack5",
|
||||
"tabbable",
|
||||
"tldts",
|
||||
@@ -210,31 +215,68 @@
|
||||
"@types/node-forge",
|
||||
"@types/node-ipc",
|
||||
"@yao-pkg/pkg",
|
||||
"anyhow",
|
||||
"arboard",
|
||||
"babel-loader",
|
||||
"base64",
|
||||
"bindgen",
|
||||
"browserslist",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"core-foundation",
|
||||
"copy-webpack-plugin",
|
||||
"dirs",
|
||||
"electron",
|
||||
"electron-builder",
|
||||
"electron-log",
|
||||
"electron-reload",
|
||||
"electron-store",
|
||||
"electron-updater",
|
||||
"embed_plist",
|
||||
"futures",
|
||||
"hex",
|
||||
"homedir",
|
||||
"html-webpack-injector",
|
||||
"html-webpack-plugin",
|
||||
"interprocess",
|
||||
"json5",
|
||||
"keytar",
|
||||
"libc",
|
||||
"log",
|
||||
"lowdb",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"node-forge",
|
||||
"node-ipc",
|
||||
"oo7",
|
||||
"oslog",
|
||||
"pin-project",
|
||||
"pkg",
|
||||
"rand",
|
||||
"rxjs",
|
||||
"scopeguard",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simplelog",
|
||||
"sysinfo",
|
||||
"tsconfig-paths-webpack-plugin",
|
||||
"type-fest",
|
||||
"typenum",
|
||||
"typescript",
|
||||
"typescript-strict-plugin",
|
||||
"uniffi",
|
||||
"webpack",
|
||||
"webpack-cli",
|
||||
"webpack-dev-server",
|
||||
"webpack-node-externals",
|
||||
"widestring",
|
||||
"windows",
|
||||
"windows-registry",
|
||||
"zbus",
|
||||
"zbus_polkit",
|
||||
],
|
||||
description: "Platform owned dependencies",
|
||||
commitMessagePrefix: "[deps] Platform:",
|
||||
@@ -352,7 +394,17 @@
|
||||
reviewers: ["team:team-vault-dev"],
|
||||
},
|
||||
{
|
||||
matchPackageNames: ["@types/argon2-browser", "argon2", "argon2-browser", "big-integer"],
|
||||
matchPackageNames: [
|
||||
"@types/argon2-browser",
|
||||
"aes",
|
||||
"argon2",
|
||||
"argon2-browser",
|
||||
"big-integer",
|
||||
"cbc",
|
||||
"rsa",
|
||||
"russh-cryptovec",
|
||||
"sha2",
|
||||
],
|
||||
description: "Key Management owned dependencies",
|
||||
commitMessagePrefix: "[deps] KM:",
|
||||
reviewers: ["team:team-key-management-dev"],
|
||||
|
||||
@@ -385,6 +385,15 @@
|
||||
"editFolder": {
|
||||
"message": "Edit folder"
|
||||
},
|
||||
"editFolderWithName": {
|
||||
"message": "Edit folder: $FOLDERNAME$",
|
||||
"placeholders": {
|
||||
"foldername": {
|
||||
"content": "$1",
|
||||
"example": "Social"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newFolder": {
|
||||
"message": "New folder"
|
||||
},
|
||||
@@ -1670,6 +1679,9 @@
|
||||
"dragToSort": {
|
||||
"message": "Drag to sort"
|
||||
},
|
||||
"dragToReorder": {
|
||||
"message": "Drag to reorder"
|
||||
},
|
||||
"cfTypeText": {
|
||||
"message": "Text"
|
||||
},
|
||||
@@ -3326,9 +3338,6 @@
|
||||
"loginWithDevice": {
|
||||
"message": "Log in with device"
|
||||
},
|
||||
"loginWithDeviceEnabledInfo": {
|
||||
"message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?"
|
||||
},
|
||||
"fingerprintPhraseHeader": {
|
||||
"message": "Fingerprint phrase"
|
||||
},
|
||||
@@ -3341,9 +3350,6 @@
|
||||
"viewAllLogInOptions": {
|
||||
"message": "View all log in options"
|
||||
},
|
||||
"viewAllLoginOptionsV1": {
|
||||
"message": "View all log in options"
|
||||
},
|
||||
"notificationSentDevice": {
|
||||
"message": "A notification has been sent to your device."
|
||||
},
|
||||
@@ -3534,9 +3540,6 @@
|
||||
"adminApprovalRequestSentToAdmins": {
|
||||
"message": "Your request has been sent to your admin."
|
||||
},
|
||||
"youWillBeNotifiedOnceApproved": {
|
||||
"message": "You will be notified once approved."
|
||||
},
|
||||
"troubleLoggingIn": {
|
||||
"message": "Trouble logging in?"
|
||||
},
|
||||
@@ -4697,6 +4700,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"reorderWebsiteUriButton": {
|
||||
"message": "Reorder website URI. Use arrow key to move item up or down."
|
||||
},
|
||||
"reorderFieldUp": {
|
||||
"message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$",
|
||||
"placeholders": {
|
||||
@@ -5128,6 +5134,33 @@
|
||||
"extraWide": {
|
||||
"message": "Extra wide"
|
||||
},
|
||||
"sshKeyWrongPassword": {
|
||||
"message": "The password you entered is incorrect."
|
||||
},
|
||||
"importSshKey": {
|
||||
"message": "Import"
|
||||
},
|
||||
"confirmSshKeyPassword": {
|
||||
"message": "Confirm password"
|
||||
},
|
||||
"enterSshKeyPasswordDesc": {
|
||||
"message": "Enter the password for the SSH key."
|
||||
},
|
||||
"enterSshKeyPassword": {
|
||||
"message": "Enter password"
|
||||
},
|
||||
"invalidSshKey": {
|
||||
"message": "The SSH key is invalid"
|
||||
},
|
||||
"sshKeyTypeUnsupported": {
|
||||
"message": "The SSH key type is not supported"
|
||||
},
|
||||
"importSshKeyFromClipboard": {
|
||||
"message": "Import key from clipboard"
|
||||
},
|
||||
"sshKeyImported": {
|
||||
"message": "SSH key imported successfully"
|
||||
},
|
||||
"cannotRemoveViewOnlyCollections": {
|
||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<form #form (ngSubmit)="submit()">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/home">{{ "close" | i18n }}</button>
|
||||
<button type="button" routerLink="/login">{{ "close" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "appName" | i18n }}</span>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<app-header [noTheme]="true"></app-header>
|
||||
<div class="center-content">
|
||||
<div class="content login-page">
|
||||
<div class="logo-image"></div>
|
||||
<p class="lead text-center">{{ "loginOrCreateNewAccount" | i18n }}</p>
|
||||
<form #form [formGroup]="formGroup" (ngSubmit)="submit()">
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
||||
<input id="email" type="email" formControlName="email" appInputVerbatim="false" />
|
||||
</div>
|
||||
<environment-selector></environment-selector>
|
||||
<div class="remember-email-check">
|
||||
<input
|
||||
id="rememberEmail"
|
||||
type="checkbox"
|
||||
name="rememberEmail"
|
||||
formControlName="rememberEmail"
|
||||
/>
|
||||
<label for="rememberEmail">{{ "rememberEmail" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<button type="submit" class="btn primary block">
|
||||
<b>{{ "continue" | i18n }}</b>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="createAccountLink">
|
||||
{{ "newAroundHere" | i18n }}
|
||||
<a routerLink="/signup" (click)="setLoginEmailValues()">{{ "createAccount" | i18n }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,130 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, switchMap, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { AccountSwitcherService } from "./account-switching/services/account-switcher.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-home",
|
||||
templateUrl: "home.component.html",
|
||||
})
|
||||
export class HomeComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(EnvironmentSelectorComponent, { static: true })
|
||||
environmentSelector!: EnvironmentSelectorComponent;
|
||||
private destroyed$: Subject<void> = new Subject();
|
||||
|
||||
loginInitiated = false;
|
||||
formGroup = this.formBuilder.group({
|
||||
email: ["", [Validators.required, Validators.email]],
|
||||
rememberEmail: [false],
|
||||
});
|
||||
|
||||
constructor(
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router,
|
||||
private i18nService: I18nService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
private accountSwitcherService: AccountSwitcherService,
|
||||
private toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
private route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.listenForUnauthUiRefreshFlagChanges();
|
||||
|
||||
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
const rememberEmail = this.loginEmailService.getRememberEmail();
|
||||
|
||||
if (email != null) {
|
||||
this.formGroup.patchValue({ email, rememberEmail });
|
||||
} else {
|
||||
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
|
||||
|
||||
if (storedEmail != null) {
|
||||
this.formGroup.patchValue({ email: storedEmail, rememberEmail: true });
|
||||
}
|
||||
}
|
||||
|
||||
this.environmentSelector.onOpenSelfHostedSettings
|
||||
.pipe(
|
||||
switchMap(async () => {
|
||||
await this.setLoginEmailValues();
|
||||
await this.router.navigate(["environment"]);
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
||||
private listenForUnauthUiRefreshFlagChanges() {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
|
||||
.pipe(
|
||||
tap(async (flag) => {
|
||||
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
|
||||
if (flag) {
|
||||
const qParams = await firstValueFrom(this.route.queryParams);
|
||||
|
||||
const uniqueQueryParams = {
|
||||
...qParams,
|
||||
// adding a unique timestamp to the query params to force a reload
|
||||
t: new Date().getTime().toString(),
|
||||
};
|
||||
|
||||
await this.router.navigate(["/login"], {
|
||||
queryParams: uniqueQueryParams,
|
||||
});
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
get availableAccounts$() {
|
||||
return this.accountSwitcherService.availableAccounts$;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccured"),
|
||||
message: this.i18nService.t("invalidEmail"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await this.setLoginEmailValues();
|
||||
await this.router.navigate(["login"], {
|
||||
queryParams: { email: this.formGroup.controls.email.value },
|
||||
});
|
||||
}
|
||||
|
||||
async setLoginEmailValues() {
|
||||
// Note: Browser saves email settings here instead of the login component
|
||||
this.loginEmailService.setRememberEmail(this.formGroup.controls.rememberEmail.value);
|
||||
await this.loginEmailService.setLoginEmail(this.formGroup.controls.email.value);
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup">
|
||||
<header>
|
||||
<h1 class="login-center">
|
||||
<span class="title">{{ "logIn" | i18n }}</span>
|
||||
</h1>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<input id="email" type="text" formControlName="email" [hidden]="true" />
|
||||
<input
|
||||
id="rememberEmail"
|
||||
type="checkbox"
|
||||
formControlName="rememberEmail"
|
||||
[hidden]="true"
|
||||
/>
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
class="monospaced"
|
||||
formControlName="masterPassword"
|
||||
appInputVerbatim
|
||||
appAutofocus
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword()"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" [hidden]="!showCaptcha()">
|
||||
<iframe
|
||||
id="hcaptcha_iframe"
|
||||
height="80"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="button" class="btn link" routerLink="/hint" (click)="saveEmailSettings()">
|
||||
<b>{{ "getMasterPasswordHint" | i18n }}</b>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content login-buttons">
|
||||
<button type="submit" class="btn primary block" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading"
|
||||
><b>{{ "logInWithMasterPassword" | i18n }}</b></span
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="tw-mb-3" *ngIf="showLoginWithDevice">
|
||||
<button type="button" class="btn block" (click)="startAuthRequestLogin()">
|
||||
<span> <i class="bwi bwi-mobile"></i> {{ "loginWithDevice" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" (click)="launchSsoBrowser()" class="btn block">
|
||||
<i class="bwi bwi-provider" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
|
||||
</button>
|
||||
<div class="small">
|
||||
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ loggedEmail }}</p>
|
||||
<a routerLink="/home">{{ "notYou" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</form>
|
||||
@@ -1,142 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, NgZone, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
} 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 { 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@Component({
|
||||
selector: "app-login",
|
||||
templateUrl: "login-v1.component.html",
|
||||
})
|
||||
export class LoginComponentV1 extends BaseLoginComponent implements OnInit {
|
||||
constructor(
|
||||
devicesApiService: DevicesApiServiceAbstraction,
|
||||
appIdService: AppIdService,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected i18nService: I18nService,
|
||||
protected stateService: StateService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
syncService: SyncService,
|
||||
logService: LogService,
|
||||
ngZone: NgZone,
|
||||
formBuilder: FormBuilder,
|
||||
formValidationErrorService: FormValidationErrorsService,
|
||||
route: ActivatedRoute,
|
||||
loginEmailService: LoginEmailServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
devicesApiService,
|
||||
appIdService,
|
||||
loginStrategyService,
|
||||
router,
|
||||
platformUtilsService,
|
||||
i18nService,
|
||||
stateService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
cryptoFunctionService,
|
||||
logService,
|
||||
ngZone,
|
||||
formBuilder,
|
||||
formValidationErrorService,
|
||||
route,
|
||||
loginEmailService,
|
||||
ssoLoginService,
|
||||
toastService,
|
||||
);
|
||||
this.onSuccessfulLogin = async () => {
|
||||
await syncService.fullSync(true);
|
||||
};
|
||||
this.successRoute = "/tabs/vault";
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await super.ngOnInit();
|
||||
await this.validateEmail();
|
||||
}
|
||||
|
||||
settings() {
|
||||
// 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(["environment"]);
|
||||
}
|
||||
|
||||
async launchSsoBrowser() {
|
||||
// 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)) +
|
||||
":clientId=browser";
|
||||
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
|
||||
await this.ssoLoginService.setCodeVerifier(codeVerifier);
|
||||
await this.ssoLoginService.setSsoState(state);
|
||||
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
let url = env.getWebVaultUrl();
|
||||
if (url == null) {
|
||||
url = "https://vault.bitwarden.com";
|
||||
}
|
||||
|
||||
const redirectUri = url + "/sso-connector.html";
|
||||
|
||||
// Launch browser
|
||||
this.platformUtilsService.launchUri(
|
||||
url +
|
||||
"/#/sso?clientId=browser" +
|
||||
"&redirectUri=" +
|
||||
encodeURIComponent(redirectUri) +
|
||||
"&state=" +
|
||||
state +
|
||||
"&codeChallenge=" +
|
||||
codeChallenge +
|
||||
"&email=" +
|
||||
encodeURIComponent(this.formGroup.controls.email.value),
|
||||
);
|
||||
}
|
||||
|
||||
async saveEmailSettings() {
|
||||
// values should be saved on home component
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<div class="login-with-device">
|
||||
<header>
|
||||
<h1 class="login-center">
|
||||
<span class="title">{{ "logIn" | i18n }}</span>
|
||||
</h1>
|
||||
</header>
|
||||
<div class="content login-page">
|
||||
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
|
||||
<div>
|
||||
<p class="lead">{{ "logInRequestSent" | i18n }}</p>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
{{ "notificationSentDevicePart1" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
class="tw-cursor-pointer"
|
||||
[href]="deviceManagementUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>{{ "notificationSentDeviceAnchor" | i18n }}</a
|
||||
>. {{ "notificationSentDevicePart2" | i18n }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<b class="fingerprint-phrase-header">{{ "fingerprintPhraseHeader" | i18n }}</b>
|
||||
<p class="fingerprint-text">
|
||||
<code>{{ fingerprintPhrase }}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="resend-notification" *ngIf="showResendNotification">
|
||||
<a (click)="startAuthRequestLogin()">{{ "resendNotification" | i18n }}</a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
{{ "loginWithDeviceEnabledInfo" | i18n }}
|
||||
<a href="#" (click)="back()">{{ "viewAllLoginOptionsV1" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
|
||||
<div>
|
||||
<p class="lead">{{ "adminApprovalRequested" | i18n }}</p>
|
||||
|
||||
<div>
|
||||
<p>{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
|
||||
<p>{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<b class="fingerprint-phrase-header">{{ "fingerprintPhraseHeader" | i18n }}</b>
|
||||
<p class="fingerprint-text">
|
||||
<code>{{ fingerprintPhrase }}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
{{ "troubleLoggingIn" | i18n }}
|
||||
<a routerLink="/login-initiated">{{ "viewAllLoginOptionsV1" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,85 +0,0 @@
|
||||
import { Location } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { LoginViaAuthRequestComponentV1 as BaseLoginViaAuthRequestComponentV1 } from "@bitwarden/angular/auth/components/login-via-auth-request-v1.component";
|
||||
import {
|
||||
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 { 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
selector: "app-login-via-auth-request",
|
||||
templateUrl: "login-via-auth-request-v1.component.html",
|
||||
})
|
||||
export class LoginViaAuthRequestComponentV1 extends BaseLoginViaAuthRequestComponentV1 {
|
||||
constructor(
|
||||
router: Router,
|
||||
keyService: KeyService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
appIdService: AppIdService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
apiService: ApiService,
|
||||
authService: AuthService,
|
||||
logService: LogService,
|
||||
environmentService: EnvironmentService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
anonymousHubService: AnonymousHubService,
|
||||
validationService: ValidationService,
|
||||
loginEmailService: LoginEmailServiceAbstraction,
|
||||
syncService: SyncService,
|
||||
deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
authRequestService: AuthRequestServiceAbstraction,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
private location: Location,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
keyService,
|
||||
cryptoFunctionService,
|
||||
appIdService,
|
||||
passwordGenerationService,
|
||||
apiService,
|
||||
authService,
|
||||
logService,
|
||||
environmentService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
anonymousHubService,
|
||||
validationService,
|
||||
accountService,
|
||||
loginEmailService,
|
||||
deviceTrustService,
|
||||
authRequestService,
|
||||
loginStrategyService,
|
||||
toastService,
|
||||
);
|
||||
this.onSuccessfulLogin = async () => {
|
||||
await syncService.fullSync(true);
|
||||
};
|
||||
}
|
||||
|
||||
protected back() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/home">{{ "cancel" | i18n }}</button>
|
||||
<button type="button" routerLink="/login">{{ "cancel" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "setMasterPassword" | i18n }}</span>
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("AuthPopoutWindow", () => {
|
||||
|
||||
it("closes any existing popup window types that are open to the login extension route", async () => {
|
||||
const loginTab = createChromeTabMock({
|
||||
url: chrome.runtime.getURL("popup/index.html#/home"),
|
||||
url: chrome.runtime.getURL("popup/index.html#/login"),
|
||||
});
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([loginTab]);
|
||||
jest.spyOn(BrowserApi, "removeWindow");
|
||||
|
||||
@@ -13,7 +13,7 @@ const AuthPopoutType = {
|
||||
|
||||
const extensionUnlockUrls = new Set([
|
||||
chrome.runtime.getURL("popup/index.html#/lock"),
|
||||
chrome.runtime.getURL("popup/index.html#/home"),
|
||||
chrome.runtime.getURL("popup/index.html#/login"),
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -364,7 +364,7 @@ export class AutofillComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
BrowserApi.updateDefaultBrowserAutofillSettings(!this.defaultBrowserAutofillDisabled);
|
||||
await BrowserApi.updateDefaultBrowserAutofillSettings(!this.defaultBrowserAutofillDisabled);
|
||||
}
|
||||
|
||||
private handleOverrideDialogAccept = async () => {
|
||||
|
||||
@@ -21,8 +21,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
});
|
||||
|
||||
describe("isFieldForLoginForm", () => {
|
||||
it("does not disqualify totp fields for premium users with flag set to true", () => {
|
||||
inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true;
|
||||
it("does not disqualify totp fields for premium users", () => {
|
||||
inlineMenuFieldQualificationService["premiumEnabled"] = true;
|
||||
const field = mock<AutofillField>({
|
||||
type: "text",
|
||||
@@ -37,24 +36,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("disqualifies totp fields for premium users with flag set to false", () => {
|
||||
inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = false;
|
||||
inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true;
|
||||
const field = mock<AutofillField>({
|
||||
type: "text",
|
||||
autoCompleteType: "one-time-code",
|
||||
htmlName: "totp",
|
||||
htmlID: "totp",
|
||||
placeholder: "totp",
|
||||
});
|
||||
|
||||
expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("disqualifies totp fields for non-premium users with flag set to true", () => {
|
||||
inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true;
|
||||
it("disqualifies totp fields for non-premium users", () => {
|
||||
inlineMenuFieldQualificationService["premiumEnabled"] = false;
|
||||
const field = mock<AutofillField>({
|
||||
type: "text",
|
||||
|
||||
@@ -151,17 +151,14 @@ export class InlineMenuFieldQualificationService
|
||||
]);
|
||||
private totpFieldAutocompleteValue = "one-time-code";
|
||||
private inlineMenuFieldQualificationFlagSet = false;
|
||||
private inlineMenuTotpFeatureFlag = false;
|
||||
private premiumEnabled = false;
|
||||
|
||||
constructor() {
|
||||
void Promise.all([
|
||||
sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"),
|
||||
sendExtensionMessage("getInlineMenuTotpFeatureFlag"),
|
||||
sendExtensionMessage("getUserPremiumStatus"),
|
||||
]).then(([fieldQualificationFlag, totpFeatureFlag, premiumStatus]) => {
|
||||
]).then(([fieldQualificationFlag, premiumStatus]) => {
|
||||
this.inlineMenuFieldQualificationFlagSet = !!fieldQualificationFlag?.result;
|
||||
this.inlineMenuTotpFeatureFlag = !!totpFeatureFlag?.result;
|
||||
this.premiumEnabled = !!premiumStatus?.result;
|
||||
});
|
||||
}
|
||||
@@ -180,7 +177,7 @@ export class InlineMenuFieldQualificationService
|
||||
/**
|
||||
* Totp inline menu is available only for premium users.
|
||||
*/
|
||||
if (this.inlineMenuTotpFeatureFlag && this.premiumEnabled) {
|
||||
if (this.premiumEnabled) {
|
||||
const isTotpField = this.isTotpField(field);
|
||||
// Autofill does not fill totp inputs with a "password" `type` attribute value
|
||||
const passwordType = field.type === "password";
|
||||
|
||||
@@ -103,6 +103,7 @@ export function createChromeTabMock(customFields = {}): chrome.tabs.Tab {
|
||||
selected: true,
|
||||
discarded: false,
|
||||
autoDiscardable: false,
|
||||
frozen: false,
|
||||
groupId: 2,
|
||||
url: "https://jest-testing-website.com",
|
||||
...customFields,
|
||||
|
||||
@@ -1011,6 +1011,7 @@ export default class MainBackground {
|
||||
this.encryptService,
|
||||
this.pinService,
|
||||
this.accountService,
|
||||
this.sdkService,
|
||||
);
|
||||
|
||||
this.individualVaultExportService = new IndividualVaultExportService(
|
||||
|
||||
@@ -78,7 +78,6 @@ export default class RuntimeBackground {
|
||||
BiometricsCommands.GetBiometricsStatusForUser,
|
||||
"getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag",
|
||||
"getInlineMenuFieldQualificationFeatureFlag",
|
||||
"getInlineMenuTotpFeatureFlag",
|
||||
"getUserPremiumStatus",
|
||||
];
|
||||
|
||||
@@ -217,9 +216,6 @@ export default class RuntimeBackground {
|
||||
);
|
||||
return result;
|
||||
}
|
||||
case "getInlineMenuTotpFeatureFlag": {
|
||||
return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuTotp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { mock } from "jest-mock-extended";
|
||||
|
||||
import { BrowserApi } from "./browser-api";
|
||||
|
||||
type ChromeSettingsGet = chrome.types.ChromeSetting<boolean>["get"];
|
||||
|
||||
describe("BrowserApi", () => {
|
||||
const executeScriptResult = ["value"];
|
||||
|
||||
@@ -468,19 +470,23 @@ describe("BrowserApi", () => {
|
||||
|
||||
describe("browserAutofillSettingsOverridden", () => {
|
||||
it("returns true if the browser autofill settings are overridden", async () => {
|
||||
const expectedDetails = {
|
||||
value: false,
|
||||
levelOfControl: "controlled_by_this_extension",
|
||||
} as chrome.types.ChromeSettingGetResultDetails;
|
||||
chrome.privacy.services.autofillAddressEnabled.get = jest.fn((details, callback) =>
|
||||
callback(expectedDetails),
|
||||
);
|
||||
chrome.privacy.services.autofillCreditCardEnabled.get = jest.fn((details, callback) =>
|
||||
callback(expectedDetails),
|
||||
);
|
||||
chrome.privacy.services.passwordSavingEnabled.get = jest.fn((details, callback) =>
|
||||
callback(expectedDetails),
|
||||
);
|
||||
const mockFn = jest.fn<
|
||||
void,
|
||||
[
|
||||
details: chrome.types.ChromeSettingGetDetails,
|
||||
callback: (details: chrome.types.ChromeSettingGetResult<boolean>) => void,
|
||||
],
|
||||
never
|
||||
>((details, callback) => {
|
||||
callback({
|
||||
value: false,
|
||||
levelOfControl: "controlled_by_this_extension",
|
||||
});
|
||||
});
|
||||
chrome.privacy.services.autofillAddressEnabled.get = mockFn as unknown as ChromeSettingsGet;
|
||||
chrome.privacy.services.autofillCreditCardEnabled.get =
|
||||
mockFn as unknown as ChromeSettingsGet;
|
||||
chrome.privacy.services.passwordSavingEnabled.get = mockFn as unknown as ChromeSettingsGet;
|
||||
|
||||
const result = await BrowserApi.browserAutofillSettingsOverridden();
|
||||
|
||||
@@ -488,19 +494,24 @@ describe("BrowserApi", () => {
|
||||
});
|
||||
|
||||
it("returns false if the browser autofill settings are not overridden", async () => {
|
||||
const expectedDetails = {
|
||||
value: true,
|
||||
levelOfControl: "controlled_by_this_extension",
|
||||
} as chrome.types.ChromeSettingGetResultDetails;
|
||||
chrome.privacy.services.autofillAddressEnabled.get = jest.fn((details, callback) =>
|
||||
callback(expectedDetails),
|
||||
);
|
||||
chrome.privacy.services.autofillCreditCardEnabled.get = jest.fn((details, callback) =>
|
||||
callback(expectedDetails),
|
||||
);
|
||||
chrome.privacy.services.passwordSavingEnabled.get = jest.fn((details, callback) =>
|
||||
callback(expectedDetails),
|
||||
);
|
||||
const mockFn = jest.fn<
|
||||
void,
|
||||
[
|
||||
details: chrome.types.ChromeSettingGetDetails,
|
||||
callback: (details: chrome.types.ChromeSettingGetResult<boolean>) => void,
|
||||
],
|
||||
never
|
||||
>((details, callback) => {
|
||||
callback({
|
||||
value: true,
|
||||
levelOfControl: "controlled_by_this_extension",
|
||||
});
|
||||
});
|
||||
|
||||
chrome.privacy.services.autofillAddressEnabled.get = mockFn as unknown as ChromeSettingsGet;
|
||||
chrome.privacy.services.autofillCreditCardEnabled.get =
|
||||
mockFn as unknown as ChromeSettingsGet;
|
||||
chrome.privacy.services.passwordSavingEnabled.get = mockFn as unknown as ChromeSettingsGet;
|
||||
|
||||
const result = await BrowserApi.browserAutofillSettingsOverridden();
|
||||
|
||||
@@ -508,19 +519,23 @@ describe("BrowserApi", () => {
|
||||
});
|
||||
|
||||
it("returns false if the browser autofill settings are not controlled by the extension", async () => {
|
||||
const expectedDetails = {
|
||||
value: false,
|
||||
levelOfControl: "controlled_by_other_extensions",
|
||||
} as chrome.types.ChromeSettingGetResultDetails;
|
||||
chrome.privacy.services.autofillAddressEnabled.get = jest.fn((details, callback) =>
|
||||
callback(expectedDetails),
|
||||
);
|
||||
chrome.privacy.services.autofillCreditCardEnabled.get = jest.fn((details, callback) =>
|
||||
callback(expectedDetails),
|
||||
);
|
||||
chrome.privacy.services.passwordSavingEnabled.get = jest.fn((details, callback) =>
|
||||
callback(expectedDetails),
|
||||
);
|
||||
const mockFn = jest.fn<
|
||||
void,
|
||||
[
|
||||
details: chrome.types.ChromeSettingGetDetails,
|
||||
callback: (details: chrome.types.ChromeSettingGetResult<boolean>) => void,
|
||||
],
|
||||
never
|
||||
>((details, callback) => {
|
||||
callback({
|
||||
value: false,
|
||||
levelOfControl: "controlled_by_other_extensions",
|
||||
});
|
||||
});
|
||||
chrome.privacy.services.autofillAddressEnabled.get = mockFn as unknown as ChromeSettingsGet;
|
||||
chrome.privacy.services.autofillCreditCardEnabled.get =
|
||||
mockFn as unknown as ChromeSettingsGet;
|
||||
chrome.privacy.services.passwordSavingEnabled.get = mockFn as unknown as ChromeSettingsGet;
|
||||
|
||||
const result = await BrowserApi.browserAutofillSettingsOverridden();
|
||||
|
||||
|
||||
@@ -504,7 +504,9 @@ export class BrowserApi {
|
||||
*
|
||||
* @param permissions - The permissions to check.
|
||||
*/
|
||||
static async permissionsGranted(permissions: string[]): Promise<boolean> {
|
||||
static async permissionsGranted(
|
||||
permissions: chrome.runtime.ManifestPermissions[],
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve) =>
|
||||
chrome.permissions.contains({ permissions }, (result) => resolve(result)),
|
||||
);
|
||||
@@ -594,7 +596,7 @@ export class BrowserApi {
|
||||
* Identifies if the browser autofill settings are overridden by the extension.
|
||||
*/
|
||||
static async browserAutofillSettingsOverridden(): Promise<boolean> {
|
||||
const checkOverrideStatus = (details: chrome.types.ChromeSettingGetResultDetails) =>
|
||||
const checkOverrideStatus = (details: chrome.types.ChromeSettingGetResult<boolean>) =>
|
||||
details.levelOfControl === "controlled_by_this_extension" && !details.value;
|
||||
|
||||
const autofillAddressOverridden: boolean = await new Promise((resolve) =>
|
||||
@@ -623,10 +625,10 @@ export class BrowserApi {
|
||||
*
|
||||
* @param value - Determines whether to enable or disable the autofill settings.
|
||||
*/
|
||||
static updateDefaultBrowserAutofillSettings(value: boolean) {
|
||||
chrome.privacy.services.autofillAddressEnabled.set({ value });
|
||||
chrome.privacy.services.autofillCreditCardEnabled.set({ value });
|
||||
chrome.privacy.services.passwordSavingEnabled.set({ value });
|
||||
static async updateDefaultBrowserAutofillSettings(value: boolean) {
|
||||
await chrome.privacy.services.autofillAddressEnabled.set({ value });
|
||||
await chrome.privacy.services.autofillCreditCardEnabled.set({ value });
|
||||
await chrome.privacy.services.passwordSavingEnabled.set({ value });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,8 +64,16 @@ export class PopupViewCacheService implements ViewCacheService {
|
||||
filter((e) => e instanceof NavigationEnd),
|
||||
/** Skip the first navigation triggered by `popupRouterCacheGuard` */
|
||||
skip(1),
|
||||
filter((e: NavigationEnd) =>
|
||||
// viewing/editing a cipher and navigating back to the vault list should not clear the cache
|
||||
["/view-cipher", "/edit-cipher", "/tabs/vault"].every(
|
||||
(route) => !e.urlAfterRedirects.startsWith(route),
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe(() => this.clearState());
|
||||
.subscribe((e) => {
|
||||
return this.clearState();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
EnvironmentSelectorRouteData,
|
||||
ExtensionDefaultOverlayPosition,
|
||||
} from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import { unauthUiRefreshRedirect } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-redirect";
|
||||
import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap";
|
||||
import {
|
||||
activeAuthGuard,
|
||||
@@ -58,15 +57,9 @@ import {
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
ExtensionAnonLayoutWrapperData,
|
||||
} from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
|
||||
import { HintComponent } from "../auth/popup/hint.component";
|
||||
import { HomeComponent } from "../auth/popup/home.component";
|
||||
import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component";
|
||||
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
|
||||
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
|
||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
|
||||
import { TwoFactorOptionsComponentV1 } from "../auth/popup/two-factor-options-v1.component";
|
||||
import { TwoFactorComponentV1 } from "../auth/popup/two-factor-v1.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||
@@ -131,20 +124,19 @@ const routes: Routes = [
|
||||
children: [], // Children lets us have an empty component.
|
||||
canActivate: [
|
||||
popupRouterCacheGuard,
|
||||
redirectGuard({ loggedIn: "/tabs/current", loggedOut: "/home", locked: "/lock" }),
|
||||
redirectGuard({ loggedIn: "/tabs/current", loggedOut: "/login", locked: "/lock" }),
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "home",
|
||||
redirectTo: "login",
|
||||
pathMatch: "full",
|
||||
},
|
||||
{
|
||||
path: "vault",
|
||||
redirectTo: "/tabs/vault",
|
||||
pathMatch: "full",
|
||||
},
|
||||
{
|
||||
path: "home",
|
||||
component: HomeComponent,
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides), unauthUiRefreshRedirect("/login")],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "fido2",
|
||||
component: Fido2Component,
|
||||
@@ -206,40 +198,6 @@ const routes: Routes = [
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
...unauthUiRefreshSwap(
|
||||
SsoComponentV1,
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "sso",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "sso",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: {
|
||||
pageIcon: VaultIcon,
|
||||
pageTitle: {
|
||||
key: "enterpriseSingleSignOn",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "singleSignOnEnterOrgIdentifierText",
|
||||
},
|
||||
elevation: 1,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: SsoComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: ExtensionDefaultOverlayPosition,
|
||||
} satisfies EnvironmentSelectorRouteData,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "device-verification",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
@@ -420,158 +378,7 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
...unauthUiRefreshSwap(
|
||||
LoginViaAuthRequestComponentV1,
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "logInRequestSent",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
},
|
||||
showLogo: false,
|
||||
showBackButton: true,
|
||||
elevation: 1,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginViaAuthRequestComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginViaAuthRequestComponentV1,
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "adminApprovalRequested",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "adminApprovalRequestSentToAdmins",
|
||||
},
|
||||
showLogo: false,
|
||||
showBackButton: true,
|
||||
elevation: 1,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
children: [{ path: "", component: LoginViaAuthRequestComponent }],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
HintComponent,
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: {
|
||||
elevation: 1,
|
||||
} satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "requestPasswordHint",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
|
||||
},
|
||||
pageIcon: UserLockIcon,
|
||||
showBackButton: true,
|
||||
elevation: 1,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: PasswordHintComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: ExtensionDefaultOverlayPosition,
|
||||
} satisfies EnvironmentSelectorRouteData,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginComponentV1,
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: { elevation: 1 },
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: {
|
||||
pageIcon: VaultIcon,
|
||||
pageTitle: {
|
||||
key: "logInToBitwarden",
|
||||
},
|
||||
elevation: 1,
|
||||
showAcctSwitcher: true,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginComponent },
|
||||
{ path: "", component: LoginSecondaryContentComponent, outlet: "secondary" },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: ExtensionDefaultOverlayPosition,
|
||||
} satisfies EnvironmentSelectorRouteData,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
},
|
||||
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||
},
|
||||
),
|
||||
|
||||
{
|
||||
path: "",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
@@ -597,7 +404,7 @@ const routes: Routes = [
|
||||
component: RegistrationStartSecondaryComponent,
|
||||
outlet: "secondary",
|
||||
data: {
|
||||
loginRoute: "/home",
|
||||
loginRoute: "/login",
|
||||
} satisfies RegistrationStartSecondaryComponentData,
|
||||
},
|
||||
],
|
||||
@@ -617,6 +424,127 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: {
|
||||
pageIcon: VaultIcon,
|
||||
pageTitle: {
|
||||
key: "logInToBitwarden",
|
||||
},
|
||||
elevation: 1,
|
||||
showAcctSwitcher: true,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginComponent },
|
||||
{ path: "", component: LoginSecondaryContentComponent, outlet: "secondary" },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: ExtensionDefaultOverlayPosition,
|
||||
} satisfies EnvironmentSelectorRouteData,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "sso",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: {
|
||||
pageIcon: VaultIcon,
|
||||
pageTitle: {
|
||||
key: "enterpriseSingleSignOn",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "singleSignOnEnterOrgIdentifierText",
|
||||
},
|
||||
elevation: 1,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: SsoComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: ExtensionDefaultOverlayPosition,
|
||||
} satisfies EnvironmentSelectorRouteData,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "logInRequestSent",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
},
|
||||
showBackButton: true,
|
||||
elevation: 1,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginViaAuthRequestComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "requestPasswordHint",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
|
||||
},
|
||||
pageIcon: UserLockIcon,
|
||||
showBackButton: true,
|
||||
elevation: 1,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: PasswordHintComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: ExtensionDefaultOverlayPosition,
|
||||
} satisfies EnvironmentSelectorRouteData,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "adminApprovalRequested",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "adminApprovalRequestSentToAdmins",
|
||||
},
|
||||
showLogo: false,
|
||||
showBackButton: true,
|
||||
elevation: 1,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
children: [{ path: "", component: LoginViaAuthRequestComponent }],
|
||||
},
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
},
|
||||
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||
},
|
||||
{
|
||||
path: "lock",
|
||||
canActivate: [lockGuard()],
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs";
|
||||
|
||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
@@ -68,7 +70,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private animationControlService: AnimationControlService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private biometricsService: BiometricsService,
|
||||
) {}
|
||||
private deviceTrustToastService: DeviceTrustToastService,
|
||||
) {
|
||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
initPopupClosedListener();
|
||||
@@ -113,9 +118,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
this.changeDetectorRef.detectChanges();
|
||||
} else if (msg.command === "authBlocked" || msg.command === "goHome") {
|
||||
// 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(["home"]);
|
||||
await this.router.navigate(["login"]);
|
||||
} else if (
|
||||
msg.command === "locked" &&
|
||||
(msg.userId == null || msg.userId == this.activeUserId)
|
||||
|
||||
@@ -22,10 +22,7 @@ import { CurrentAccountComponent } from "../auth/popup/account-switching/current
|
||||
import { EnvironmentComponent } from "../auth/popup/environment.component";
|
||||
import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
|
||||
import { HintComponent } from "../auth/popup/hint.component";
|
||||
import { HomeComponent } from "../auth/popup/home.component";
|
||||
import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component";
|
||||
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
|
||||
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
|
||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
@@ -98,9 +95,6 @@ import "../platform/popup/locales";
|
||||
ColorPasswordCountPipe,
|
||||
EnvironmentComponent,
|
||||
HintComponent,
|
||||
HomeComponent,
|
||||
LoginViaAuthRequestComponentV1,
|
||||
LoginComponentV1,
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
SetPasswordComponent,
|
||||
SsoComponentV1,
|
||||
|
||||
@@ -448,38 +448,6 @@ main:not(popup-page main) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-with-device {
|
||||
.fingerprint-phrase-header {
|
||||
padding-top: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@include themify($themes) {
|
||||
.fingerprint-text {
|
||||
color: themed("codeColor");
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||
monospace;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.resend-notification {
|
||||
padding-bottom: 1rem;
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-top: 1rem;
|
||||
|
||||
a {
|
||||
padding-top: 1rem;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#login-initiated {
|
||||
.margin-auto {
|
||||
margin: auto;
|
||||
|
||||
@@ -130,7 +130,11 @@ import {
|
||||
KeyService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { LockComponentService } from "@bitwarden/key-management-ui";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
DefaultSshImportPromptService,
|
||||
PasswordRepromptService,
|
||||
SshImportPromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
@@ -653,6 +657,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: ExtensionLoginDecryptionOptionsService,
|
||||
deps: [MessagingServiceAbstraction, Router],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SshImportPromptService,
|
||||
useClass: DefaultSshImportPromptService,
|
||||
deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
(onRefresh)="refreshCurrentTab()"
|
||||
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null"
|
||||
showAutofillButton
|
||||
[disableDescriptionMargin]="showEmptyAutofillTip$ | async"
|
||||
[primaryActionAutofill]="clickItemsToAutofillVaultView"
|
||||
[groupByType]="groupByType()"
|
||||
></app-vault-list-items-container>
|
||||
|
||||
@@ -27,12 +27,7 @@
|
||||
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
|
||||
{{ "note" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
bitMenuItem
|
||||
[routerLink]="['/add-cipher']"
|
||||
[queryParams]="buildQueryParams(cipherType.SshKey)"
|
||||
*ngIf="sshKeysEnabled"
|
||||
>
|
||||
<a bitMenuItem [routerLink]="['/add-cipher']" [queryParams]="buildQueryParams(cipherType.SshKey)">
|
||||
<i class="bwi bwi-key" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeSshKey" | i18n }}
|
||||
</a>
|
||||
|
||||
@@ -5,8 +5,6 @@ import { Component, Input, OnInit } from "@angular/core";
|
||||
import { Router, RouterLink } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -40,13 +38,9 @@ export class NewItemDropdownV2Component implements OnInit {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private dialogService: DialogService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
sshKeysEnabled = false;
|
||||
|
||||
async ngOnInit() {
|
||||
this.sshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem);
|
||||
this.tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<a
|
||||
tabIndex="0"
|
||||
bitLink
|
||||
class="tw-font-bold"
|
||||
linkType="primary"
|
||||
routerLink="/appearance"
|
||||
(keydown.enter)="goToAppearance()"
|
||||
|
||||
@@ -70,7 +70,12 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #descriptionText>
|
||||
<div *ngIf="description" class="tw-text-muted tw-px-1 tw-mb-2" bitTypography="body2">
|
||||
<div
|
||||
*ngIf="description"
|
||||
class="tw-text-muted tw-px-1 tw-mb-2"
|
||||
[ngClass]="{ '!tw-mb-0': disableDescriptionMargin }"
|
||||
bitTypography="body2"
|
||||
>
|
||||
{{ description }}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -245,6 +245,12 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
|
||||
@Input({ transform: booleanAttribute })
|
||||
disableSectionMargin: boolean = false;
|
||||
|
||||
/**
|
||||
* Remove the description margin
|
||||
*/
|
||||
@Input({ transform: booleanAttribute })
|
||||
disableDescriptionMargin: boolean = false;
|
||||
|
||||
/**
|
||||
* The tooltip text for the organization icon for ciphers that belong to an organization.
|
||||
* @param cipher
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
slot="end"
|
||||
type="button"
|
||||
(click)="openAddEditFolderDialog(folder)"
|
||||
[appA11yTitle]="'editFolder' | i18n"
|
||||
[appA11yTitle]="'editFolderWithName' | i18n: folder.name"
|
||||
bitIconButton="bwi-pencil-square"
|
||||
class="tw-self-end"
|
||||
data-testid="edit-folder-button"
|
||||
|
||||
@@ -21,6 +21,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
@@ -58,6 +59,7 @@ export class LoginCommand {
|
||||
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
protected authService: AuthService,
|
||||
protected apiService: ApiService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
@@ -321,7 +323,7 @@ export class LoginCommand {
|
||||
})({
|
||||
type: "input",
|
||||
name: "token",
|
||||
message: "New device login code:",
|
||||
message: "New device verification required. Enter OTP sent to login email:",
|
||||
});
|
||||
newDeviceToken = answer.token;
|
||||
}
|
||||
@@ -454,7 +456,7 @@ export class LoginCommand {
|
||||
request.newMasterPasswordHash = newPasswordHash;
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
|
||||
await this.apiService.postPassword(request);
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
|
||||
return await this.handleUpdatePasswordSuccessResponse();
|
||||
} catch (e) {
|
||||
@@ -491,7 +493,7 @@ export class LoginCommand {
|
||||
request.newMasterPasswordHash = newPasswordHash;
|
||||
request.masterPasswordHint = hint;
|
||||
|
||||
await this.apiService.putUpdateTempPassword(request);
|
||||
await this.masterPasswordApiService.putUpdateTempPassword(request);
|
||||
|
||||
return await this.handleUpdatePasswordSuccessResponse();
|
||||
} catch (e) {
|
||||
|
||||
@@ -155,6 +155,7 @@ export class Program extends BaseProgram {
|
||||
this.serviceContainer.loginStrategyService,
|
||||
this.serviceContainer.authService,
|
||||
this.serviceContainer.apiService,
|
||||
this.serviceContainer.masterPasswordApiService,
|
||||
this.serviceContainer.cryptoFunctionService,
|
||||
this.serviceContainer.environmentService,
|
||||
this.serviceContainer.passwordGenerationService,
|
||||
|
||||
@@ -36,6 +36,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { MasterPasswordApiService as MasterPasswordApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
AccountServiceImplementation,
|
||||
@@ -46,6 +47,7 @@ import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||
import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.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 { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
@@ -280,6 +282,7 @@ export class ServiceContainer {
|
||||
sdkLoadService: SdkLoadService;
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
ssoUrlService: SsoUrlService;
|
||||
masterPasswordApiService: MasterPasswordApiServiceAbstraction;
|
||||
|
||||
constructor() {
|
||||
let p = null;
|
||||
@@ -779,6 +782,7 @@ export class ServiceContainer {
|
||||
this.encryptService,
|
||||
this.pinService,
|
||||
this.accountService,
|
||||
this.sdkService,
|
||||
);
|
||||
|
||||
this.individualExportService = new IndividualVaultExportService(
|
||||
@@ -841,6 +845,8 @@ export class ServiceContainer {
|
||||
this.organizationService,
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.masterPasswordApiService = new MasterPasswordApiService(this.apiService, this.logService);
|
||||
}
|
||||
|
||||
async logout() {
|
||||
|
||||
@@ -9,11 +9,55 @@ edition = "2021"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
aes = "=0.8.4"
|
||||
anyhow = "=1.0.94"
|
||||
arboard = { version = "=3.4.1", default-features = false }
|
||||
argon2 = "=0.5.3"
|
||||
base64 = "=0.22.1"
|
||||
bindgen = "0.71.1"
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" }
|
||||
byteorder = "=1.5.0"
|
||||
bytes = "1.9.0"
|
||||
cbc = "=0.1.2"
|
||||
core-foundation = "=0.10.0"
|
||||
dirs = "=6.0.0"
|
||||
ed25519 = "=2.2.3"
|
||||
embed_plist = "=1.2.2"
|
||||
futures = "=0.3.31"
|
||||
hex = "=0.4.3"
|
||||
homedir = "=0.3.4"
|
||||
interprocess = "=2.2.1"
|
||||
keytar = "=0.1.6"
|
||||
libc = "=0.2.169"
|
||||
log = "=0.4.25"
|
||||
napi = "=2.16.15"
|
||||
napi-build = "=2.1.4"
|
||||
napi-derive = "=2.16.13"
|
||||
oo7 = "=0.3.3"
|
||||
oslog = "=0.2.0"
|
||||
pin-project = "=1.1.8"
|
||||
pkcs8 = "=0.10.2"
|
||||
rand = "=0.8.5"
|
||||
rsa = "=0.9.6"
|
||||
russh-cryptovec = "=0.7.3"
|
||||
scopeguard = "=1.2.0"
|
||||
security-framework = "=3.1.0"
|
||||
security-framework-sys = "=2.13.0"
|
||||
serde = "=1.0.209"
|
||||
serde_json = "=1.0.127"
|
||||
tokio = "=1.43.0"
|
||||
tokio-util = "=0.7.13"
|
||||
tokio-stream = "=0.1.15"
|
||||
sha2 = "=0.10.8"
|
||||
simplelog = "=0.12.2"
|
||||
ssh-encoding = "=0.2.0"
|
||||
ssh-key = {version = "=0.6.7", default-features = false }
|
||||
sysinfo = "0.33.1"
|
||||
thiserror = "=1.0.69"
|
||||
tokio = "=1.43.0"
|
||||
tokio-stream = "=0.1.15"
|
||||
tokio-util = "=0.7.13"
|
||||
typenum = "=1.17.0"
|
||||
uniffi = "=0.28.3"
|
||||
widestring = "=1.1.0"
|
||||
windows = "=0.58.0"
|
||||
windows-registry = "=0.4.0"
|
||||
zbus = "=4.4.0"
|
||||
zbus_polkit = "=4.0.0"
|
||||
|
||||
@@ -18,47 +18,47 @@ default = [
|
||||
manual_test = []
|
||||
|
||||
[dependencies]
|
||||
aes = "=0.8.4"
|
||||
aes = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
arboard = { version = "=3.4.1", default-features = false, features = [
|
||||
arboard = { workspace = true, features = [
|
||||
"wayland-data-control",
|
||||
] }
|
||||
argon2 = { version = "=0.5.3", features = ["zeroize"] }
|
||||
base64 = "=0.22.1"
|
||||
byteorder = "=1.5.0"
|
||||
cbc = { version = "=0.1.2", features = ["alloc"] }
|
||||
homedir = "=0.3.4"
|
||||
pin-project = "=1.1.8"
|
||||
dirs = "=6.0.0"
|
||||
futures = "=0.3.31"
|
||||
interprocess = { version = "=2.2.1", features = ["tokio"] }
|
||||
argon2 = { workspace = true, features = ["zeroize"] }
|
||||
base64 = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
cbc = { workspace = true, features = ["alloc"] }
|
||||
homedir = { workspace = true }
|
||||
pin-project = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
interprocess = { workspace = true, features = ["tokio"] }
|
||||
log = { workspace = true }
|
||||
rand = "=0.8.5"
|
||||
russh-cryptovec = "=0.7.3"
|
||||
scopeguard = "=1.2.0"
|
||||
sha2 = "=0.10.8"
|
||||
ssh-encoding = "=0.2.0"
|
||||
ssh-key = { version = "=0.6.7", default-features = false, features = [
|
||||
rand = { workspace = true }
|
||||
russh-cryptovec = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
ssh-encoding = { workspace = true }
|
||||
ssh-key = { workspace = true, features = [
|
||||
"encryption",
|
||||
"ed25519",
|
||||
"rsa",
|
||||
"getrandom",
|
||||
] }
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" }
|
||||
bitwarden-russh = { workspace = true }
|
||||
tokio = { workspace = true, features = ["io-util", "sync", "macros", "net"] }
|
||||
tokio-stream = { workspace = true, features = ["net"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
thiserror = { workspace = true }
|
||||
typenum = "=1.17.0"
|
||||
pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] }
|
||||
rsa = "=0.9.6"
|
||||
ed25519 = { version = "=2.2.3", features = ["pkcs8"] }
|
||||
bytes = "1.9.0"
|
||||
sysinfo = { version = "0.33.1", features = ["windows"] }
|
||||
typenum = { workspace = true }
|
||||
pkcs8 = { workspace = true, features = ["alloc", "encryption", "pem"] }
|
||||
rsa = { workspace = true }
|
||||
ed25519 = { workspace = true, features = ["pkcs8"] }
|
||||
bytes = { workspace = true }
|
||||
sysinfo = { workspace = true, features = ["windows"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
widestring = { version = "=1.1.0", optional = true }
|
||||
windows = { version = "=0.58.0", features = [
|
||||
widestring = { workspace = true, optional = true }
|
||||
windows = { workspace = true, features = [
|
||||
"Foundation",
|
||||
"Security_Credentials_UI",
|
||||
"Security_Cryptography",
|
||||
@@ -72,17 +72,17 @@ windows = { version = "=0.58.0", features = [
|
||||
], optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dev-dependencies]
|
||||
keytar = "=0.1.6"
|
||||
keytar = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = { version = "=0.10.0", optional = true }
|
||||
security-framework = { version = "=3.1.0", optional = true }
|
||||
security-framework-sys = { version = "=2.13.0", optional = true }
|
||||
core-foundation = { workspace = true, optional = true }
|
||||
security-framework = { workspace = true, optional = true }
|
||||
security-framework-sys = { workspace = true, optional = true }
|
||||
desktop_objc = { path = "../objc" }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
oo7 = "=0.3.3"
|
||||
libc = "=0.2.169"
|
||||
oo7 = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
|
||||
zbus = { version = "=4.4.0", optional = true }
|
||||
zbus_polkit = { version = "=4.0.0", optional = true }
|
||||
zbus = { workspace = true, optional = true }
|
||||
zbus_polkit = { workspace = true, optional = true }
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
use ed25519;
|
||||
use pkcs8::{
|
||||
der::Decode, EncryptedPrivateKeyInfo, ObjectIdentifier, PrivateKeyInfo, SecretDocument,
|
||||
};
|
||||
use ssh_key::{
|
||||
private::{Ed25519Keypair, Ed25519PrivateKey, RsaKeypair},
|
||||
HashAlg, LineEnding,
|
||||
};
|
||||
|
||||
const PKCS1_HEADER: &str = "-----BEGIN RSA PRIVATE KEY-----";
|
||||
const PKCS8_UNENCRYPTED_HEADER: &str = "-----BEGIN PRIVATE KEY-----";
|
||||
const PKCS8_ENCRYPTED_HEADER: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
|
||||
const OPENSSH_HEADER: &str = "-----BEGIN OPENSSH PRIVATE KEY-----";
|
||||
|
||||
pub const RSA_PKCS8_ALGORITHM_OID: ObjectIdentifier =
|
||||
ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
|
||||
|
||||
#[derive(Debug)]
|
||||
enum KeyType {
|
||||
Ed25519,
|
||||
Rsa,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub fn import_key(
|
||||
encoded_key: String,
|
||||
password: String,
|
||||
) -> Result<SshKeyImportResult, anyhow::Error> {
|
||||
match encoded_key.lines().next() {
|
||||
Some(PKCS1_HEADER) => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::UnsupportedKeyType,
|
||||
ssh_key: None,
|
||||
}),
|
||||
Some(PKCS8_UNENCRYPTED_HEADER) => match import_pkcs8_key(encoded_key, None) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
}),
|
||||
},
|
||||
Some(PKCS8_ENCRYPTED_HEADER) => match import_pkcs8_key(encoded_key, Some(password)) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(err) => match err {
|
||||
SshKeyImportError::PasswordRequired => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::PasswordRequired,
|
||||
ssh_key: None,
|
||||
}),
|
||||
SshKeyImportError::WrongPassword => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::WrongPassword,
|
||||
ssh_key: None,
|
||||
}),
|
||||
SshKeyImportError::ParsingError => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
}),
|
||||
},
|
||||
},
|
||||
Some(OPENSSH_HEADER) => import_openssh_key(encoded_key, password),
|
||||
Some(_) => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
}),
|
||||
None => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn import_pkcs8_key(
|
||||
encoded_key: String,
|
||||
password: Option<String>,
|
||||
) -> Result<SshKeyImportResult, SshKeyImportError> {
|
||||
let der = match SecretDocument::from_pem(&encoded_key) {
|
||||
Ok((_, doc)) => doc,
|
||||
Err(_) => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let decrypted_der = match password.clone() {
|
||||
Some(password) => {
|
||||
let encrypted_private_key_info = match EncryptedPrivateKeyInfo::from_der(der.as_bytes())
|
||||
{
|
||||
Ok(info) => info,
|
||||
Err(_) => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
match encrypted_private_key_info.decrypt(password.as_bytes()) {
|
||||
Ok(der) => der,
|
||||
Err(_) => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::WrongPassword,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None => der,
|
||||
};
|
||||
|
||||
let key_type: KeyType = match PrivateKeyInfo::from_der(decrypted_der.as_bytes())
|
||||
.map_err(|_| SshKeyImportError::ParsingError)?
|
||||
.algorithm
|
||||
.oid
|
||||
{
|
||||
ed25519::pkcs8::ALGORITHM_OID => KeyType::Ed25519,
|
||||
RSA_PKCS8_ALGORITHM_OID => KeyType::Rsa,
|
||||
_ => KeyType::Unknown,
|
||||
};
|
||||
|
||||
match key_type {
|
||||
KeyType::Ed25519 => {
|
||||
let pk: ed25519::KeypairBytes = match password {
|
||||
Some(password) => {
|
||||
pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password)
|
||||
.map_err(|err| match err {
|
||||
ed25519::pkcs8::Error::EncryptedPrivateKey(_) => {
|
||||
SshKeyImportError::WrongPassword
|
||||
}
|
||||
_ => SshKeyImportError::ParsingError,
|
||||
})?
|
||||
}
|
||||
None => ed25519::pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key)
|
||||
.map_err(|_| SshKeyImportError::ParsingError)?,
|
||||
};
|
||||
let pk: Ed25519Keypair =
|
||||
Ed25519Keypair::from(Ed25519PrivateKey::from_bytes(&pk.secret_key));
|
||||
let private_key = ssh_key::private::PrivateKey::from(pk);
|
||||
Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::Success,
|
||||
ssh_key: Some(SshKey {
|
||||
private_key: private_key.to_openssh(LineEnding::LF).unwrap().to_string(),
|
||||
public_key: private_key.public_key().to_string(),
|
||||
key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
KeyType::Rsa => {
|
||||
let pk: rsa::RsaPrivateKey = match password {
|
||||
Some(password) => {
|
||||
pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password)
|
||||
.map_err(|err| match err {
|
||||
pkcs8::Error::EncryptedPrivateKey(_) => {
|
||||
SshKeyImportError::WrongPassword
|
||||
}
|
||||
_ => SshKeyImportError::ParsingError,
|
||||
})?
|
||||
}
|
||||
None => pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key)
|
||||
.map_err(|_| SshKeyImportError::ParsingError)?,
|
||||
};
|
||||
let rsa_keypair: Result<RsaKeypair, ssh_key::Error> = RsaKeypair::try_from(pk);
|
||||
match rsa_keypair {
|
||||
Ok(rsa_keypair) => {
|
||||
let private_key = ssh_key::private::PrivateKey::from(rsa_keypair);
|
||||
Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::Success,
|
||||
ssh_key: Some(SshKey {
|
||||
private_key: private_key
|
||||
.to_openssh(LineEnding::LF)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
public_key: private_key.public_key().to_string(),
|
||||
key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
Err(_) => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
_ => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::UnsupportedKeyType,
|
||||
ssh_key: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn import_openssh_key(
|
||||
encoded_key: String,
|
||||
password: String,
|
||||
) -> Result<SshKeyImportResult, anyhow::Error> {
|
||||
let private_key = ssh_key::private::PrivateKey::from_openssh(&encoded_key);
|
||||
let private_key = match private_key {
|
||||
Ok(k) => k,
|
||||
Err(err) => {
|
||||
match err {
|
||||
ssh_key::Error::AlgorithmUnknown
|
||||
| ssh_key::Error::AlgorithmUnsupported { algorithm: _ } => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::UnsupportedKeyType,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if private_key.is_encrypted() && password.is_empty() {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::PasswordRequired,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
let private_key = if private_key.is_encrypted() {
|
||||
match private_key.decrypt(password.as_bytes()) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::WrongPassword,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
private_key
|
||||
};
|
||||
|
||||
match private_key.to_openssh(LineEnding::LF) {
|
||||
Ok(private_key_openssh) => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::Success,
|
||||
ssh_key: Some(SshKey {
|
||||
private_key: private_key_openssh.to_string(),
|
||||
public_key: private_key.public_key().to_string(),
|
||||
key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(),
|
||||
}),
|
||||
}),
|
||||
Err(_) => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum SshKeyImportStatus {
|
||||
/// ssh key was parsed correctly and will be returned in the result
|
||||
Success,
|
||||
/// ssh key was parsed correctly but is encrypted and requires a password
|
||||
PasswordRequired,
|
||||
/// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect
|
||||
WrongPassword,
|
||||
/// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key
|
||||
ParsingError,
|
||||
/// ssh key type is not supported
|
||||
UnsupportedKeyType,
|
||||
}
|
||||
|
||||
pub enum SshKeyImportError {
|
||||
ParsingError,
|
||||
PasswordRequired,
|
||||
WrongPassword,
|
||||
}
|
||||
|
||||
pub struct SshKeyImportResult {
|
||||
pub status: SshKeyImportStatus,
|
||||
pub ssh_key: Option<SshKey>,
|
||||
}
|
||||
|
||||
pub struct SshKey {
|
||||
pub private_key: String,
|
||||
pub public_key: String,
|
||||
pub key_fingerprint: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn import_key_ed25519_openssh_unencrypted() {
|
||||
let private_key = include_str!("./test_keys/ed25519_openssh_unencrypted");
|
||||
let public_key = include_str!("./test_keys/ed25519_openssh_unencrypted.pub").trim();
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_ed25519_openssh_encrypted() {
|
||||
let private_key = include_str!("./test_keys/ed25519_openssh_encrypted");
|
||||
let public_key = include_str!("./test_keys/ed25519_openssh_encrypted.pub").trim();
|
||||
let result = import_key(private_key.to_string(), "password".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_rsa_openssh_unencrypted() {
|
||||
let private_key = include_str!("./test_keys/rsa_openssh_unencrypted");
|
||||
let public_key = include_str!("./test_keys/rsa_openssh_unencrypted.pub").trim();
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_rsa_openssh_encrypted() {
|
||||
let private_key = include_str!("./test_keys/rsa_openssh_encrypted");
|
||||
let public_key = include_str!("./test_keys/rsa_openssh_encrypted.pub").trim();
|
||||
let result = import_key(private_key.to_string(), "password".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_ed25519_pkcs8_unencrypted() {
|
||||
let private_key = include_str!("./test_keys/ed25519_pkcs8_unencrypted");
|
||||
let public_key =
|
||||
include_str!("./test_keys/ed25519_pkcs8_unencrypted.pub").replace("testkey", "");
|
||||
let public_key = public_key.trim();
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_rsa_pkcs8_unencrypted() {
|
||||
let private_key = include_str!("./test_keys/rsa_pkcs8_unencrypted");
|
||||
// for whatever reason pkcs8 + rsa does not include the comment in the public key
|
||||
let public_key =
|
||||
include_str!("./test_keys/rsa_pkcs8_unencrypted.pub").replace("testkey", "");
|
||||
let public_key = public_key.trim();
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_rsa_pkcs8_encrypted() {
|
||||
let private_key = include_str!("./test_keys/rsa_pkcs8_encrypted");
|
||||
let public_key = include_str!("./test_keys/rsa_pkcs8_encrypted.pub").replace("testkey", "");
|
||||
let public_key = public_key.trim();
|
||||
let result = import_key(private_key.to_string(), "password".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_ed25519_openssh_encrypted_wrong_password() {
|
||||
let private_key = include_str!("./test_keys/ed25519_openssh_encrypted");
|
||||
let result = import_key(private_key.to_string(), "wrongpassword".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::WrongPassword);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_non_key_error() {
|
||||
let result = import_key("not a key".to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::ParsingError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_ecdsa_error() {
|
||||
let private_key = include_str!("./test_keys/ecdsa_openssh_unencrypted");
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType);
|
||||
}
|
||||
|
||||
// Putty-exported keys should be supported, but are not due to a parser incompatibility.
|
||||
// Should this test start failing, please change it to expect a correct key, and
|
||||
// make sure the documentation support for putty-exported keys this is updated.
|
||||
// https://bitwarden.atlassian.net/browse/PM-14989
|
||||
#[test]
|
||||
fn import_key_ed25519_putty() {
|
||||
let private_key = include_str!("./test_keys/ed25519_putty_openssh_unencrypted");
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::ParsingError);
|
||||
}
|
||||
|
||||
// Putty-exported keys should be supported, but are not due to a parser incompatibility.
|
||||
// Should this test start failing, please change it to expect a correct key, and
|
||||
// make sure the documentation support for putty-exported keys this is updated.
|
||||
// https://bitwarden.atlassian.net/browse/PM-14989
|
||||
#[test]
|
||||
fn import_key_rsa_openssh_putty() {
|
||||
let private_key = include_str!("./test_keys/rsa_putty_openssh_unencrypted");
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::ParsingError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_rsa_pkcs8_putty() {
|
||||
let private_key = include_str!("./test_keys/rsa_putty_pkcs1_unencrypted");
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ mod platform_ssh_agent;
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
mod peercred_unix_listener_stream;
|
||||
|
||||
pub mod importer;
|
||||
pub mod peerinfo;
|
||||
mod request_parser;
|
||||
|
||||
|
||||
@@ -15,16 +15,16 @@ bench = false
|
||||
|
||||
[dependencies]
|
||||
desktop_core = { path = "../core" }
|
||||
futures = "=0.3.31"
|
||||
futures = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tokio-util = { workspace = true }
|
||||
uniffi = { version = "=0.28.3", features = ["cli"] }
|
||||
uniffi = { workspace = true, features = ["cli"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
oslog = "=0.2.0"
|
||||
oslog = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { version = "=0.28.3", features = ["build"] }
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
|
||||
@@ -14,12 +14,12 @@ default = []
|
||||
manual_test = []
|
||||
|
||||
[dependencies]
|
||||
base64 = "=0.22.1"
|
||||
hex = "=0.4.3"
|
||||
base64 = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
desktop_core = { path = "../core" }
|
||||
napi = { version = "=2.16.15", features = ["async"] }
|
||||
napi-derive = "=2.16.13"
|
||||
napi = { workspace = true, features = ["async"] }
|
||||
napi-derive = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@@ -27,7 +27,7 @@ tokio-util = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-registry = "=0.4.0"
|
||||
windows-registry = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "=2.1.4"
|
||||
napi-build = { workspace = true }
|
||||
|
||||
17
apps/desktop/desktop_native/napi/index.d.ts
vendored
17
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -51,22 +51,6 @@ export declare namespace sshagent {
|
||||
publicKey: string
|
||||
keyFingerprint: string
|
||||
}
|
||||
export const enum SshKeyImportStatus {
|
||||
/** ssh key was parsed correctly and will be returned in the result */
|
||||
Success = 0,
|
||||
/** ssh key was parsed correctly but is encrypted and requires a password */
|
||||
PasswordRequired = 1,
|
||||
/** ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect */
|
||||
WrongPassword = 2,
|
||||
/** ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key */
|
||||
ParsingError = 3,
|
||||
/** ssh key type is not supported (e.g. ecdsa) */
|
||||
UnsupportedKeyType = 4
|
||||
}
|
||||
export interface SshKeyImportResult {
|
||||
status: SshKeyImportStatus
|
||||
sshKey?: SshKey
|
||||
}
|
||||
export interface SshUiRequest {
|
||||
cipherId?: string
|
||||
isList: boolean
|
||||
@@ -79,7 +63,6 @@ export declare namespace sshagent {
|
||||
export function isRunning(agentState: SshAgentState): boolean
|
||||
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
|
||||
export function lock(agentState: SshAgentState): void
|
||||
export function importKey(encodedKey: string, password: string): SshKeyImportResult
|
||||
export function clearKeys(agentState: SshAgentState): void
|
||||
export class SshAgentState { }
|
||||
}
|
||||
|
||||
@@ -182,67 +182,6 @@ pub mod sshagent {
|
||||
pub key_fingerprint: String,
|
||||
}
|
||||
|
||||
impl From<desktop_core::ssh_agent::importer::SshKey> for SshKey {
|
||||
fn from(key: desktop_core::ssh_agent::importer::SshKey) -> Self {
|
||||
SshKey {
|
||||
private_key: key.private_key,
|
||||
public_key: key.public_key,
|
||||
key_fingerprint: key.key_fingerprint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub enum SshKeyImportStatus {
|
||||
/// ssh key was parsed correctly and will be returned in the result
|
||||
Success,
|
||||
/// ssh key was parsed correctly but is encrypted and requires a password
|
||||
PasswordRequired,
|
||||
/// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect
|
||||
WrongPassword,
|
||||
/// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key
|
||||
ParsingError,
|
||||
/// ssh key type is not supported (e.g. ecdsa)
|
||||
UnsupportedKeyType,
|
||||
}
|
||||
|
||||
impl From<desktop_core::ssh_agent::importer::SshKeyImportStatus> for SshKeyImportStatus {
|
||||
fn from(status: desktop_core::ssh_agent::importer::SshKeyImportStatus) -> Self {
|
||||
match status {
|
||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::Success => {
|
||||
SshKeyImportStatus::Success
|
||||
}
|
||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::PasswordRequired => {
|
||||
SshKeyImportStatus::PasswordRequired
|
||||
}
|
||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::WrongPassword => {
|
||||
SshKeyImportStatus::WrongPassword
|
||||
}
|
||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::ParsingError => {
|
||||
SshKeyImportStatus::ParsingError
|
||||
}
|
||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::UnsupportedKeyType => {
|
||||
SshKeyImportStatus::UnsupportedKeyType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct SshKeyImportResult {
|
||||
pub status: SshKeyImportStatus,
|
||||
pub ssh_key: Option<SshKey>,
|
||||
}
|
||||
|
||||
impl From<desktop_core::ssh_agent::importer::SshKeyImportResult> for SshKeyImportResult {
|
||||
fn from(result: desktop_core::ssh_agent::importer::SshKeyImportResult) -> Self {
|
||||
SshKeyImportResult {
|
||||
status: result.status.into(),
|
||||
ssh_key: result.ssh_key.map(|k| k.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct SshUIRequest {
|
||||
pub cipher_id: Option<String>,
|
||||
@@ -359,13 +298,6 @@ pub mod sshagent {
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn import_key(encoded_key: String, password: String) -> napi::Result<SshKeyImportResult> {
|
||||
let result = desktop_core::ssh_agent::importer::import_key(encoded_key, password)
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> {
|
||||
let bitwarden_agent_state = &mut agent_state.state;
|
||||
|
||||
@@ -8,11 +8,11 @@ publish = { workspace = true }
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
desktop_core = { path = "../core" }
|
||||
futures = "=0.3.31"
|
||||
futures = { workspace = true }
|
||||
log = { workspace = true }
|
||||
simplelog = "=0.12.2"
|
||||
simplelog = { workspace = true }
|
||||
tokio = { workspace = true, features = ["io-std", "io-util", "macros", "rt"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
embed_plist = "=1.2.2"
|
||||
embed_plist = { workspace = true }
|
||||
|
||||
@@ -6,4 +6,5 @@ version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
bindgen = "0.71.1"
|
||||
bindgen = { workspace = true }
|
||||
|
||||
|
||||
@@ -423,7 +423,7 @@
|
||||
"enableHardwareAccelerationDesc" | i18n
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="showSshAgentOption">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label for="enableSshAgent">
|
||||
<input
|
||||
|
||||
@@ -22,7 +22,6 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
@@ -67,7 +66,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
showAlwaysShowDock = false;
|
||||
requireEnableTray = false;
|
||||
showDuckDuckGoIntegrationOption = false;
|
||||
showSshAgentOption = false;
|
||||
showOpenAtLoginOption = false;
|
||||
isWindows: boolean;
|
||||
isLinux: boolean;
|
||||
@@ -223,7 +221,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showSshAgentOption = await this.configService.getFeatureFlag(FeatureFlag.SSHAgent);
|
||||
this.userHasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
||||
|
||||
this.isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop;
|
||||
|
||||
@@ -51,17 +51,13 @@ import {
|
||||
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { HintComponent } from "../auth/hint.component";
|
||||
import { LoginDecryptionOptionsComponentV1 } from "../auth/login/login-decryption-options/login-decryption-options-v1.component";
|
||||
import { LoginComponentV1 } from "../auth/login/login-v1.component";
|
||||
import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-request-v1.component";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { SsoComponentV1 } from "../auth/sso-v1.component";
|
||||
import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { VaultComponent } from "../vault/app/vault/vault.component";
|
||||
|
||||
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
|
||||
import { SendComponent } from "./tools/send/send.component";
|
||||
|
||||
/**
|
||||
@@ -167,33 +163,6 @@ const routes: Routes = [
|
||||
},
|
||||
{ path: "accessibility-cookie", component: AccessibilityCookieComponent },
|
||||
{ path: "set-password", component: SetPasswordComponent },
|
||||
...unauthUiRefreshSwap(
|
||||
SsoComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "sso",
|
||||
},
|
||||
{
|
||||
path: "sso",
|
||||
data: {
|
||||
pageIcon: VaultIcon,
|
||||
pageTitle: {
|
||||
key: "enterpriseSingleSignOn",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "singleSignOnEnterOrgIdentifierText",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: SsoComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "send",
|
||||
component: SendComponent,
|
||||
@@ -209,139 +178,10 @@ const routes: Routes = [
|
||||
component: RemovePasswordComponent,
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
...unauthUiRefreshSwap(
|
||||
LoginViaAuthRequestComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login-with-device",
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "logInRequestSent",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginViaAuthRequestComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginViaAuthRequestComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "adminApprovalRequested",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "adminApprovalRequestSentToAdmins",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [{ path: "", component: LoginViaAuthRequestComponent }],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
HintComponent,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn()],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "requestPasswordHint",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
|
||||
},
|
||||
pageIcon: UserLockIcon,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: PasswordHintComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login",
|
||||
component: LoginComponentV1,
|
||||
canActivate: [maxAccountsGuardFn()],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [maxAccountsGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "logInToBitwarden",
|
||||
},
|
||||
pageIcon: VaultIcon,
|
||||
},
|
||||
children: [
|
||||
{ path: "", component: LoginComponent },
|
||||
{ path: "", component: LoginSecondaryContentComponent, outlet: "secondary" },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
},
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
},
|
||||
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "passkeys",
|
||||
component: Fido2PlaceholderComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
@@ -383,6 +223,110 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [maxAccountsGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "logInToBitwarden",
|
||||
},
|
||||
pageIcon: VaultIcon,
|
||||
},
|
||||
children: [
|
||||
{ path: "", component: LoginComponent },
|
||||
{ path: "", component: LoginSecondaryContentComponent, outlet: "secondary" },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
},
|
||||
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||
},
|
||||
{
|
||||
path: "sso",
|
||||
data: {
|
||||
pageIcon: VaultIcon,
|
||||
pageTitle: {
|
||||
key: "enterpriseSingleSignOn",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "singleSignOnEnterOrgIdentifierText",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: SsoComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "logInRequestSent",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginViaAuthRequestComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "adminApprovalRequested",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "adminApprovalRequestSentToAdmins",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [{ path: "", component: LoginViaAuthRequestComponent }],
|
||||
},
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "requestPasswordHint",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
|
||||
},
|
||||
pageIcon: UserLockIcon,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: PasswordHintComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "lock",
|
||||
canActivate: [lockGuard()],
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Router } from "@angular/router";
|
||||
import { filter, firstValueFrom, map, Subject, takeUntil, timeout, withLatestFrom } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { FingerprintDialogComponent, LoginApprovalComponent } from "@bitwarden/auth/angular";
|
||||
@@ -157,7 +159,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private stateEventRunnerService: StateEventRunnerService,
|
||||
private accountService: AccountService,
|
||||
private organizationService: OrganizationService,
|
||||
) {}
|
||||
private deviceTrustToastService: DeviceTrustToastService,
|
||||
) {
|
||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
|
||||
|
||||
@@ -46,8 +46,6 @@ import { HeaderComponent } from "./layout/header.component";
|
||||
import { NavComponent } from "./layout/nav.component";
|
||||
import { SearchComponent } from "./layout/search/search.component";
|
||||
import { SharedModule } from "./shared/shared.module";
|
||||
import { AddEditComponent as SendAddEditComponent } from "./tools/send/add-edit.component";
|
||||
import { SendComponent } from "./tools/send/send.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -60,6 +58,7 @@ import { SendComponent } from "./tools/send/send.component";
|
||||
DeleteAccountComponent,
|
||||
UserVerificationComponent,
|
||||
DecryptionFailureDialogComponent,
|
||||
NavComponent,
|
||||
],
|
||||
declarations: [
|
||||
AccessibilityCookieComponent,
|
||||
@@ -76,13 +75,10 @@ import { SendComponent } from "./tools/send/send.component";
|
||||
FolderAddEditComponent,
|
||||
HeaderComponent,
|
||||
HintComponent,
|
||||
NavComponent,
|
||||
PasswordHistoryComponent,
|
||||
PremiumComponent,
|
||||
RemovePasswordComponent,
|
||||
SearchComponent,
|
||||
SendAddEditComponent,
|
||||
SendComponent,
|
||||
SetPasswordComponent,
|
||||
SettingsComponent,
|
||||
ShareComponent,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<div
|
||||
style="background:white; display:flex; justify-content: center; align-items: center; flex-direction: column"
|
||||
>
|
||||
<h1 style="color: black">Select your passkey</h1>
|
||||
<br />
|
||||
<button
|
||||
style="color:black; padding: 10px 20px; border: 1px solid black; margin: 10px"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
(click)="closeModal()"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class Fido2PlaceholderComponent {
|
||||
constructor(
|
||||
private readonly desktopSettingsService: DesktopSettingsService,
|
||||
private readonly router: Router,
|
||||
) {}
|
||||
|
||||
async closeModal() {
|
||||
await this.router.navigate(["/"]);
|
||||
await this.desktopSettingsService.setInModalMode(false);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { RouterLink, RouterLinkActive } from "@angular/router";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-nav",
|
||||
templateUrl: "nav.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink, RouterLinkActive],
|
||||
})
|
||||
export class NavComponent {
|
||||
items: any[] = [
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
AuthService,
|
||||
AuthService as AuthServiceAbstraction,
|
||||
} from "@bitwarden/common/auth/abstractions/auth.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 { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
@@ -102,6 +103,7 @@ import {
|
||||
BiometricsService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { LockComponentService } from "@bitwarden/key-management-ui";
|
||||
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service";
|
||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||
@@ -366,6 +368,7 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DesktopSetPasswordJitService,
|
||||
deps: [
|
||||
ApiService,
|
||||
MasterPasswordApiService,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
@@ -430,6 +433,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DesktopLoginApprovalComponentService,
|
||||
deps: [I18nServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SshImportPromptService,
|
||||
useClass: DefaultSshImportPromptService,
|
||||
deps: [DialogService, ToastService, PlatformUtilsServiceAbstraction, I18nServiceAbstraction],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { CommonModule, DatePipe } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -16,11 +17,13 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { CalloutModule, DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-send-add-edit",
|
||||
templateUrl: "add-edit.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, JslibModule, ReactiveFormsModule, CalloutModule],
|
||||
})
|
||||
export class AddEditComponent extends BaseAddEditComponent {
|
||||
constructor(
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@@ -17,6 +20,7 @@ import { SendService } from "@bitwarden/common/tools/send/services/send.service.
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||
import { NavComponent } from "../../layout/nav.component";
|
||||
import { SearchBarService } from "../../layout/search/search-bar.service";
|
||||
|
||||
import { AddEditComponent } from "./add-edit.component";
|
||||
@@ -32,6 +36,8 @@ const BroadcasterSubscriptionId = "SendComponent";
|
||||
@Component({
|
||||
selector: "app-send",
|
||||
templateUrl: "send.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, JslibModule, FormsModule, NavComponent, AddEditComponent],
|
||||
})
|
||||
export class SendComponent extends BaseSendComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(AddEditComponent) addEditComponent: AddEditComponent;
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
<div id="login-page" class="page-top-padding">
|
||||
<form
|
||||
id="login-page"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
[formGroup]="formGroup"
|
||||
attr.aria-hidden="{{ showingModal }}"
|
||||
>
|
||||
<div id="content" class="content" style="padding-top: 50px">
|
||||
<a (click)="invalidateEmail()" class="tw-cursor-pointer">
|
||||
<img class="logo-image" alt="Bitwarden" />
|
||||
</a>
|
||||
<p class="lead">{{ "loginOrCreateNewAccount" | i18n }}</p>
|
||||
<!-- start email -->
|
||||
<ng-container *ngIf="!validatedEmail; else loginPage">
|
||||
<div class="box last">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
formControlName="email"
|
||||
appInputVerbatim="false"
|
||||
(keyup.enter)="continue()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<environment-selector #environmentSelector (onOpenSelfHostedSettings)="settings()">
|
||||
</environment-selector>
|
||||
</div>
|
||||
<div class="checkbox remember-email">
|
||||
<label for="rememberEmail">
|
||||
<input
|
||||
id="rememberEmail"
|
||||
type="checkbox"
|
||||
name="rememberEmail"
|
||||
formControlName="rememberEmail"
|
||||
/>
|
||||
{{ "rememberEmail" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="buttons with-rows">
|
||||
<div class="buttons-row">
|
||||
<button type="button" class="btn primary block" (click)="continue()">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<p class="no-margin">{{ "newAroundHere" | i18n }}</p>
|
||||
<button type="button" class="text text-primary" routerLink="/signup">
|
||||
{{ "createAccount" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template [formGroup]="formGroup" #loginPage>
|
||||
<div class="box last">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
class="monospaced"
|
||||
formControlName="masterPassword"
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
(click)="togglePassword()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box last" [hidden]="!showCaptcha()">
|
||||
<div class="box-content">
|
||||
<iframe
|
||||
id="hcaptcha_iframe"
|
||||
style="margin-top: 20px"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
<div class="box-content-row">
|
||||
<button
|
||||
class="btn block"
|
||||
type="button"
|
||||
routerLink="/accessibility-cookie"
|
||||
(click)="saveEmailSettings()"
|
||||
>
|
||||
<i class="bwi bwi-universal-access" aria-hidden="true"></i>
|
||||
{{ "loadAccessibilityCookie" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons with-rows">
|
||||
<div class="buttons-row">
|
||||
<button type="submit" class="btn primary block" [disabled]="form.loading">
|
||||
<b [hidden]="form.loading"
|
||||
><i class="bwi bwi-sign-in" aria-hidden="true"></i>
|
||||
{{ "loginWithMasterPassword" | i18n }}</b
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons-row" *ngIf="showLoginWithDevice">
|
||||
<button type="button" class="btn block" (click)="startAuthRequestLogin()">
|
||||
<i class="bwi bwi-mobile" aria-hidden="true"></i>
|
||||
{{ "logInWithAnotherDevice" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons-row">
|
||||
<button
|
||||
type="button"
|
||||
(click)="launchSsoBrowser('desktop', 'bitwarden://sso-callback')"
|
||||
class="btn block"
|
||||
>
|
||||
<i class="bwi bwi-provider" aria-hidden="true"></i>
|
||||
{{ "enterpriseSingleSignOn" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<button
|
||||
type="button"
|
||||
class="text text-primary password-hint-btn"
|
||||
routerLink="/hint"
|
||||
(click)="saveEmailSettings()"
|
||||
>
|
||||
{{ "getMasterPasswordHint" | i18n }}
|
||||
</button>
|
||||
<div>
|
||||
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ loggedEmail }}</p>
|
||||
<a [routerLink]="[]" (click)="toggleValidateEmail(false)">{{ "notYou" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ng-template #environment></ng-template>
|
||||
@@ -1,266 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
} 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { EnvironmentComponent } from "../environment.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "LoginComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-login",
|
||||
templateUrl: "login-v1.component.html",
|
||||
})
|
||||
export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("environment", { read: ViewContainerRef, static: true })
|
||||
environmentModal: ViewContainerRef;
|
||||
|
||||
protected componentDestroyed$: Subject<void> = new Subject();
|
||||
webVaultHostname = "";
|
||||
|
||||
showingModal = false;
|
||||
|
||||
private deferFocus: boolean = null;
|
||||
|
||||
get loggedEmail() {
|
||||
return this.formGroup.value.email;
|
||||
}
|
||||
|
||||
constructor(
|
||||
devicesApiService: DevicesApiServiceAbstraction,
|
||||
appIdService: AppIdService,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
syncService: SyncService,
|
||||
private modalService: ModalService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
stateService: StateService,
|
||||
environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
ngZone: NgZone,
|
||||
private messagingService: MessagingService,
|
||||
logService: LogService,
|
||||
formBuilder: FormBuilder,
|
||||
formValidationErrorService: FormValidationErrorsService,
|
||||
route: ActivatedRoute,
|
||||
loginEmailService: LoginEmailServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
devicesApiService,
|
||||
appIdService,
|
||||
loginStrategyService,
|
||||
router,
|
||||
platformUtilsService,
|
||||
i18nService,
|
||||
stateService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
cryptoFunctionService,
|
||||
logService,
|
||||
ngZone,
|
||||
formBuilder,
|
||||
formValidationErrorService,
|
||||
route,
|
||||
loginEmailService,
|
||||
ssoLoginService,
|
||||
toastService,
|
||||
);
|
||||
this.onSuccessfulLogin = () => {
|
||||
return syncService.fullSync(true);
|
||||
};
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.listenForUnauthUiRefreshFlagChanges();
|
||||
|
||||
await super.ngOnInit();
|
||||
await this.getLoginWithDevice(this.loggedEmail);
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
case "windowHidden":
|
||||
this.onWindowHidden();
|
||||
break;
|
||||
case "windowIsFocused":
|
||||
if (this.deferFocus === null) {
|
||||
this.deferFocus = !message.windowIsFocused;
|
||||
if (!this.deferFocus) {
|
||||
this.focusInput();
|
||||
}
|
||||
} else if (this.deferFocus && message.windowIsFocused) {
|
||||
this.focusInput();
|
||||
this.deferFocus = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
this.messagingService.send("getWindowIsFocused");
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
this.componentDestroyed$.next();
|
||||
this.componentDestroyed$.complete();
|
||||
}
|
||||
|
||||
private listenForUnauthUiRefreshFlagChanges() {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
|
||||
.pipe(
|
||||
tap(async (flag) => {
|
||||
if (flag) {
|
||||
const qParams = await firstValueFrom(this.route.queryParams);
|
||||
|
||||
const uniqueQueryParams = {
|
||||
...qParams,
|
||||
// adding a unique timestamp to the query params to force a reload
|
||||
t: new Date().getTime().toString(),
|
||||
};
|
||||
|
||||
await this.router.navigate(["/"], {
|
||||
queryParams: uniqueQueryParams,
|
||||
});
|
||||
}
|
||||
}),
|
||||
takeUntil(this.componentDestroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async settings() {
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||
EnvironmentComponent,
|
||||
this.environmentModal,
|
||||
);
|
||||
|
||||
modal.onShown.pipe(takeUntil(this.componentDestroyed$)).subscribe(() => {
|
||||
this.showingModal = true;
|
||||
});
|
||||
|
||||
modal.onClosed.pipe(takeUntil(this.componentDestroyed$)).subscribe(() => {
|
||||
this.showingModal = false;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||
childComponent.onSaved.pipe(takeUntil(this.componentDestroyed$)).subscribe(async () => {
|
||||
modal.close();
|
||||
await this.getLoginWithDevice(this.loggedEmail);
|
||||
});
|
||||
}
|
||||
|
||||
onWindowHidden() {
|
||||
this.showPassword = false;
|
||||
}
|
||||
|
||||
async continue() {
|
||||
await super.validateEmail();
|
||||
if (!this.formGroup.controls.email.valid) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccured"),
|
||||
message: this.i18nService.t("invalidEmail"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.focusInput();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.validatedEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
await super.submit();
|
||||
if (this.captchaSiteKey) {
|
||||
const content = document.getElementById("content") as HTMLDivElement;
|
||||
content.setAttribute("style", "width:335px");
|
||||
}
|
||||
}
|
||||
|
||||
private focusInput() {
|
||||
const email = this.loggedEmail;
|
||||
document.getElementById(email == null || email === "" ? "email" : "masterPassword")?.focus();
|
||||
}
|
||||
|
||||
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
||||
if (!ipc.platform.isAppImage && !ipc.platform.isSnapStore && !ipc.platform.isDev) {
|
||||
return super.launchSsoBrowser(clientId, ssoRedirectUri);
|
||||
}
|
||||
const email = this.formGroup.controls.email.value;
|
||||
|
||||
// Save off email for SSO
|
||||
await this.ssoLoginService.setSsoEmail(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);
|
||||
|
||||
try {
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(codeChallenge, state, email);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (err) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccured"),
|
||||
this.i18nService.t("ssoError"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the validatedEmail flag to false, which will show the login page.
|
||||
*/
|
||||
invalidateEmail() {
|
||||
this.validatedEmail = false;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<div id="login-with-device-page">
|
||||
<div id="content" class="content">
|
||||
<img class="logo-image" alt="Bitwarden" />
|
||||
|
||||
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
|
||||
<p class="lead text-center">{{ "logInRequestSent" | i18n }}</p>
|
||||
|
||||
<div class="box last">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<div class="section">
|
||||
<p class="section">
|
||||
{{ "notificationSentDevicePart1" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
class="tw-cursor-pointer"
|
||||
[href]="deviceManagementUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>{{ "notificationSentDeviceAnchor" | i18n }}</a
|
||||
>. {{ "notificationSentDevicePart2" | i18n }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="fingerprint section">
|
||||
<h4>{{ "fingerprintPhraseHeader" | i18n }}</h4>
|
||||
<code>{{ fingerprintPhrase }}</code>
|
||||
</div>
|
||||
|
||||
<div class="section" *ngIf="showResendNotification">
|
||||
<a [routerLink]="[]" disabled="true" (click)="startAuthRequestLogin()">{{
|
||||
"resendNotification" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
|
||||
<div class="sub-options another-method">
|
||||
<p class="no-margin description-text">
|
||||
{{ "needAnotherOption" | i18n }}
|
||||
<a type="button" class="text text-primary" (click)="back()">
|
||||
{{ "viewAllLoginOptions" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
|
||||
<p class="lead text-center">{{ "adminApprovalRequested" | i18n }}</p>
|
||||
|
||||
<div class="box last">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<div class="section">
|
||||
<p class="section">{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
|
||||
<p class="section">{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
|
||||
</div>
|
||||
|
||||
<div class="fingerprint section">
|
||||
<h4>{{ "fingerprintPhraseHeader" | i18n }}</h4>
|
||||
<code>{{ fingerprintPhrase }}</code>
|
||||
</div>
|
||||
|
||||
<div class="sub-options another-method">
|
||||
<p class="no-margin description-text">
|
||||
{{ "troubleLoggingIn" | i18n }}
|
||||
<a type="button" class="text text-primary" (click)="back()">
|
||||
{{ "viewAllLoginOptions" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #environment></ng-template>
|
||||
@@ -1,117 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Location } from "@angular/common";
|
||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { LoginViaAuthRequestComponentV1 as BaseLoginViaAuthRequestComponentV1 } from "@bitwarden/angular/auth/components/login-via-auth-request-v1.component";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import {
|
||||
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 { 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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { EnvironmentComponent } from "../environment.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-login-via-auth-request",
|
||||
templateUrl: "login-via-auth-request-v1.component.html",
|
||||
})
|
||||
export class LoginViaAuthRequestComponentV1 extends BaseLoginViaAuthRequestComponentV1 {
|
||||
@ViewChild("environment", { read: ViewContainerRef, static: true })
|
||||
environmentModal: ViewContainerRef;
|
||||
showingModal = false;
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
keyService: KeyService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
appIdService: AppIdService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
apiService: ApiService,
|
||||
authService: AuthService,
|
||||
logService: LogService,
|
||||
environmentService: EnvironmentService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
anonymousHubService: AnonymousHubService,
|
||||
validationService: ValidationService,
|
||||
private modalService: ModalService,
|
||||
syncService: SyncService,
|
||||
loginEmailService: LoginEmailServiceAbstraction,
|
||||
deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
authRequestService: AuthRequestServiceAbstraction,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
private location: Location,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
keyService,
|
||||
cryptoFunctionService,
|
||||
appIdService,
|
||||
passwordGenerationService,
|
||||
apiService,
|
||||
authService,
|
||||
logService,
|
||||
environmentService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
anonymousHubService,
|
||||
validationService,
|
||||
accountService,
|
||||
loginEmailService,
|
||||
deviceTrustService,
|
||||
authRequestService,
|
||||
loginStrategyService,
|
||||
toastService,
|
||||
);
|
||||
|
||||
this.onSuccessfulLogin = () => {
|
||||
return syncService.fullSync(true);
|
||||
};
|
||||
}
|
||||
|
||||
async settings() {
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||
EnvironmentComponent,
|
||||
this.environmentModal,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
modal.onShown.subscribe(() => {
|
||||
this.showingModal = true;
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
modal.onClosed.subscribe(() => {
|
||||
this.showingModal = false;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
childComponent.onSaved.subscribe(() => {
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
|
||||
back() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,10 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components
|
||||
import { SharedModule } from "../../app/shared/shared.module";
|
||||
|
||||
import { LoginDecryptionOptionsComponentV1 } from "./login-decryption-options/login-decryption-options-v1.component";
|
||||
import { LoginComponentV1 } from "./login-v1.component";
|
||||
import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, RouterModule],
|
||||
declarations: [
|
||||
LoginComponentV1,
|
||||
LoginViaAuthRequestComponentV1,
|
||||
EnvironmentSelectorComponent,
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
],
|
||||
exports: [LoginComponentV1, LoginViaAuthRequestComponentV1],
|
||||
declarations: [EnvironmentSelectorComponent, LoginDecryptionOptionsComponentV1],
|
||||
exports: [],
|
||||
})
|
||||
export class LoginModule {}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -40,6 +41,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
|
||||
policyApiService: PolicyApiServiceAbstraction,
|
||||
policyService: PolicyService,
|
||||
router: Router,
|
||||
masterPasswordApiService: MasterPasswordApiService,
|
||||
syncService: SyncService,
|
||||
route: ActivatedRoute,
|
||||
private broadcasterService: BroadcasterService,
|
||||
@@ -63,6 +65,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On
|
||||
policyApiService,
|
||||
policyService,
|
||||
router,
|
||||
masterPasswordApiService,
|
||||
apiService,
|
||||
syncService,
|
||||
route,
|
||||
|
||||
@@ -25,16 +25,6 @@ export class MainSshAgentService {
|
||||
private logService: LogService,
|
||||
private messagingService: MessagingService,
|
||||
) {
|
||||
ipcMain.handle(
|
||||
"sshagent.importkey",
|
||||
async (
|
||||
event: any,
|
||||
{ privateKey, password }: { privateKey: string; password?: string },
|
||||
): Promise<sshagent.SshKeyImportResult> => {
|
||||
return sshagent.importKey(privateKey, password);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle("sshagent.init", async (event: any, message: any) => {
|
||||
this.init();
|
||||
});
|
||||
|
||||
@@ -24,8 +24,6 @@ import {
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
@@ -58,23 +56,13 @@ export class SshAgentService implements OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
private desktopSettingsService: DesktopSettingsService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SSHAgent)
|
||||
.pipe(
|
||||
concatMap(async (enabled) => {
|
||||
this.isFeatureFlagEnabled = enabled;
|
||||
if (!(await ipc.platform.sshAgent.isLoaded()) && enabled) {
|
||||
await ipc.platform.sshAgent.init();
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
if (!(await ipc.platform.sshAgent.isLoaded())) {
|
||||
await ipc.platform.sshAgent.init();
|
||||
}
|
||||
|
||||
await this.initListeners();
|
||||
}
|
||||
|
||||
@@ -3060,9 +3060,6 @@
|
||||
"adminApprovalRequestSentToAdmins": {
|
||||
"message": "Your request has been sent to your admin."
|
||||
},
|
||||
"youWillBeNotifiedOnceApproved": {
|
||||
"message": "You will be notified once approved."
|
||||
},
|
||||
"troubleLoggingIn": {
|
||||
"message": "Trouble logging in?"
|
||||
},
|
||||
@@ -3532,9 +3529,6 @@
|
||||
"unknownApplication": {
|
||||
"message": "An application"
|
||||
},
|
||||
"sshKeyPasswordUnsupported": {
|
||||
"message": "Importing password protected SSH keys is not yet supported"
|
||||
},
|
||||
"invalidSshKey": {
|
||||
"message": "The SSH key is invalid"
|
||||
},
|
||||
@@ -3544,7 +3538,7 @@
|
||||
"importSshKeyFromClipboard": {
|
||||
"message": "Import key from clipboard"
|
||||
},
|
||||
"sshKeyPasted": {
|
||||
"sshKeyImported": {
|
||||
"message": "SSH key imported successfully"
|
||||
},
|
||||
"fileSavedToDevice": {
|
||||
|
||||
@@ -284,6 +284,8 @@ export class Main {
|
||||
this.migrationRunner.run().then(
|
||||
async () => {
|
||||
await this.toggleHardwareAcceleration();
|
||||
// Reset modal mode to make sure main window is displayed correctly
|
||||
await this.desktopSettingsService.resetInModalMode();
|
||||
await this.windowMain.init();
|
||||
await this.i18nService.init();
|
||||
await this.messagingMain.init();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import * as path from "path";
|
||||
import * as url from "url";
|
||||
|
||||
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
@@ -9,6 +10,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { BiometricStateService, BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
|
||||
import { cleanUserAgent, isDev } from "../utils";
|
||||
|
||||
import { WindowMain } from "./window.main";
|
||||
|
||||
@@ -49,6 +51,11 @@ export class TrayMain {
|
||||
label: this.i18nService.t("showHide"),
|
||||
click: () => this.toggleWindow(),
|
||||
},
|
||||
{
|
||||
visible: isDev(),
|
||||
label: "Fake Popup",
|
||||
click: () => this.fakePopup(),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("exit"),
|
||||
@@ -190,7 +197,7 @@ export class TrayMain {
|
||||
this.hideDock();
|
||||
}
|
||||
} else {
|
||||
this.windowMain.win.show();
|
||||
this.windowMain.show();
|
||||
if (this.isDarwin()) {
|
||||
this.showDock();
|
||||
}
|
||||
@@ -203,4 +210,38 @@ export class TrayMain {
|
||||
this.windowMain.win.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to test modal behavior during development and could be removed in the future.
|
||||
* @returns
|
||||
*/
|
||||
private async fakePopup() {
|
||||
if (this.windowMain.win == null || this.windowMain.win.isDestroyed()) {
|
||||
await this.windowMain.createWindow("modal-app");
|
||||
return;
|
||||
}
|
||||
|
||||
// Restyle existing
|
||||
const existingWin = this.windowMain.win;
|
||||
|
||||
await this.desktopSettingsService.setInModalMode(true);
|
||||
await existingWin.loadURL(
|
||||
url.format({
|
||||
protocol: "file:",
|
||||
//pathname: `${__dirname}/index.html`,
|
||||
pathname: path.join(__dirname, "/index.html"),
|
||||
slashes: true,
|
||||
hash: "/passkeys",
|
||||
query: {
|
||||
redirectUrl: "/passkeys",
|
||||
},
|
||||
}),
|
||||
{
|
||||
userAgent: cleanUserAgent(existingWin.webContents.userAgent),
|
||||
},
|
||||
);
|
||||
existingWin.once("ready-to-show", () => {
|
||||
existingWin.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as path from "path";
|
||||
import * as url from "url";
|
||||
|
||||
import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "electron";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { concatMap, firstValueFrom, pairwise } from "rxjs";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
@@ -14,6 +14,7 @@ import { processisolations } from "@bitwarden/desktop-napi";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { WindowState } from "../platform/models/domain/window-state";
|
||||
import { applyMainWindowStyles, applyPopupModalStyles } from "../platform/popup-modal-styles";
|
||||
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
|
||||
import { cleanUserAgent, isDev, isLinux, isMac, isMacAppStore, isWindows } from "../utils";
|
||||
|
||||
@@ -77,6 +78,24 @@ export class WindowMain {
|
||||
}
|
||||
});
|
||||
|
||||
this.desktopSettingsService.inModalMode$
|
||||
.pipe(
|
||||
pairwise(),
|
||||
concatMap(async ([lastValue, newValue]) => {
|
||||
if (lastValue && !newValue) {
|
||||
// Reset the window state to the main window state
|
||||
applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]);
|
||||
// Because modal is used in front of another app, UX wise it makes sense to hide the main window when leaving modal mode.
|
||||
this.win.hide();
|
||||
} else if (!lastValue && newValue) {
|
||||
// Apply the popup modal styles
|
||||
applyPopupModalStyles(this.win);
|
||||
this.win.show();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.desktopSettingsService.preventScreenshots$.subscribe((prevent) => {
|
||||
if (this.win == null) {
|
||||
return;
|
||||
@@ -182,7 +201,20 @@ export class WindowMain {
|
||||
});
|
||||
}
|
||||
|
||||
async createWindow(): Promise<void> {
|
||||
/// Show the window with main window styles
|
||||
show() {
|
||||
if (this.win != null) {
|
||||
applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]);
|
||||
this.win.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the main window. The template argument is used to determine the styling of the window and what url will be loaded.
|
||||
* When the template is "modal-app", the window will be styled as a modal and the passkeys page will be loaded.
|
||||
* TODO: We might want to refactor the template argument to accomodate more target pages, e.g. ssh-agent.
|
||||
*/
|
||||
async createWindow(template: "full-app" | "modal-app" = "full-app"): Promise<void> {
|
||||
this.windowStates[mainWindowSizeKey] = await this.getWindowState(
|
||||
this.defaultWidth,
|
||||
this.defaultHeight,
|
||||
@@ -216,6 +248,12 @@ export class WindowMain {
|
||||
},
|
||||
});
|
||||
|
||||
if (template === "modal-app") {
|
||||
applyPopupModalStyles(this.win);
|
||||
} else {
|
||||
applyMainWindowStyles(this.win, this.windowStates[mainWindowSizeKey]);
|
||||
}
|
||||
|
||||
this.win.webContents.on("dom-ready", () => {
|
||||
this.win.webContents.zoomFactor = this.windowStates[mainWindowSizeKey].zoomFactor ?? 1.0;
|
||||
});
|
||||
@@ -225,21 +263,41 @@ export class WindowMain {
|
||||
}
|
||||
|
||||
// Show it later since it might need to be maximized.
|
||||
this.win.show();
|
||||
// use once event to avoid flash on unstyled content.
|
||||
this.win.once("ready-to-show", () => {
|
||||
this.win.show();
|
||||
});
|
||||
|
||||
// and load the index.html of the app.
|
||||
// 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.win.loadURL(
|
||||
url.format({
|
||||
protocol: "file:",
|
||||
pathname: path.join(__dirname, "/index.html"),
|
||||
slashes: true,
|
||||
}),
|
||||
{
|
||||
userAgent: cleanUserAgent(this.win.webContents.userAgent),
|
||||
},
|
||||
);
|
||||
if (template === "full-app") {
|
||||
// and load the index.html of the app.
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
void this.win.loadURL(
|
||||
url.format({
|
||||
protocol: "file:",
|
||||
pathname: path.join(__dirname, "/index.html"),
|
||||
slashes: true,
|
||||
}),
|
||||
{
|
||||
userAgent: cleanUserAgent(this.win.webContents.userAgent),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// we're in modal mode - load the passkeys page
|
||||
await this.win.loadURL(
|
||||
url.format({
|
||||
protocol: "file:",
|
||||
pathname: path.join(__dirname, "/index.html"),
|
||||
slashes: true,
|
||||
hash: "/passkeys",
|
||||
query: {
|
||||
redirectUrl: "/passkeys",
|
||||
},
|
||||
}),
|
||||
{
|
||||
userAgent: cleanUserAgent(this.win.webContents.userAgent),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Open the DevTools.
|
||||
if (isDev()) {
|
||||
@@ -336,6 +394,12 @@ export class WindowMain {
|
||||
return;
|
||||
}
|
||||
|
||||
const inModalMode = await firstValueFrom(this.desktopSettingsService.inModalMode$);
|
||||
|
||||
if (inModalMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const bounds = win.getBounds();
|
||||
|
||||
@@ -346,9 +410,14 @@ export class WindowMain {
|
||||
}
|
||||
}
|
||||
|
||||
this.windowStates[configKey].isMaximized = win.isMaximized();
|
||||
// We treat fullscreen as maximized (would be even better to store isFullscreen as its own flag).
|
||||
this.windowStates[configKey].isMaximized = win.isMaximized() || win.isFullScreen();
|
||||
this.windowStates[configKey].displayBounds = screen.getDisplayMatching(bounds).bounds;
|
||||
|
||||
// Maybe store these as well?
|
||||
// win.isFocused();
|
||||
// win.isVisible();
|
||||
|
||||
if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) {
|
||||
this.windowStates[configKey].x = bounds.x;
|
||||
this.windowStates[configKey].y = bounds.y;
|
||||
|
||||
52
apps/desktop/src/platform/popup-modal-styles.ts
Normal file
52
apps/desktop/src/platform/popup-modal-styles.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
import { WindowState } from "./models/domain/window-state";
|
||||
|
||||
// change as needed, however limited by mainwindow minimum size
|
||||
const popupWidth = 680;
|
||||
const popupHeight = 500;
|
||||
|
||||
export function applyPopupModalStyles(window: BrowserWindow) {
|
||||
window.unmaximize();
|
||||
window.setSize(popupWidth, popupHeight);
|
||||
window.center();
|
||||
window.setWindowButtonVisibility?.(false);
|
||||
window.setMenuBarVisibility?.(false);
|
||||
window.setResizable(false);
|
||||
window.setAlwaysOnTop(true);
|
||||
|
||||
// Adjusting from full screen is a bit more hassle
|
||||
if (window.isFullScreen()) {
|
||||
window.setFullScreen(false);
|
||||
window.once("leave-full-screen", () => {
|
||||
window.setSize(popupWidth, popupHeight);
|
||||
window.center();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function applyMainWindowStyles(window: BrowserWindow, existingWindowState: WindowState) {
|
||||
window.setMinimumSize(680, 500);
|
||||
|
||||
// need to guard against null/undefined values
|
||||
|
||||
if (existingWindowState?.width && existingWindowState?.height) {
|
||||
window.setSize(existingWindowState.width, existingWindowState.height);
|
||||
}
|
||||
|
||||
if (existingWindowState?.x && existingWindowState?.y) {
|
||||
window.setPosition(existingWindowState.x, existingWindowState.y);
|
||||
}
|
||||
|
||||
window.setWindowButtonVisibility?.(true);
|
||||
window.setMenuBarVisibility?.(true);
|
||||
window.setResizable(true);
|
||||
window.setAlwaysOnTop(false);
|
||||
|
||||
// We're currently not recovering the maximized state, mostly due to conflicts with hiding the window.
|
||||
// window.setFullScreen(existingWindowState.isMaximized);
|
||||
|
||||
// if (existingWindowState.isMaximized) {
|
||||
// window.maximize();
|
||||
// }
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { sshagent as ssh } from "desktop_native/napi";
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
@@ -64,13 +63,6 @@ const sshAgent = {
|
||||
clearKeys: async () => {
|
||||
return await ipcRenderer.invoke("sshagent.clearkeys");
|
||||
},
|
||||
importKey: async (key: string, password: string): Promise<ssh.SshKeyImportResult> => {
|
||||
const res = await ipcRenderer.invoke("sshagent.importkey", {
|
||||
privateKey: key,
|
||||
password: password,
|
||||
});
|
||||
return res;
|
||||
},
|
||||
isLoaded(): Promise<boolean> {
|
||||
return ipcRenderer.invoke("sshagent.isloaded");
|
||||
},
|
||||
|
||||
@@ -75,6 +75,10 @@ const MINIMIZE_ON_COPY = new UserKeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "
|
||||
clearOn: [], // User setting, no need to clear
|
||||
});
|
||||
|
||||
const IN_MODAL_MODE = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "inModalMode", {
|
||||
deserializer: (b) => b,
|
||||
});
|
||||
|
||||
const PREVENT_SCREENSHOTS = new KeyDefinition<boolean>(
|
||||
DESKTOP_SETTINGS_DISK,
|
||||
"preventScreenshots",
|
||||
@@ -170,6 +174,10 @@ export class DesktopSettingsService {
|
||||
*/
|
||||
minimizeOnCopy$ = this.minimizeOnCopyState.state$.pipe(map(Boolean));
|
||||
|
||||
private readonly inModalModeState = this.stateProvider.getGlobal(IN_MODAL_MODE);
|
||||
|
||||
inModalMode$ = this.inModalModeState.state$.pipe(map(Boolean));
|
||||
|
||||
constructor(private stateProvider: StateProvider) {
|
||||
this.window$ = this.windowState.state$.pipe(
|
||||
map((window) =>
|
||||
@@ -178,6 +186,14 @@ export class DesktopSettingsService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to clear the setting on application start to make sure we don't end up
|
||||
* stuck in modal mode if the application is force-closed in modal mode.
|
||||
*/
|
||||
async resetInModalMode() {
|
||||
await this.inModalModeState.update(() => false);
|
||||
}
|
||||
|
||||
async setHardwareAcceleration(enabled: boolean) {
|
||||
await this.hwState.update(() => enabled);
|
||||
}
|
||||
@@ -286,6 +302,14 @@ export class DesktopSettingsService {
|
||||
await this.stateProvider.getUser(userId, MINIMIZE_ON_COPY).update(() => value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the modal mode of the application. Setting this changes the windows-size and other properties.
|
||||
* @param value `true` if the application is in modal mode, `false` if it is not.
|
||||
*/
|
||||
async setInModalMode(value: boolean) {
|
||||
await this.inModalModeState.update(() => value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the setting for whether or not the screenshot protection is enabled.
|
||||
* @param value `true` if the screenshot protection is enabled, `false` if it is not.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@import "variables.scss";
|
||||
|
||||
#login-page,
|
||||
#login-with-device-page,
|
||||
#lock-page,
|
||||
#sso-page,
|
||||
#set-password-page,
|
||||
@@ -191,7 +190,6 @@
|
||||
}
|
||||
|
||||
#login-page,
|
||||
#login-with-device-page,
|
||||
#login-decryption-options-page {
|
||||
flex-direction: column;
|
||||
justify-content: unset;
|
||||
@@ -222,41 +220,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
#login-with-device-page {
|
||||
.content {
|
||||
display: block;
|
||||
padding-top: 70px;
|
||||
width: 350px !important;
|
||||
|
||||
.fingerprint {
|
||||
margin: auto;
|
||||
width: 315px;
|
||||
|
||||
.fingerpint-header {
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.another-method {
|
||||
display: flex;
|
||||
margin: auto;
|
||||
.description-text {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
@include themify($themes) {
|
||||
color: themed("codeColor");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#login-approval-page {
|
||||
.section-title {
|
||||
padding: 20px;
|
||||
|
||||
@@ -512,6 +512,15 @@
|
||||
[ngClass]="{ 'bwi-eye': !showPrivateKey, 'bwi-eye-slash': showPrivateKey }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'importSshKeyFromClipboard' | i18n }}"
|
||||
(click)="importSshKeyFromClipboard()"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-paste" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
@@ -559,16 +568,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
(click)="importSshKeyFromClipboard()"
|
||||
>
|
||||
{{ "importSshKeyFromClipboard" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { NgForm } from "@angular/forms";
|
||||
import { sshagent as sshAgent } from "desktop_native/napi";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
|
||||
@@ -25,8 +23,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { SshKeyPasswordPromptComponent } from "@bitwarden/importer-ui";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
const BroadcasterSubscriptionId = "AddEditComponent";
|
||||
|
||||
@@ -60,6 +57,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
toastService: ToastService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
sdkService: SdkService,
|
||||
sshImportPromptService: SshImportPromptService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
@@ -82,6 +80,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
cipherAuthorizationService,
|
||||
toastService,
|
||||
sdkService,
|
||||
sshImportPromptService,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,69 +158,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
this.cipher.revisionDate = cipher.revisionDate;
|
||||
}
|
||||
|
||||
async importSshKeyFromClipboard(password: string = "") {
|
||||
const key = await this.platformUtilsService.readFromClipboard();
|
||||
const parsedKey = await ipc.platform.sshAgent.importKey(key, password);
|
||||
if (parsedKey == null) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("invalidSshKey"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (parsedKey.status) {
|
||||
case sshAgent.SshKeyImportStatus.ParsingError:
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("invalidSshKey"),
|
||||
});
|
||||
return;
|
||||
case sshAgent.SshKeyImportStatus.UnsupportedKeyType:
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("sshKeyTypeUnsupported"),
|
||||
});
|
||||
return;
|
||||
case sshAgent.SshKeyImportStatus.PasswordRequired:
|
||||
case sshAgent.SshKeyImportStatus.WrongPassword:
|
||||
if (password !== "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("sshKeyWrongPassword"),
|
||||
});
|
||||
} else {
|
||||
password = await this.getSshKeyPassword();
|
||||
if (password === "") {
|
||||
return;
|
||||
}
|
||||
await this.importSshKeyFromClipboard(password);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
this.cipher.sshKey.privateKey = parsedKey.sshKey.privateKey;
|
||||
this.cipher.sshKey.publicKey = parsedKey.sshKey.publicKey;
|
||||
this.cipher.sshKey.keyFingerprint = parsedKey.sshKey.keyFingerprint;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("sshKeyPasted"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getSshKeyPassword(): Promise<string> {
|
||||
const dialog = this.dialogService.open<string>(SshKeyPasswordPromptComponent, {
|
||||
ariaModal: true,
|
||||
});
|
||||
|
||||
return await lastValueFrom(dialog.closed);
|
||||
}
|
||||
|
||||
truncateString(value: string, length: number) {
|
||||
return value.length > length ? value.substring(0, length) + "..." : value;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
<li
|
||||
class="filter-option"
|
||||
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.SshKey }"
|
||||
*ngIf="isSshKeysEnabled"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/type-filter.component";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-type-filter",
|
||||
templateUrl: "type-filter.component.html",
|
||||
})
|
||||
export class TypeFilterComponent extends BaseTypeFilterComponent implements OnInit {
|
||||
isSshKeysEnabled = false;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
export class TypeFilterComponent extends BaseTypeFilterComponent {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.isSshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</ng-container>
|
||||
<small *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
||||
@@ -45,22 +45,16 @@
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-org-vault-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-3" *ngIf="!hideVaultFilters">
|
||||
<div class="groupings">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<app-organization-vault-filter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-flex tw-flex-row">
|
||||
<div class="tw-w-1/4 tw-mr-5" *ngIf="!hideVaultFilters">
|
||||
<app-organization-vault-filter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
<div [class]="hideVaultFilters ? 'col-12' : 'col-9'">
|
||||
<div [class]="hideVaultFilters ? 'tw-w-4/5' : 'tw-w-3/4'">
|
||||
<bit-toggle-group
|
||||
*ngIf="showAddAccessToggle && activeFilter.selectedCollectionNode"
|
||||
[selected]="addAccessStatus$ | async"
|
||||
@@ -140,7 +134,7 @@
|
||||
*ngIf="performingInitialLoad"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
catchError,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
@@ -76,6 +77,7 @@ import {
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
|
||||
import {
|
||||
ResellerWarning,
|
||||
ResellerWarningService,
|
||||
@@ -256,6 +258,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private resellerWarningService: ResellerWarningService,
|
||||
private accountService: AccountService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -636,12 +639,18 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
combineLatest([
|
||||
of(org),
|
||||
this.organizationApiService.getSubscription(org.id),
|
||||
this.organizationBillingService.getPaymentSource(org.id),
|
||||
from(this.organizationBillingService.getPaymentSource(org.id)).pipe(
|
||||
catchError((error: unknown) => {
|
||||
this.billingNotificationService.handleError(error);
|
||||
return of(null);
|
||||
}),
|
||||
),
|
||||
]),
|
||||
),
|
||||
map(([org, sub, paymentSource]) => {
|
||||
return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource);
|
||||
}),
|
||||
map(([org, sub, paymentSource]) =>
|
||||
this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(org, sub, paymentSource),
|
||||
),
|
||||
filter((result) => result !== null),
|
||||
);
|
||||
|
||||
this.resellerWarning$ = organization$.pipe(
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
buttonType="primary"
|
||||
[disabled]="loading || dialogReadonly"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
{{ buttonDisplayName | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
OrganizationUserUserMiniResponse,
|
||||
CollectionResponse,
|
||||
CollectionView,
|
||||
CollectionService,
|
||||
Collection,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
getOrganizationById,
|
||||
@@ -32,13 +34,17 @@ import {
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SelectModule, BitValidators, DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { openChangePlanDialog } from "../../../../../billing/organizations/change-plan-dialog.component";
|
||||
import { SharedModule } from "../../../../../shared";
|
||||
import { GroupApiService, GroupView } from "../../../core";
|
||||
import { freeOrgCollectionLimitValidator } from "../../validators/free-org-collection-limit.validator";
|
||||
import { PermissionMode } from "../access-selector/access-selector.component";
|
||||
import {
|
||||
AccessItemType,
|
||||
@@ -55,6 +61,19 @@ export enum CollectionDialogTabType {
|
||||
Access = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing button labels for the "Add New Collection" dialog.
|
||||
*
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
enum ButtonType {
|
||||
/** Displayed when the user has reached the maximum number of collections allowed for the organization. */
|
||||
Upgrade = "upgrade",
|
||||
/** Displayed when the user can still add more collections within the allowed limit. */
|
||||
Save = "save",
|
||||
}
|
||||
|
||||
export interface CollectionDialogParams {
|
||||
collectionId?: string;
|
||||
organizationId: string;
|
||||
@@ -78,6 +97,7 @@ export enum CollectionDialogAction {
|
||||
Saved = "saved",
|
||||
Canceled = "canceled",
|
||||
Deleted = "deleted",
|
||||
Upgrade = "upgrade",
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -107,6 +127,9 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
protected PermissionMode = PermissionMode;
|
||||
protected showDeleteButton = false;
|
||||
protected showAddAccessWarning = false;
|
||||
protected collections: Collection[];
|
||||
protected buttonDisplayName: ButtonType = ButtonType.Save;
|
||||
private orgExceedingCollectionLimit!: Organization;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: CollectionDialogParams,
|
||||
@@ -122,6 +145,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private accountService: AccountService,
|
||||
private toastService: ToastService,
|
||||
private collectionService: CollectionService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info;
|
||||
}
|
||||
@@ -151,6 +176,23 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
this.formGroup.patchValue({ selectedOrg: this.params.organizationId });
|
||||
await this.loadOrg(this.params.organizationId);
|
||||
}
|
||||
|
||||
const isBreadcrumbEventLogsEnabled = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs),
|
||||
);
|
||||
|
||||
if (isBreadcrumbEventLogsEnabled) {
|
||||
this.collections = await this.collectionService.getAll();
|
||||
this.organizationSelected.setAsyncValidators(
|
||||
freeOrgCollectionLimitValidator(this.organizations$, this.collections, this.i18nService),
|
||||
);
|
||||
this.formGroup.updateValueAndValidity();
|
||||
}
|
||||
|
||||
this.organizationSelected.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((_) => {
|
||||
this.organizationSelected.markAsTouched();
|
||||
this.formGroup.updateValueAndValidity();
|
||||
});
|
||||
}
|
||||
|
||||
async loadOrg(orgId: string) {
|
||||
@@ -263,6 +305,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
get organizationSelected() {
|
||||
return this.formGroup.controls.selectedOrg;
|
||||
}
|
||||
|
||||
protected get collectionId() {
|
||||
return this.params.collectionId;
|
||||
}
|
||||
@@ -287,6 +333,12 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (this.buttonDisplayName == ButtonType.Upgrade) {
|
||||
this.close(CollectionDialogAction.Upgrade);
|
||||
this.changePlan(this.orgExceedingCollectionLimit);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
const accessTabError = this.formGroup.controls.access.hasError("managePermissionRequired");
|
||||
|
||||
@@ -369,6 +421,16 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private changePlan(org: Organization) {
|
||||
openChangePlanDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: org.id,
|
||||
subscription: null,
|
||||
productTierType: org.productTierType,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private handleAddAccessWarning(): boolean {
|
||||
if (
|
||||
!this.organization?.allowAdminAccessToAllCollectionItems &&
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { AbstractControl, FormControl, ValidationErrors } from "@angular/forms";
|
||||
import { lastValueFrom, Observable, of } from "rxjs";
|
||||
|
||||
import { Collection } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { freeOrgCollectionLimitValidator } from "./free-org-collection-limit.validator";
|
||||
|
||||
describe("freeOrgCollectionLimitValidator", () => {
|
||||
let i18nService: I18nService;
|
||||
|
||||
beforeEach(() => {
|
||||
i18nService = {
|
||||
t: (key: string) => key,
|
||||
} as any;
|
||||
});
|
||||
|
||||
it("returns null if organization is not found", async () => {
|
||||
const orgs: Organization[] = [];
|
||||
const validator = freeOrgCollectionLimitValidator(of(orgs), [], i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result: Observable<ValidationErrors> = validator(control) as Observable<ValidationErrors>;
|
||||
|
||||
const value = await lastValueFrom(result);
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null if control is not an instance of FormControl", async () => {
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), [], i18nService);
|
||||
const control = {} as AbstractControl;
|
||||
|
||||
const result: Observable<ValidationErrors | null> = validator(
|
||||
control,
|
||||
) as Observable<ValidationErrors>;
|
||||
|
||||
const value = await lastValueFrom(result);
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null if control is not provided", async () => {
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), [], i18nService);
|
||||
|
||||
const result: Observable<ValidationErrors | null> = validator(
|
||||
undefined as any,
|
||||
) as Observable<ValidationErrors>;
|
||||
|
||||
const value = await lastValueFrom(result);
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null if organization has not reached collection limit (Observable)", async () => {
|
||||
const org = { id: "org-id", maxCollections: 2 } as Organization;
|
||||
const collections = [{ organizationId: "org-id" } as Collection];
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), collections, i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result$ = validator(control) as Observable<ValidationErrors | null>;
|
||||
|
||||
const value = await lastValueFrom(result$);
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
|
||||
it("returns error if organization has reached collection limit (Observable)", async () => {
|
||||
const org = { id: "org-id", maxCollections: 1 } as Organization;
|
||||
const collections = [{ organizationId: "org-id" } as Collection];
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), collections, i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result$ = validator(control) as Observable<ValidationErrors | null>;
|
||||
|
||||
const value = await lastValueFrom(result$);
|
||||
expect(value).toEqual({
|
||||
cannotCreateCollections: { message: "cannotCreateCollection" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { AbstractControl, AsyncValidatorFn, FormControl, ValidationErrors } from "@angular/forms";
|
||||
import { map, Observable, of } from "rxjs";
|
||||
|
||||
import { Collection } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
export function freeOrgCollectionLimitValidator(
|
||||
orgs: Observable<Organization[]>,
|
||||
collections: Collection[],
|
||||
i18nService: I18nService,
|
||||
): AsyncValidatorFn {
|
||||
return (control: AbstractControl): Observable<ValidationErrors | null> => {
|
||||
if (!(control instanceof FormControl)) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
const orgId = control.value;
|
||||
|
||||
if (!orgId) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return orgs.pipe(
|
||||
map((organizations) => organizations.find((org) => org.id === orgId)),
|
||||
map((org) => {
|
||||
if (!org) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orgCollections = collections.filter((c) => c.organizationId === org.id);
|
||||
const hasReachedLimit = org.maxCollections === orgCollections.length;
|
||||
|
||||
if (hasReachedLimit) {
|
||||
return {
|
||||
cannotCreateCollections: { message: i18nService.t("cannotCreateCollection") },
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
// @ts-strict-ignore
|
||||
import { DOCUMENT } from "@angular/common";
|
||||
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import * as jq from "jquery";
|
||||
import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
@@ -95,7 +97,10 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private apiService: ApiService,
|
||||
private appIdService: AppIdService,
|
||||
private processReloadService: ProcessReloadServiceAbstraction,
|
||||
) {}
|
||||
private deviceTrustToastService: DeviceTrustToastService,
|
||||
) {
|
||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.i18nService.locale$.pipe(takeUntil(this.destroy$)).subscribe((locale) => {
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
<form
|
||||
[bitSubmit]="submitForm.bind(null, false)"
|
||||
[appApiAction]="formPromise"
|
||||
[formGroup]="formGroup"
|
||||
>
|
||||
<ng-container *ngIf="!validatedEmail">
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
||||
<input bitInput type="email" formControlName="email" appAutofocus />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3 tw-flex tw-items-start">
|
||||
<bit-form-control class="tw-mb-0">
|
||||
<input type="checkbox" bitCheckbox formControlName="rememberEmail" />
|
||||
<bit-label>{{ "rememberEmail" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<button
|
||||
bitButton
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
class="tw-w-full"
|
||||
(click)="validateEmail()"
|
||||
>
|
||||
<span> {{ "continue" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3 tw-flex tw-flex-col tw-items-center tw-justify-center">
|
||||
<p class="tw-mb-3">{{ "or" | i18n }}</p>
|
||||
|
||||
<a
|
||||
bitLink
|
||||
block
|
||||
linkType="primary"
|
||||
routerLink="/login-with-passkey"
|
||||
(mousedown)="$event.preventDefault()"
|
||||
>
|
||||
<span><i class="bwi bwi-passkey"></i> {{ "logInWithPasskey" | i18n }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<p class="tw-m-0 tw-text-sm">
|
||||
{{ "newAroundHere" | i18n }}
|
||||
<!-- Two notes:
|
||||
(1) We check the value and validity of email so we don't send an invalid email to autofill
|
||||
on load of register for both enter and mouse based navigation
|
||||
(2) We use mousedown to trigger navigation so that the onBlur form validation does not fire
|
||||
and move the create account link down the page on click which causes the user to miss actually
|
||||
clicking on the link. Mousedown fires before onBlur.
|
||||
-->
|
||||
<a
|
||||
bitLink
|
||||
routerLink="/signup"
|
||||
[queryParams]="emailFormControl.valid ? { email: emailFormControl.value } : {}"
|
||||
(mousedown)="goToRegister()"
|
||||
>
|
||||
{{ "createAccount" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
</ng-container>
|
||||
|
||||
<div [ngClass]="{ 'tw-hidden': !validatedEmail }">
|
||||
<div class="tw-mb-6 tw-h-28">
|
||||
<bit-form-field class="!tw-mb-1">
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input type="password" bitInput #masterPasswordInput formControlName="masterPassword" />
|
||||
<button type="button" bitSuffix bitIconButton bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
<a
|
||||
bitLink
|
||||
class="tw-mt-2"
|
||||
routerLink="/hint"
|
||||
(mousedown)="goToHint()"
|
||||
(click)="saveEmailSettings()"
|
||||
>{{ "getMasterPasswordHint" | i18n }}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div [hidden]="!showCaptcha()">
|
||||
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3 tw-flex tw-space-x-4">
|
||||
<button bitButton buttonType="primary" bitFormButton type="submit" [block]="true">
|
||||
<span> {{ "loginWithMasterPassword" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3" *ngIf="showLoginWithDevice">
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
[block]="true"
|
||||
buttonType="secondary"
|
||||
(click)="startAuthRequestLogin()"
|
||||
>
|
||||
<span> <i class="bwi bwi-mobile"></i> {{ "loginWithDevice" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-3">
|
||||
<a
|
||||
routerLink="/sso"
|
||||
[queryParams]="{ email: formGroup.value.email }"
|
||||
(click)="saveEmailSettings()"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
class="tw-w-full"
|
||||
>
|
||||
<i class="bwi bwi-provider tw-mr-2"></i>
|
||||
{{ "enterpriseSingleSignOn" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="tw-m-0 tw-text-sm">
|
||||
<p class="tw-mb-1">{{ "loggingInAs" | i18n }} {{ loggedEmail }}</p>
|
||||
<a bitLink [routerLink]="[]" (click)="toggleValidateEmail(false)">{{ "notYou" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,224 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, NgZone, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { takeUntil } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
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 { 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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { RouterService } from "../../core";
|
||||
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
|
||||
import { OrganizationInvite } from "../organization-invite/organization-invite";
|
||||
|
||||
@Component({
|
||||
selector: "app-login",
|
||||
templateUrl: "login-v1.component.html",
|
||||
})
|
||||
export class LoginComponentV1 extends BaseLoginComponent implements OnInit {
|
||||
showResetPasswordAutoEnrollWarning = false;
|
||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
policies: Policy[];
|
||||
|
||||
constructor(
|
||||
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||
devicesApiService: DevicesApiServiceAbstraction,
|
||||
appIdService: AppIdService,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyService: InternalPolicyService,
|
||||
logService: LogService,
|
||||
ngZone: NgZone,
|
||||
protected stateService: StateService,
|
||||
private routerService: RouterService,
|
||||
formBuilder: FormBuilder,
|
||||
formValidationErrorService: FormValidationErrorsService,
|
||||
loginEmailService: LoginEmailServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
devicesApiService,
|
||||
appIdService,
|
||||
loginStrategyService,
|
||||
router,
|
||||
platformUtilsService,
|
||||
i18nService,
|
||||
stateService,
|
||||
environmentService,
|
||||
passwordGenerationService,
|
||||
cryptoFunctionService,
|
||||
logService,
|
||||
ngZone,
|
||||
formBuilder,
|
||||
formValidationErrorService,
|
||||
route,
|
||||
loginEmailService,
|
||||
ssoLoginService,
|
||||
toastService,
|
||||
);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
submitForm = async (showToast = true) => {
|
||||
return await this.submitFormHelper(showToast);
|
||||
};
|
||||
|
||||
private async submitFormHelper(showToast: boolean) {
|
||||
await super.submit(showToast);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
// If there is a query parameter called 'org', set previousUrl to `/create-organization?org=paramValue`
|
||||
if (qParams.org != null) {
|
||||
const route = this.router.createUrlTree(["create-organization"], {
|
||||
queryParams: { plan: qParams.org },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a query parameter called 'sponsorshipToken', that means they are coming
|
||||
* from an email for sponsoring a families organization. If so, then set the prevousUrl
|
||||
* to `/setup/families-for-enterprise?token=paramValue`
|
||||
*/
|
||||
if (qParams.sponsorshipToken != null) {
|
||||
const route = this.router.createUrlTree(["setup/families-for-enterprise"], {
|
||||
queryParams: { token: qParams.sponsorshipToken },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
}
|
||||
|
||||
await super.ngOnInit();
|
||||
});
|
||||
|
||||
// If there's an existing org invite, use it to get the password policies
|
||||
const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite();
|
||||
if (orgInvite != null) {
|
||||
await this.initPasswordPolicies(orgInvite);
|
||||
}
|
||||
}
|
||||
|
||||
async goAfterLogIn(userId: UserId) {
|
||||
const masterPassword = this.formGroup.value.masterPassword;
|
||||
|
||||
// Check master password against policy
|
||||
if (this.enforcedPasswordPolicyOptions != null) {
|
||||
const strengthResult = this.passwordStrengthService.getPasswordStrength(
|
||||
masterPassword,
|
||||
this.formGroup.value.email,
|
||||
);
|
||||
const masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||
|
||||
// If invalid, save policies and require update
|
||||
if (
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
masterPasswordScore,
|
||||
masterPassword,
|
||||
this.enforcedPasswordPolicyOptions,
|
||||
)
|
||||
) {
|
||||
const policiesData: { [id: string]: PolicyData } = {};
|
||||
this.policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p)));
|
||||
await this.policyService.replace(policiesData, userId);
|
||||
await this.router.navigate(["update-password"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.loginEmailService.clearValues();
|
||||
await this.router.navigate([this.successRoute]);
|
||||
}
|
||||
|
||||
async goToHint() {
|
||||
await this.saveEmailSettings();
|
||||
await this.router.navigateByUrl("/hint");
|
||||
}
|
||||
|
||||
async goToRegister() {
|
||||
if (this.emailFormControl.valid) {
|
||||
await this.router.navigate(["/signup"], {
|
||||
queryParams: { email: this.emailFormControl.value },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate(["/signup"]);
|
||||
}
|
||||
|
||||
protected override async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
await this.router.navigate(["migrate-legacy-encryption"]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async initPasswordPolicies(invite: OrganizationInvite): Promise<void> {
|
||||
try {
|
||||
this.policies = await this.policyApiService.getPoliciesByToken(
|
||||
invite.organizationId,
|
||||
invite.token,
|
||||
invite.email,
|
||||
invite.organizationUserId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
if (this.policies == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions(
|
||||
this.policies,
|
||||
invite.organizationId,
|
||||
);
|
||||
|
||||
// Set to true if policy enabled and auto-enroll enabled
|
||||
this.showResetPasswordAutoEnrollWarning =
|
||||
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
||||
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(this.policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enforcedPasswordPolicyOptions) => {
|
||||
this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<div
|
||||
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
|
||||
>
|
||||
<div>
|
||||
<img class="logo logo-themed" alt="Bitwarden" />
|
||||
|
||||
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
|
||||
<p class="tw-mx-4 tw-mb-4 tw-mt-3 tw-text-center tw-text-xl">
|
||||
{{ "loginOrCreateNewAccount" | i18n }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
||||
>
|
||||
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "logInRequestSent" | i18n }}</h2>
|
||||
|
||||
<p class="tw-mb-6">
|
||||
{{ "notificationSentDeviceComplete" | i18n }}
|
||||
</p>
|
||||
|
||||
<div class="tw-mb-6">
|
||||
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
|
||||
<p>
|
||||
<code>{{ fingerprintPhrase }}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tw-my-10" *ngIf="showResendNotification">
|
||||
<a [routerLink]="[]" disabled="true" (click)="startAuthRequestLogin()">{{
|
||||
"resendNotification" | i18n
|
||||
}}</a>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="tw-mt-3">
|
||||
{{ "loginWithDeviceEnabledNote" | i18n }}
|
||||
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="state == StateEnum.AdminAuthRequest">
|
||||
<div
|
||||
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
|
||||
>
|
||||
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "adminApprovalRequested" | i18n }}</h2>
|
||||
|
||||
<div>
|
||||
<p class="tw-mb-6">{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
|
||||
<p class="tw-mb-6">{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
|
||||
</div>
|
||||
|
||||
<div class="tw-mb-6">
|
||||
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
|
||||
<p>
|
||||
<code>{{ fingerprintPhrase }}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="tw-mt-3">
|
||||
{{ "troubleLoggingIn" | i18n }}
|
||||
<a routerLink="/login-initiated">{{ "viewAllLoginOptions" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { LoginViaAuthRequestComponentV1 as BaseLoginViaAuthRequestComponentV1 } from "@bitwarden/angular/auth/components/login-via-auth-request-v1.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-login-via-auth-request",
|
||||
templateUrl: "login-via-auth-request-v1.component.html",
|
||||
})
|
||||
export class LoginViaAuthRequestComponentV1 extends BaseLoginViaAuthRequestComponentV1 {}
|
||||
@@ -5,23 +5,11 @@ import { CheckboxModule } from "@bitwarden/components";
|
||||
import { SharedModule } from "../../../app/shared";
|
||||
|
||||
import { LoginDecryptionOptionsComponentV1 } from "./login-decryption-options/login-decryption-options-v1.component";
|
||||
import { LoginComponentV1 } from "./login-v1.component";
|
||||
import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component";
|
||||
import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, CheckboxModule],
|
||||
declarations: [
|
||||
LoginComponentV1,
|
||||
LoginViaAuthRequestComponentV1,
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
LoginViaWebAuthnComponent,
|
||||
],
|
||||
exports: [
|
||||
LoginComponentV1,
|
||||
LoginViaAuthRequestComponentV1,
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
LoginViaWebAuthnComponent,
|
||||
],
|
||||
declarations: [LoginDecryptionOptionsComponentV1, LoginViaWebAuthnComponent],
|
||||
exports: [LoginDecryptionOptionsComponentV1, LoginViaWebAuthnComponent],
|
||||
})
|
||||
export class LoginModule {}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Router } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
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 { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
@@ -50,7 +50,7 @@ export class ChangePasswordComponent
|
||||
private auditService: AuditService,
|
||||
private cipherService: CipherService,
|
||||
private syncService: SyncService,
|
||||
private apiService: ApiService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private router: Router,
|
||||
dialogService: DialogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
@@ -213,14 +213,14 @@ export class ChangePasswordComponent
|
||||
|
||||
try {
|
||||
if (this.rotateUserKey) {
|
||||
this.formPromise = this.apiService.postPassword(request).then(async () => {
|
||||
this.formPromise = this.masterPasswordApiService.postPassword(request).then(async () => {
|
||||
// we need to save this for local masterkey verification during rotation
|
||||
await this.masterPasswordService.setMasterKeyHash(newLocalKeyHash, userId as UserId);
|
||||
await this.masterPasswordService.setMasterKey(newMasterKey, userId as UserId);
|
||||
return this.updateKey();
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.apiService.postPassword(request);
|
||||
this.formPromise = this.masterPasswordApiService.postPassword(request);
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
|
||||
@@ -9,7 +9,13 @@ import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import { CipherViewComponent, DefaultTaskService, TaskService } from "@bitwarden/vault";
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
CipherViewComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultTaskService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service";
|
||||
|
||||
@@ -34,6 +40,7 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService {
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
|
||||
],
|
||||
})
|
||||
export class EmergencyViewDialogComponent {
|
||||
|
||||
@@ -180,8 +180,20 @@ export class DeviceManagementComponent {
|
||||
private updateDeviceTable(devices: Array<DeviceView>): void {
|
||||
this.dataSource.data = devices
|
||||
.map((device: DeviceView): DeviceTableData | null => {
|
||||
if (!device.id || !device.type || !device.creationDate) {
|
||||
this.validationService.showError(new Error("Invalid device data"));
|
||||
if (device.id == undefined) {
|
||||
this.validationService.showError(new Error(this.i18nService.t("deviceIdMissing")));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (device.type == undefined) {
|
||||
this.validationService.showError(new Error(this.i18nService.t("deviceTypeMissing")));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (device.creationDate == undefined) {
|
||||
this.validationService.showError(
|
||||
new Error(this.i18nService.t("deviceCreationDateMissing")),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { BillingNotificationService } from "../services/billing-notification.service";
|
||||
import { BillingSharedModule } from "../shared/billing-shared.module";
|
||||
import { PaymentComponent } from "../shared/payment/payment.component";
|
||||
|
||||
@@ -208,6 +209,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
private taxService: TaxServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
private organizationBillingService: OrganizationBillingService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -228,10 +230,14 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
const { accountCredit, paymentSource } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
try {
|
||||
const { accountCredit, paymentSource } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
} catch (error) {
|
||||
this.billingNotificationService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.selfHosted) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { BillingNotificationService } from "../../services/billing-notification.service";
|
||||
import { TrialFlowService } from "../../services/trial-flow.service";
|
||||
import {
|
||||
AddCreditDialogResult,
|
||||
@@ -66,6 +67,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
protected syncService: SyncService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
) {
|
||||
this.activatedRoute.params
|
||||
.pipe(
|
||||
@@ -115,47 +117,52 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
|
||||
protected load = async (): Promise<void> => {
|
||||
this.loading = true;
|
||||
const { accountCredit, paymentSource, subscriptionStatus } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
this.subscriptionStatus = subscriptionStatus;
|
||||
try {
|
||||
const { accountCredit, paymentSource, subscriptionStatus } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
this.subscriptionStatus = subscriptionStatus;
|
||||
|
||||
if (this.organizationId) {
|
||||
const organizationSubscriptionPromise = this.organizationApiService.getSubscription(
|
||||
this.organizationId,
|
||||
);
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const organizationPromise = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
if (this.organizationId) {
|
||||
const organizationSubscriptionPromise = this.organizationApiService.getSubscription(
|
||||
this.organizationId,
|
||||
);
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const organizationPromise = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
|
||||
[this.organizationSubscriptionResponse, this.organization] = await Promise.all([
|
||||
organizationSubscriptionPromise,
|
||||
organizationPromise,
|
||||
]);
|
||||
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||
this.organization,
|
||||
this.organizationSubscriptionResponse,
|
||||
paymentSource,
|
||||
);
|
||||
[this.organizationSubscriptionResponse, this.organization] = await Promise.all([
|
||||
organizationSubscriptionPromise,
|
||||
organizationPromise,
|
||||
]);
|
||||
this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues(
|
||||
this.organization,
|
||||
this.organizationSubscriptionResponse,
|
||||
paymentSource,
|
||||
);
|
||||
}
|
||||
this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false;
|
||||
// If the flag `launchPaymentModalAutomatically` is set to true,
|
||||
// we schedule a timeout (delay of 800ms) to automatically launch the payment modal.
|
||||
// This delay ensures that any prior UI/rendering operations complete before triggering the modal.
|
||||
if (this.launchPaymentModalAutomatically) {
|
||||
window.setTimeout(async () => {
|
||||
await this.changePayment();
|
||||
this.launchPaymentModalAutomatically = false;
|
||||
this.location.replaceState(this.location.path(), "", {});
|
||||
}, 800);
|
||||
}
|
||||
} catch (error) {
|
||||
this.billingNotificationService.handleError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false;
|
||||
// If the flag `launchPaymentModalAutomatically` is set to true,
|
||||
// we schedule a timeout (delay of 800ms) to automatically launch the payment modal.
|
||||
// This delay ensures that any prior UI/rendering operations complete before triggering the modal.
|
||||
if (this.launchPaymentModalAutomatically) {
|
||||
window.setTimeout(async () => {
|
||||
await this.changePayment();
|
||||
this.launchPaymentModalAutomatically = false;
|
||||
this.location.replaceState(this.location.path(), "", {});
|
||||
}, 800);
|
||||
}
|
||||
this.loading = false;
|
||||
};
|
||||
|
||||
protected updatePaymentMethod = async (): Promise<void> => {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { BillingNotificationService } from "./billing-notification.service";
|
||||
|
||||
describe("BillingNotificationService", () => {
|
||||
let service: BillingNotificationService;
|
||||
let logService: MockProxy<LogService>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
|
||||
beforeEach(() => {
|
||||
logService = mock<LogService>();
|
||||
toastService = mock<ToastService>();
|
||||
service = new BillingNotificationService(logService, toastService);
|
||||
});
|
||||
|
||||
describe("handleError", () => {
|
||||
it("should log error and show toast for ErrorResponse", () => {
|
||||
const error = new ErrorResponse(["test error"], 400);
|
||||
|
||||
expect(() => service.handleError(error)).toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(error);
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: error.getSingleMessage(),
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error toast with the provided error", () => {
|
||||
const error = new ErrorResponse(["test error"], 400);
|
||||
|
||||
expect(() => service.handleError(error, "Test Title")).toThrow();
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "error",
|
||||
title: "Test Title",
|
||||
message: error.getSingleMessage(),
|
||||
});
|
||||
});
|
||||
|
||||
it("should only log error for non-ErrorResponse", () => {
|
||||
const error = new Error("test error");
|
||||
|
||||
expect(() => service.handleError(error)).toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(error);
|
||||
expect(toastService.showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("showSuccess", () => {
|
||||
it("shows success toast with default title when provided title is empty", () => {
|
||||
const message = "test message";
|
||||
service.showSuccess(message);
|
||||
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message,
|
||||
});
|
||||
});
|
||||
|
||||
it("should show success toast with custom title", () => {
|
||||
const message = "test message";
|
||||
service.showSuccess(message, "Success Title");
|
||||
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
title: "Success Title",
|
||||
message,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class BillingNotificationService {
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
handleError(error: unknown, title: string = "") {
|
||||
this.logService.error(error);
|
||||
if (error instanceof ErrorResponse) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: title,
|
||||
message: error.getSingleMessage(),
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
showSuccess(message: string, title: string = "") {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: title,
|
||||
message: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
@NgModule({})
|
||||
import { BillingNotificationService } from "./billing-notification.service";
|
||||
|
||||
@NgModule({
|
||||
providers: [BillingNotificationService],
|
||||
})
|
||||
export class BillingServicesModule {}
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.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 { ClientType } from "@bitwarden/common/enums";
|
||||
@@ -96,6 +97,7 @@ import {
|
||||
DefaultThemeStateService,
|
||||
ThemeStateService,
|
||||
} from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
KdfConfigService,
|
||||
@@ -103,6 +105,7 @@ import {
|
||||
BiometricsService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { LockComponentService } from "@bitwarden/key-management-ui";
|
||||
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
import { flagEnabled } from "../../utils/flags";
|
||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||
@@ -277,6 +280,7 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebSetPasswordJitService,
|
||||
deps: [
|
||||
ApiService,
|
||||
MasterPasswordApiService,
|
||||
KeyServiceAbstraction,
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
@@ -349,6 +353,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebLoginDecryptionOptionsService,
|
||||
deps: [MessagingService, RouterService, AcceptOrganizationInviteService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SshImportPromptService,
|
||||
useClass: DefaultSshImportPromptService,
|
||||
deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -75,7 +76,7 @@ export class MigrateFromLegacyEncryptionComponent {
|
||||
} catch (e) {
|
||||
// If the error is due to missing folders, we can delete all folders and try again
|
||||
if (
|
||||
e instanceof Error &&
|
||||
e instanceof ErrorResponse &&
|
||||
e.message === "All existing folders must be included in the rotation."
|
||||
) {
|
||||
const deleteFolders = await this.dialogService.openSimpleDialog({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user