1
0
mirror of https://github.com/bitwarden/browser synced 2026-03-01 19:11:22 +00:00

Merge branch 'main' into km/pm-18576/fix-missing-userid-on-remove-password

This commit is contained in:
Maciej Zieniuk
2025-03-13 12:25:25 +00:00
221 changed files with 3745 additions and 5134 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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()],

View File

@@ -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) => {

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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[] = [

View File

@@ -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({

View File

@@ -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(

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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 {}

View File

@@ -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,

View File

@@ -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();
});

View File

@@ -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();
}

View File

@@ -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": {

View File

@@ -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();

View File

@@ -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();
});
}
}

View File

@@ -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;

View 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();
// }
}

View File

@@ -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");
},

View File

@@ -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.

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -82,7 +82,6 @@
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.SshKey }"
*ngIf="isSshKeysEnabled"
>
<span class="filter-buttons">
<button

View File

@@ -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);
}
}