mirror of
https://github.com/bitwarden/browser
synced 2026-02-13 06:54:07 +00:00
* Turn on passkeys and dev mode
* PM-19138: Add try-catch to desktop-autofill (#13964)
* PM-19424: React to IPC disconnect (#14123)
* React to IPC disconnects
* Minor cleanup
* Update apps/desktop/package.json
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* Relaxed ordering
---------
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* Autofill/pm 9034 implement passkey for unlocked accounts (#13826)
* Passkey stuff
Co-authored-by: Anders Åberg <github@andersaberg.com>
* Ugly hacks
* Work On Modal State Management
* Applying modalStyles
* modal
* Improved hide/show
* fixed promise
* File name
* fix prettier
* Protecting against null API's and undefined data
* Only show fake popup to devs
* cleanup mock code
* rename minmimal-app to modal-app
* Added comment
* Added comment
* removed old comment
* Avoided changing minimum size
* Add small comment
* Rename component
* adress feedback
* Fixed uppercase file
* Fixed build
* Added codeowners
* added void
* commentary
* feat: reset setting on app start
* Moved reset to be in main / process launch
* Add comment to create window
* Added a little bit of styling
* Use Messaging service to loadUrl
* Enable passkeysautofill
* Add logging
* halfbaked
* Integration working
* And now it works without extra delay
* Clean up
* add note about messaging
* lb
* removed console.logs
* Cleanup and adress review feedback
* This hides the swift UI
* add modal components
* update modal with correct ciphers and functionality
* add create screen
* pick credential, draft
* Remove logger
* a whole lot of wiring
* not working
* Improved wiring
* Cancel after 90s
* Introduced observable
* update cipher handling
* update to use matchesUri
* Launching bitwarden if its not running
* Passing position from native to electron
* Rename inModalMode to modalMode
* remove tap
* revert spaces
* added back isDev
* cleaned up a bit
* Cleanup swift file
* tweaked logging
* clean up
* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Update apps/desktop/src/platform/services/desktop-settings.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* adress position feedback
* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Removed extra logging
* Adjusted error logging
* Use .error to log errors
* remove dead code
* Update desktop-autofill.service.ts
* use parseCredentialId instead of guidToRawFormat
* Update apps/desktop/src/autofill/services/desktop-autofill.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Change windowXy to a Record instead of [number,number]
* Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Remove unsued dep and comment
* changed timeout to be spec recommended maxium, 10 minutes, for now.
* Correctly assume UP
* Removed extra cancelRequest in deinint
* Add timeout and UV to confirmChoseCipher
UV is performed by UI, not the service
* Improved docs regarding undefined cipherId
* cleanup: UP is no longer undefined
* Run completeError if ipc messages conversion failed
* don't throw, instead return undefined
* Disabled passkey provider
* Throw error if no activeUserId was found
* removed comment
* Fixed lint
* removed unsued service
* reset entitlement formatting
* Update entitlements.mas.plist
* Fix build issues
* Fix import issues
* Update route names to use `fido2`
* Fix being unable to select a passkey
* Fix linting issues
* Followup to fix merge issues and other comments
* Update `userHandle` value
* Add error handling for missing session or other errors
* Remove unused route
* Fix linting issues
* Simplify updateCredential method
* Followup to remove comments and timeouts and handle errors
* Address lint issue by using `takeUntilDestroyed`
* PR Followup for typescript and vault concerns
* Add try block for cipher creation
* Make userId manditory for cipher service
---------
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: Anders Åberg <github@andersaberg.com>
Co-authored-by: Anders Åberg <anders@andersaberg.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Evan Bassler <evanbassler@Mac.attlocal.net>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* PM-11455: Trigger sync when user enables OS setting (#14127)
* Implemented a SendNativeStatus command
This allows reporting status or asking the electron app to do something.
* fmt
* Update apps/desktop/src/autofill/services/desktop-autofill.service.ts
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* clean up
* Don't add empty callbacks
* Removed comment
---------
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* Added support for handling a locked vault
Handle unlocktimeout
* PM-19511: Add support for ExcludedCredentials (#14128)
* works
* Add mapping
* remove the build script
* cleanup
* simplify updatedCipher (#14179)
* Fix base64url decode on MacOS passkeys (#14227)
* Add support for padding in base64url decode
* whitespace
* whitespace
* Autofill/pm 17444 use reprompt (#14004)
* Passkey stuff
Co-authored-by: Anders Åberg <github@andersaberg.com>
* Ugly hacks
* Work On Modal State Management
* Applying modalStyles
* modal
* Improved hide/show
* fixed promise
* File name
* fix prettier
* Protecting against null API's and undefined data
* Only show fake popup to devs
* cleanup mock code
* rename minmimal-app to modal-app
* Added comment
* Added comment
* removed old comment
* Avoided changing minimum size
* Add small comment
* Rename component
* adress feedback
* Fixed uppercase file
* Fixed build
* Added codeowners
* added void
* commentary
* feat: reset setting on app start
* Moved reset to be in main / process launch
* Add comment to create window
* Added a little bit of styling
* Use Messaging service to loadUrl
* Enable passkeysautofill
* Add logging
* halfbaked
* Integration working
* And now it works without extra delay
* Clean up
* add note about messaging
* lb
* removed console.logs
* Cleanup and adress review feedback
* This hides the swift UI
* add modal components
* update modal with correct ciphers and functionality
* add create screen
* pick credential, draft
* Remove logger
* a whole lot of wiring
* not working
* Improved wiring
* Cancel after 90s
* Introduced observable
* update cipher handling
* update to use matchesUri
* Launching bitwarden if its not running
* Passing position from native to electron
* Rename inModalMode to modalMode
* remove tap
* revert spaces
* added back isDev
* cleaned up a bit
* Cleanup swift file
* tweaked logging
* clean up
* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Update apps/desktop/src/platform/services/desktop-settings.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* adress position feedback
* Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Removed extra logging
* Adjusted error logging
* Use .error to log errors
* remove dead code
* Update desktop-autofill.service.ts
* use parseCredentialId instead of guidToRawFormat
* Update apps/desktop/src/autofill/services/desktop-autofill.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Change windowXy to a Record instead of [number,number]
* Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Remove unsued dep and comment
* changed timeout to be spec recommended maxium, 10 minutes, for now.
* Correctly assume UP
* Removed extra cancelRequest in deinint
* Add timeout and UV to confirmChoseCipher
UV is performed by UI, not the service
* Improved docs regarding undefined cipherId
* cleanup: UP is no longer undefined
* Run completeError if ipc messages conversion failed
* don't throw, instead return undefined
* Disabled passkey provider
* Throw error if no activeUserId was found
* removed comment
* Fixed lint
* removed unsued service
* reset entitlement formatting
* Update entitlements.mas.plist
* Fix build issues
* Fix import issues
* Update route names to use `fido2`
* Fix being unable to select a passkey
* Fix linting issues
* Added support for handling a locked vault
* Followup to fix merge issues and other comments
* Update `userHandle` value
* Add error handling for missing session or other errors
* Remove unused route
* Fix linting issues
* Simplify updateCredential method
* Add master password reprompt on passkey create
* Followup to remove comments and timeouts and handle errors
* Address lint issue by using `takeUntilDestroyed`
* Add MP prompt to cipher selection
* Change how timeout is handled
* Include `of` from rxjs
* Hide blue header for passkey popouts (#14095)
* Hide blue header for passkey popouts
* Fix issue with test
* Fix ngOnDestroy complaint
* Import OnDestroy correctly
* Only require master password if item requires it
---------
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: Anders Åberg <github@andersaberg.com>
Co-authored-by: Anders Åberg <anders@andersaberg.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Evan Bassler <evanbassler@Mac.attlocal.net>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
* Change modal size to 600x600
* Improve MacOS Syncing
This changes the behaviour to react to logoff, but not to account locks. It also adds better error handling on the native side.
* Improved modalPosition by allowing multiple calls to applyModalStyles
* moved imports to please lint
* Make passkey header stick for select and create (#14357)
* Added local build command
* Exclude credentials using kvc to avoid comilation error in cicd (#14568)
* Fix syntax error
* Don't use kvc
* Enables the autofill extension in mac and mas builds (#14373)
* Enables autofill extension building
* Try use macos-14
* add --break-system-packages for macos14
* revert using build-native
* try add rustup target add x86_64-apple-darwin
* add more rustup target add x86_64-apple-darwin
* try to force sdk version
* Show SDK versions
* USE KVC for excludedCredentials
* added xcodebuild deugging
* Revert "try to force sdk version"
This reverts commit d94f2550ad.
* Use macos-15
* undo merge
* remove macos-15 from cli
* remove macos-15 from browser
---------
Co-authored-by: Anders Åberg <anders@andersaberg.com>
* Improve Autofill IPC reliability (#14358)
* Delay IPC server start
* Better ipc handling
* Rename ready() to listenerReady()
---------
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
* feat: add test and check for too long buffers (#14775)
* Autofill/PM-19511: Overwrite and reprompt (#14288)
* Show items for url that don't have passkey
* Show existing login items in the UI
* Filter available cipher results (#14399)
* Filter available cipher results
* Fix linting issues
* Update logic for eligible ciphers
* Remove unused method to check matching username
* PM-20608 update styling for excludedCredentials (#14444)
* PM-20608 update styling for excludedCredentials
* Have flow correctly move to creation for excluded cipher
* Remove duplicate confirmNeCredential call
* Revert fido2-authenticator changes and move the excluded check
* Create a separate component for excluded cipher view
* Display traffic light MacOS buttons when the vault is locked (#14673)
* Remove unneccessary filter for excludedCiphers
* Remove dead code from the excluded ciphers work
* Remove excludedCipher checks from fido2 create and vault
* Remove excludedCipher remnants from vault and simplify create cipher logic
* Move cipherHasNoOtherPasskeys to shared fido2-utils
* Remove all containsExcludedCipher references
* Use `bufferToString` to convert `userHandle`
---------
Co-authored-by: Jeffrey Holland <jholland@livefront.com>
Co-authored-by: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com>
* Move modal files to `autofill` and rename dir to `credentials` (#14757)
* Show existing login items in the UI
* Filter available cipher results (#14399)
* Filter available cipher results
* Fix linting issues
* Update logic for eligible ciphers
* Remove unused method to check matching username
* PM-20608 update styling for excludedCredentials (#14444)
* PM-20608 update styling for excludedCredentials
* Have flow correctly move to creation for excluded cipher
* Remove duplicate confirmNeCredential call
* Revert fido2-authenticator changes and move the excluded check
* Create a separate component for excluded cipher view
* Display traffic light MacOS buttons when the vault is locked (#14673)
* Remove unneccessary filter for excludedCiphers
* Remove dead code from the excluded ciphers work
* Remove excludedCipher checks from fido2 create and vault
* Move modal files to `autofill` and rename dir to `credentials`
* Update merge issues
* Add tests for `cipherHasNoOtherPasskeys` (#14829)
* Adjust spacing to place new login button below other items (#14877)
* Adjust spacing to place new login button below other items
* Add correct design when no credentials available (#14879)
* Autofill/pm 21903 use translations everywhere for passkeys (#14908)
* Adjust spacing to place new login button below other items
* Add correct design when no credentials available
* Add correct design when no credentials available (#14879)
* Remove hardcoded strings and use translations in passkey flow
* Remove duplicate `select` translation
* Autofill/pm 21864 center unlock vault modal (#14867)
* Center the Locked Vault modal when using passkeys
* Revert swift changes and handle offscreen modals
* Remove comments
* Add rustup for cicd to work (#15055)
* Hide credentials that are in the bin (#15034)
* Add tests for passkey components (#15185)
* Add tests for passkey components
* Reuse cipher in chooseCipher tests and simplify mock creation
* Autofill/pm 22821 center vault modal (#15243)
* Center the vault modal for passkeys
* Add comments and fix electron-builder.json
* Set values to Int32 in the ternaries
* Refactor Fido2 Components (#15105)
* Refactor Fido2 Components
* Address error message and missing session
* Address remaining missing session
* Reset modals so subsequent creates work (#15145)
* Fix broken test
* Rename relevantCiphers to displayedCiphers
* Clean up heading settings, errors, and other concerns
* Address missing comments and throw error in try block
* fix type issue for SimpleDialogType
* fix type issue for SimpleDialogType
* Revert new type
* try using as null to satisfy type issue
* Remove use of firstValueFrom in create component
* PM-22476: Show config UI while enabling Bitwarden (#15149)
* Show config ui while enabling Bitwarden
* locals
* Added Localizable strings
* Changed the linebreakmode
* Removed swedish locals
* Add provisioning profile values to electron build (#15412)
* Address BitwardenShield icon issue
* Fix fido2-vault component
* Display the vault modal when selecting Bitwarden... (#15257)
* Passkeys filtering breaks on SSH keys (#15448)
* Display the blue header on the locked vault passkey flow (#15655)
* PM-23848: Use the MacOS UI-friendly API instead (#15650)
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Fix action text and close vault modal (#15634)
* Fix action text and close vault modal
* Fix broken tests
* Update SVG to support dark mode (#15805)
* When a locked vault is unlocked displays correctly (#15612)
* When a locked vault is unlocked displays correctly
* Keep old behavior while checking for recently unlocked vault
* Revert the electron-builder
* Simplify by using a simple redirect when vault unlocked
* Remove single use of `userSelectedCipher`
* Add a guard clause to unlock
* Revert to original spacing
* Add reactive guard to unlock vault
* Fix for passkey picker closing prematurely
* Remove unneeded root navigation in ensureUnlockedVault
* Fix vault not unlocking
* Update broken tests for lock component
* Add missing brace to preload.ts
* Run lint
* Added explainer
* Moved the explainer
* Tidying up readme
* Add feature flag to short-circuit the passkey provider (#16003)
* Add feature flag to short-circuit the passkey provider
* Check FF in renderer instead
* Lint fixes
* PM-22175: Improve launch of app + window positioning (#15658)
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Implement prepareInterfaceToProvideCredential
* Fix launch of app + window pos
* Wait for animation to complete and use proper position
* Wait for animation to complete and use proper position
* Added commentary
* Remove console.log
* Remove call to removed function
---------
Co-authored-by: Jeffrey Holland <jholland@livefront.com>
Co-authored-by: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com>
* Update fido2-vault and fido2-service implementations
* Use tailwind-alike classes for new styles
* Add label to biticons in passkey modals
* Fix broken vault test
* Revert to original `isDev` function
* Add comment to lock component describing `disable-redirect` param
* Use tailwind classes instead of custom sticky header class
* Use standard `tw-z-10` for z-index
* Change log service levels
* Mock svg icons for CI
* Add back provisioning profiles
* Remove `--break-system-packages` and simplify commands
* Revert `cipherId` param for `confirmNewCredential`
* Remove placeholder UI
* Small improvements to the readme
* Remove optional userId and deprecated method
* Autofill should own the macos_provider (#16271)
* Autofill should own the macos_provider
* Autofill should own the macos_provider
* Remove unnecessary logs, no magic numbers, revert `cipherId?`
* Fixes for broken build
* Update test issues
* [BEEEP] Use tracing in macOS provider
* Update comments and add null check for ciphers
* Update status comments and readme
* Remove electron modal mode link
* Clarify modal mode use
* Add comment about usernames
* Add comment that we don't support extensions yet
* Added comment about base64 format
* Use NO_CALLBACK_INDICATOR
* cb -> callback
* Update apps/desktop/desktop_native/napi/src/lib.rs
Co-authored-by: neuronull <9162534+neuronull@users.noreply.github.com>
* Clean up Fido2Create subscriptions and update comments
* added comment to clarify silent exception
* Add comments
* clean up unwrap()
* set log level filter to INFO
* Address modal popup issue
* plutil on Info.plist
* Adhere to style guides
* Fix broken lock ui component tests
* Fix broken lock ui component tests
* Added codeowners entry
* logservice.warning -> debug
* Uint8Array -> ArrayBuffer
* Remove autofill entitlement
* Fix linting issues
* Fix arm build issue
* Adjust build command
* Add missing entitlement
* revert missing entitlement change
* Add proper autofill entitlements
* Remove autofill extension from mas builds
* Run rust formatter
---------
Co-authored-by: Daniel García <dani-garcia@users.noreply.github.com>
Co-authored-by: Jeffrey Holland <124393578+jholland-livefront@users.noreply.github.com>
Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by: Colton Hurst <colton@coltonhurst.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Evan Bassler <evanbassler@Mac.attlocal.net>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Nathan Ansel <nathan@livefront.com>
Co-authored-by: Jeffrey Holland <jholland@livefront.com>
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: neuronull <9162534+neuronull@users.noreply.github.com>
889 lines
36 KiB
TypeScript
889 lines
36 KiB
TypeScript
// FIXME: Update this file to be type safe and remove this and next line
|
|
// @ts-strict-ignore
|
|
import {
|
|
Component,
|
|
DestroyRef,
|
|
NgZone,
|
|
OnDestroy,
|
|
OnInit,
|
|
Type,
|
|
ViewChild,
|
|
ViewContainerRef,
|
|
} from "@angular/core";
|
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
import { Router } from "@angular/router";
|
|
import {
|
|
filter,
|
|
firstValueFrom,
|
|
lastValueFrom,
|
|
map,
|
|
Subject,
|
|
switchMap,
|
|
takeUntil,
|
|
timeout,
|
|
} from "rxjs";
|
|
|
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
|
import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval";
|
|
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
|
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
|
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
|
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
|
|
import {
|
|
DESKTOP_SSO_CALLBACK,
|
|
LockService,
|
|
LogoutReason,
|
|
UserDecryptionOptionsServiceAbstraction,
|
|
} from "@bitwarden/auth/common";
|
|
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
|
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
|
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
|
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
|
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
|
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
|
import {
|
|
VaultTimeout,
|
|
VaultTimeoutAction,
|
|
VaultTimeoutService,
|
|
VaultTimeoutSettingsService,
|
|
VaultTimeoutStringType,
|
|
} from "@bitwarden/common/key-management/vault-timeout";
|
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
|
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 { 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 { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
|
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
|
|
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
|
|
import { SyncService } from "@bitwarden/common/platform/sync";
|
|
import { UserId } from "@bitwarden/common/types/guid";
|
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
|
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
|
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
|
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
|
import { DialogRef, DialogService, ToastOptions, ToastService } from "@bitwarden/components";
|
|
import { CredentialGeneratorHistoryDialogComponent } from "@bitwarden/generator-components";
|
|
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
|
|
import { AddEditFolderDialogComponent, AddEditFolderDialogResult } from "@bitwarden/vault";
|
|
|
|
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
|
import { DesktopAutotypeDefaultSettingPolicy } from "../autofill/services/desktop-autotype-policy.service";
|
|
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
|
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
|
|
|
|
import { SettingsComponent } from "./accounts/settings.component";
|
|
import { ExportDesktopComponent } from "./tools/export/export-desktop.component";
|
|
import { CredentialGeneratorComponent } from "./tools/generator/credential-generator.component";
|
|
import { ImportDesktopComponent } from "./tools/import/import-desktop.component";
|
|
|
|
const BroadcasterSubscriptionId = "AppComponent";
|
|
const IdleTimeout = 60000 * 10; // 10 minutes
|
|
const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
|
|
|
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
|
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
|
@Component({
|
|
selector: "app-root",
|
|
styles: [],
|
|
template: `
|
|
<ng-template #settings></ng-template>
|
|
<ng-template #premium></ng-template>
|
|
<ng-template #passwordHistory></ng-template>
|
|
<ng-template #exportVault></ng-template>
|
|
<ng-template #appGenerator></ng-template>
|
|
<ng-template #loginApproval></ng-template>
|
|
<app-header *ngIf="showHeader$ | async"></app-header>
|
|
|
|
<div id="container">
|
|
<div class="loading" *ngIf="loading">
|
|
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
|
</div>
|
|
<router-outlet *ngIf="!loading"></router-outlet>
|
|
</div>
|
|
|
|
<bit-toast-container></bit-toast-container>
|
|
`,
|
|
standalone: false,
|
|
})
|
|
export class AppComponent implements OnInit, OnDestroy {
|
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
|
// eslint-disable-next-line @angular-eslint/prefer-signals
|
|
@ViewChild("settings", { read: ViewContainerRef, static: true }) settingsRef: ViewContainerRef;
|
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
|
// eslint-disable-next-line @angular-eslint/prefer-signals
|
|
@ViewChild("premium", { read: ViewContainerRef, static: true }) premiumRef: ViewContainerRef;
|
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
|
// eslint-disable-next-line @angular-eslint/prefer-signals
|
|
@ViewChild("passwordHistory", { read: ViewContainerRef, static: true })
|
|
passwordHistoryRef: ViewContainerRef;
|
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
|
// eslint-disable-next-line @angular-eslint/prefer-signals
|
|
@ViewChild("exportVault", { read: ViewContainerRef, static: true })
|
|
exportVaultModalRef: ViewContainerRef;
|
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
|
// eslint-disable-next-line @angular-eslint/prefer-signals
|
|
@ViewChild("appGenerator", { read: ViewContainerRef, static: true })
|
|
generatorModalRef: ViewContainerRef;
|
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
|
// eslint-disable-next-line @angular-eslint/prefer-signals
|
|
@ViewChild("loginApproval", { read: ViewContainerRef, static: true })
|
|
loginApprovalModalRef: ViewContainerRef;
|
|
|
|
showHeader$ = this.accountService.showHeader$;
|
|
loading = false;
|
|
|
|
private lastActivity: Date = null;
|
|
private modal: ModalRef = null;
|
|
private idleTimer: number = null;
|
|
private isIdle = false;
|
|
private activeUserId: UserId = null;
|
|
private activeSimpleDialog: DialogRef<boolean> = null;
|
|
|
|
private destroy$ = new Subject<void>();
|
|
|
|
private accountCleanUpInProgress: { [userId: string]: boolean } = {};
|
|
|
|
constructor(
|
|
private masterPasswordService: MasterPasswordServiceAbstraction,
|
|
private broadcasterService: BroadcasterService,
|
|
private folderService: InternalFolderService,
|
|
private syncService: SyncService,
|
|
private cipherService: CipherService,
|
|
private authService: AuthService,
|
|
private router: Router,
|
|
private toastService: ToastService,
|
|
private i18nService: I18nService,
|
|
private ngZone: NgZone,
|
|
private vaultTimeoutService: VaultTimeoutService,
|
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
|
private keyService: KeyService,
|
|
private logService: LogService,
|
|
private messagingService: MessagingService,
|
|
private collectionService: CollectionService,
|
|
private searchService: SearchService,
|
|
private notificationsService: ServerNotificationsService,
|
|
private platformUtilsService: PlatformUtilsService,
|
|
private systemService: SystemService,
|
|
private processReloadService: ProcessReloadServiceAbstraction,
|
|
private stateService: StateService,
|
|
private eventUploadService: EventUploadService,
|
|
private policyService: InternalPolicyService,
|
|
private modalService: ModalService,
|
|
private keyConnectorService: KeyConnectorService,
|
|
private userVerificationService: UserVerificationService,
|
|
private configService: ConfigService,
|
|
private dialogService: DialogService,
|
|
private biometricStateService: BiometricStateService,
|
|
private stateEventRunnerService: StateEventRunnerService,
|
|
private accountService: AccountService,
|
|
private organizationService: OrganizationService,
|
|
private deviceTrustToastService: DeviceTrustToastService,
|
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
|
private readonly destroyRef: DestroyRef,
|
|
private readonly documentLangSetter: DocumentLangSetter,
|
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
|
private pinService: PinServiceAbstraction,
|
|
private readonly tokenService: TokenService,
|
|
private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy,
|
|
private readonly lockService: LockService,
|
|
) {
|
|
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
|
|
|
const langSubscription = this.documentLangSetter.start();
|
|
this.destroyRef.onDestroy(() => langSubscription.unsubscribe());
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => {
|
|
this.activeUserId = account?.id;
|
|
});
|
|
|
|
this.ngZone.runOutsideAngular(() => {
|
|
setTimeout(async () => {
|
|
await this.updateAppMenu();
|
|
}, 1000);
|
|
|
|
window.ontouchstart = () => this.recordActivity();
|
|
window.onmousedown = () => this.recordActivity();
|
|
window.onscroll = () => this.recordActivity();
|
|
window.onkeypress = () => this.recordActivity();
|
|
});
|
|
|
|
/// ############ DEPRECATED ############
|
|
/// Please do not use the AppComponent to send events between services.
|
|
///
|
|
/// Services that depends on other services, should do so through Dependency Injection
|
|
/// and subscribe to events through that service observable.
|
|
///
|
|
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
|
// 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.ngZone.run(async () => {
|
|
switch (message.command) {
|
|
case "loggedIn":
|
|
case "unlocked":
|
|
// 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.recordActivity();
|
|
// 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.updateAppMenu();
|
|
this.processReloadService.cancelProcessReload();
|
|
break;
|
|
case "loggedOut":
|
|
this.modalService.closeAll();
|
|
// 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.updateAppMenu();
|
|
await this.systemService.clearPendingClipboard();
|
|
await this.processReloadService.startProcessReload();
|
|
break;
|
|
case "authBlocked":
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
this.router.navigate(["login"]);
|
|
break;
|
|
case "logout":
|
|
this.loading = message.userId == null || message.userId === this.activeUserId;
|
|
await this.logOut(message.logoutReason, message.userId);
|
|
this.loading = false;
|
|
break;
|
|
case "lockVault":
|
|
await this.lockService.lock(message.userId);
|
|
break;
|
|
case "lockAllVaults": {
|
|
await this.lockService.lockAll();
|
|
break;
|
|
}
|
|
case "locked":
|
|
this.modalService.closeAll();
|
|
if (
|
|
message.userId == null ||
|
|
message.userId ===
|
|
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))))
|
|
) {
|
|
await this.router.navigate(["lock"]);
|
|
}
|
|
await this.updateAppMenu();
|
|
await this.systemService.clearPendingClipboard();
|
|
await this.processReloadService.startProcessReload();
|
|
break;
|
|
case "startProcessReload":
|
|
// 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.processReloadService.startProcessReload();
|
|
break;
|
|
case "cancelProcessReload":
|
|
this.processReloadService.cancelProcessReload();
|
|
break;
|
|
case "reloadProcess":
|
|
ipc.platform.reloadProcess();
|
|
break;
|
|
case "syncStarted":
|
|
break;
|
|
case "syncCompleted":
|
|
if (message.successfully) {
|
|
// 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.updateAppMenu();
|
|
await this.configService.ensureConfigFetched();
|
|
}
|
|
break;
|
|
case "openSettings":
|
|
await this.openModal<SettingsComponent>(SettingsComponent, this.settingsRef);
|
|
break;
|
|
case "openPremium":
|
|
this.dialogService.open(PremiumComponent);
|
|
break;
|
|
case "showFingerprintPhrase": {
|
|
const activeUserId = await firstValueFrom(
|
|
getUserId(this.accountService.activeAccount$),
|
|
);
|
|
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
|
|
if (publicKey == null) {
|
|
this.logService.error(
|
|
"[AppComponent] No public key available for the user: " +
|
|
activeUserId +
|
|
" fingerprint can't be displayed.",
|
|
);
|
|
} else {
|
|
const fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
|
|
const dialogRef = FingerprintDialogComponent.open(this.dialogService, {
|
|
fingerprint,
|
|
});
|
|
await firstValueFrom(dialogRef.closed);
|
|
}
|
|
break;
|
|
}
|
|
case "deleteAccount":
|
|
await this.deleteAccount();
|
|
break;
|
|
case "openPasswordHistory":
|
|
await this.openGeneratorHistory();
|
|
break;
|
|
case "showToast":
|
|
this.toastService._showToast(message);
|
|
break;
|
|
case "copiedToClipboard":
|
|
if (!message.clearing) {
|
|
// 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.systemService.clearClipboard(message.clipboardValue, message.clearMs);
|
|
}
|
|
break;
|
|
case "ssoCallback": {
|
|
const queryParams = {
|
|
code: message.code,
|
|
state: message.state,
|
|
redirectUri: message.redirectUri ?? DESKTOP_SSO_CALLBACK,
|
|
};
|
|
// 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(["sso"], {
|
|
queryParams: queryParams,
|
|
});
|
|
break;
|
|
}
|
|
case "premiumRequired": {
|
|
const premiumConfirmed = await this.dialogService.openSimpleDialog({
|
|
title: { key: "premiumRequired" },
|
|
content: { key: "premiumRequiredDesc" },
|
|
acceptButtonText: { key: "learnMore" },
|
|
type: "success",
|
|
});
|
|
if (premiumConfirmed) {
|
|
await this.openModal<PremiumComponent>(PremiumComponent, this.premiumRef);
|
|
}
|
|
break;
|
|
}
|
|
case "emailVerificationRequired": {
|
|
const emailVerificationConfirmed = await this.dialogService.openSimpleDialog({
|
|
title: { key: "emailVerificationRequired" },
|
|
content: { key: "emailVerificationRequiredDesc" },
|
|
acceptButtonText: { key: "learnMore" },
|
|
type: "info",
|
|
});
|
|
if (emailVerificationConfirmed) {
|
|
this.platformUtilsService.launchUri(
|
|
"https://bitwarden.com/help/create-bitwarden-account/",
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case "syncVault":
|
|
try {
|
|
await this.syncService.fullSync(true, true);
|
|
this.platformUtilsService.showToast(
|
|
"success",
|
|
null,
|
|
this.i18nService.t("syncingComplete"),
|
|
);
|
|
} catch {
|
|
this.platformUtilsService.showToast(
|
|
"error",
|
|
null,
|
|
this.i18nService.t("syncingFailed"),
|
|
);
|
|
}
|
|
break;
|
|
case "checkSyncVault":
|
|
try {
|
|
const lastSync = await this.syncService.getLastSync();
|
|
let lastSyncAgo = SyncInterval + 1;
|
|
if (lastSync != null) {
|
|
lastSyncAgo = new Date().getTime() - lastSync.getTime();
|
|
}
|
|
|
|
if (lastSyncAgo >= SyncInterval) {
|
|
await this.syncService.fullSync(false);
|
|
}
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
}
|
|
this.messagingService.send("scheduleNextSync");
|
|
break;
|
|
case "importVault":
|
|
await this.dialogService.open(ImportDesktopComponent);
|
|
break;
|
|
case "exportVault":
|
|
await this.dialogService.open(ExportDesktopComponent);
|
|
break;
|
|
case "newLogin":
|
|
this.routeToVault("add", CipherType.Login);
|
|
break;
|
|
case "newCard":
|
|
this.routeToVault("add", CipherType.Card);
|
|
break;
|
|
case "newIdentity":
|
|
this.routeToVault("add", CipherType.Identity);
|
|
break;
|
|
case "newSecureNote":
|
|
this.routeToVault("add", CipherType.SecureNote);
|
|
break;
|
|
default:
|
|
break;
|
|
case "newFolder":
|
|
await this.addFolder();
|
|
break;
|
|
case "openGenerator":
|
|
await this.openGenerator();
|
|
break;
|
|
case "convertAccountToKeyConnector":
|
|
// 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(["/remove-password"]);
|
|
break;
|
|
case "switchAccount": {
|
|
if (message.userId == null) {
|
|
this.logService.error("'switchAccount' message received without userId.");
|
|
return;
|
|
}
|
|
|
|
await this.accountService.switchAccount(message.userId);
|
|
|
|
const locked =
|
|
(await this.authService.getAuthStatus(message.userId)) ===
|
|
AuthenticationStatus.Locked;
|
|
if (locked) {
|
|
this.modalService.closeAll();
|
|
|
|
// We only have to handle TDE lock on "switchAccount" message scenarios but not normal
|
|
// lock scenarios since the user will have always decrypted the vault at least once in those cases.
|
|
const tdeEnabled = await firstValueFrom(
|
|
this.userDecryptionOptionsService
|
|
.userDecryptionOptionsById$(message.userId)
|
|
.pipe(map((decryptionOptions) => decryptionOptions?.trustedDeviceOption != null)),
|
|
);
|
|
|
|
const everHadUserKey = await firstValueFrom(
|
|
this.keyService.everHadUserKey$(message.userId),
|
|
);
|
|
if (tdeEnabled && !everHadUserKey) {
|
|
await this.router.navigate(["login-initiated"]);
|
|
} else {
|
|
await this.router.navigate(["lock"]);
|
|
}
|
|
} else {
|
|
this.messagingService.send("unlocked");
|
|
this.loading = true;
|
|
await this.syncService.fullSync(false);
|
|
this.loading = false;
|
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
this.router.navigate(["vault"]);
|
|
}
|
|
this.messagingService.send("finishSwitchAccount");
|
|
break;
|
|
}
|
|
case "systemSuspended":
|
|
await this.checkForSystemTimeout(VaultTimeoutStringType.OnSleep);
|
|
break;
|
|
case "systemLocked":
|
|
await this.checkForSystemTimeout(VaultTimeoutStringType.OnLocked);
|
|
break;
|
|
case "systemIdle":
|
|
await this.checkForSystemTimeout(VaultTimeoutStringType.OnIdle);
|
|
break;
|
|
case "openLoginApproval":
|
|
if (message.notificationId != null) {
|
|
this.dialogService.closeAll();
|
|
const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, {
|
|
notificationId: message.notificationId,
|
|
});
|
|
await firstValueFrom(dialogRef.closed);
|
|
}
|
|
break;
|
|
case "redrawMenu":
|
|
await this.updateAppMenu();
|
|
break;
|
|
case "deepLink":
|
|
this.processDeepLink(message.urlString);
|
|
break;
|
|
case "launchUri":
|
|
this.platformUtilsService.launchUri(message.url);
|
|
break;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
|
}
|
|
|
|
async addFolder() {
|
|
this.modalService.closeAll();
|
|
|
|
const dialogRef = AddEditFolderDialogComponent.open(this.dialogService);
|
|
const result = await lastValueFrom(dialogRef.closed);
|
|
if (result === AddEditFolderDialogResult.Created) {
|
|
await this.syncService.fullSync(false);
|
|
}
|
|
}
|
|
|
|
async openGenerator() {
|
|
await this.dialogService.open(CredentialGeneratorComponent);
|
|
return;
|
|
}
|
|
|
|
async openGeneratorHistory() {
|
|
await this.dialogService.open(CredentialGeneratorHistoryDialogComponent);
|
|
return;
|
|
}
|
|
|
|
private async updateAppMenu() {
|
|
let updateRequest: MenuUpdateRequest;
|
|
const stateAccounts = await firstValueFrom(this.accountService.accounts$);
|
|
|
|
if (stateAccounts == null || Object.keys(stateAccounts).length < 1) {
|
|
updateRequest = {
|
|
accounts: null,
|
|
activeUserId: null,
|
|
restrictedCipherTypes: null,
|
|
};
|
|
} else {
|
|
const accounts: { [userId: string]: MenuAccount } = {};
|
|
for (const i in stateAccounts) {
|
|
const userId = i as UserId;
|
|
if (
|
|
i != null &&
|
|
userId != null &&
|
|
!this.isAccountCleanUpInProgress(userId) // skip accounts that are being cleaned up
|
|
) {
|
|
const availableTimeoutActions = await firstValueFrom(
|
|
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId),
|
|
);
|
|
|
|
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
|
|
accounts[userId] = {
|
|
isAuthenticated: authStatus >= AuthenticationStatus.Locked,
|
|
isLocked: authStatus === AuthenticationStatus.Locked,
|
|
isLockable: availableTimeoutActions.includes(VaultTimeoutAction.Lock),
|
|
email: stateAccounts[userId].email,
|
|
userId: userId,
|
|
hasMasterPassword: await this.userVerificationService.hasMasterPassword(userId),
|
|
};
|
|
}
|
|
}
|
|
updateRequest = {
|
|
accounts: accounts,
|
|
activeUserId: await firstValueFrom(
|
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
|
),
|
|
restrictedCipherTypes: (
|
|
await firstValueFrom(this.restrictedItemTypesService.restricted$)
|
|
).map((restrictedItems) => restrictedItems.cipherType),
|
|
};
|
|
}
|
|
|
|
this.messagingService.send("updateAppMenu", { updateRequest: updateRequest });
|
|
}
|
|
|
|
private async displayLogoutReason(logoutReason: LogoutReason) {
|
|
let toastOptions: ToastOptions;
|
|
|
|
switch (logoutReason) {
|
|
case "invalidSecurityStamp":
|
|
case "sessionExpired": {
|
|
toastOptions = {
|
|
variant: "warning",
|
|
title: this.i18nService.t("loggedOut"),
|
|
message: this.i18nService.t("loginExpired"),
|
|
};
|
|
break;
|
|
}
|
|
// We don't expect these scenarios to be common, but we want the user to
|
|
// understand why they are being logged out before a process reload.
|
|
case "accessTokenUnableToBeDecrypted": {
|
|
// Don't create multiple dialogs if this fires multiple times
|
|
if (this.activeSimpleDialog) {
|
|
// Let the caller of this function listen for the dialog to close
|
|
return firstValueFrom(this.activeSimpleDialog.closed);
|
|
}
|
|
|
|
this.activeSimpleDialog = this.dialogService.openSimpleDialogRef({
|
|
title: { key: "loggedOut" },
|
|
content: { key: "accessTokenUnableToBeDecrypted" },
|
|
acceptButtonText: { key: "ok" },
|
|
cancelButtonText: null,
|
|
type: "danger",
|
|
});
|
|
|
|
await firstValueFrom(this.activeSimpleDialog.closed);
|
|
this.activeSimpleDialog = null;
|
|
|
|
break;
|
|
}
|
|
case "refreshTokenSecureStorageRetrievalFailure": {
|
|
// Don't create multiple dialogs if this fires multiple times
|
|
if (this.activeSimpleDialog) {
|
|
// Let the caller of this function listen for the dialog to close
|
|
return firstValueFrom(this.activeSimpleDialog.closed);
|
|
}
|
|
|
|
this.activeSimpleDialog = this.dialogService.openSimpleDialogRef({
|
|
title: { key: "loggedOut" },
|
|
content: { key: "refreshTokenSecureStorageRetrievalFailure" },
|
|
acceptButtonText: { key: "ok" },
|
|
cancelButtonText: null,
|
|
type: "danger",
|
|
});
|
|
|
|
await firstValueFrom(this.activeSimpleDialog.closed);
|
|
this.activeSimpleDialog = null;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (toastOptions) {
|
|
this.toastService.showToast(toastOptions);
|
|
}
|
|
}
|
|
|
|
// TODO: PM-21212 - consolidate the logic of this method into the new LogoutService
|
|
// (requires creating a desktop specific implementation of the LogoutService)
|
|
// Even though the userId parameter is no longer optional doesn't mean a message couldn't be
|
|
// passing null-ish values to us.
|
|
private async logOut(logoutReason: LogoutReason, userId: UserId) {
|
|
await this.displayLogoutReason(logoutReason);
|
|
|
|
const activeUserId = await firstValueFrom(
|
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
|
);
|
|
|
|
const userBeingLoggedOut = userId ?? activeUserId;
|
|
|
|
// Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted)
|
|
// doesn't attempt to update a user that is being logged out as we will manually
|
|
// call updateAppMenu when the logout is complete.
|
|
this.startAccountCleanUp(userBeingLoggedOut);
|
|
|
|
const nextUpAccount =
|
|
activeUserId === userBeingLoggedOut
|
|
? await firstValueFrom(this.accountService.nextUpAccount$) // We'll need to switch accounts
|
|
: null;
|
|
|
|
try {
|
|
// HACK: We shouldn't wait for authentication status to change here but instead subscribe to the
|
|
// authentication status to do various actions.
|
|
const logoutPromise = firstValueFrom(
|
|
this.authService.authStatusFor$(userBeingLoggedOut).pipe(
|
|
filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut),
|
|
timeout({
|
|
first: 5_000,
|
|
with: () => {
|
|
throw new Error(
|
|
"The logout process did not complete in a reasonable amount of time.",
|
|
);
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
|
|
// Provide the userId of the user to upload events for
|
|
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
|
|
await this.keyService.clearKeys(userBeingLoggedOut);
|
|
await this.cipherService.clear(userBeingLoggedOut);
|
|
await this.folderService.clear(userBeingLoggedOut);
|
|
await this.biometricStateService.logout(userBeingLoggedOut);
|
|
await this.pinService.logout(userBeingLoggedOut);
|
|
|
|
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
|
|
|
|
await this.stateService.clean({ userId: userBeingLoggedOut });
|
|
await this.tokenService.clearAccessToken(userBeingLoggedOut);
|
|
await this.accountService.clean(userBeingLoggedOut);
|
|
|
|
// HACK: Wait for the user logging outs authentication status to transition to LoggedOut
|
|
await logoutPromise;
|
|
} finally {
|
|
this.finishAccountCleanUp(userBeingLoggedOut);
|
|
}
|
|
|
|
// We only need to change the display at all if the account being looked at is the one
|
|
// being logged out. If it was a background account, no need to do anything.
|
|
if (userBeingLoggedOut === activeUserId) {
|
|
if (nextUpAccount != null) {
|
|
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
|
|
} else {
|
|
// We don't have another user to switch to, bring them to the login page so they
|
|
// can sign into a user.
|
|
await this.accountService.switchAccount(null);
|
|
void this.router.navigate(["login"]);
|
|
}
|
|
}
|
|
|
|
// This must come last otherwise the logout will prematurely trigger
|
|
// a process reload before all the state service user data can be cleaned up
|
|
this.authService.logOut(async () => {}, userBeingLoggedOut);
|
|
}
|
|
|
|
private async recordActivity() {
|
|
if (this.activeUserId == null) {
|
|
return;
|
|
}
|
|
|
|
const now = new Date();
|
|
if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) {
|
|
return;
|
|
}
|
|
|
|
this.lastActivity = now;
|
|
await this.accountService.setAccountActivity(this.activeUserId, now);
|
|
|
|
// Idle states
|
|
if (this.isIdle) {
|
|
this.isIdle = false;
|
|
this.idleStateChanged();
|
|
}
|
|
if (this.idleTimer != null) {
|
|
window.clearTimeout(this.idleTimer);
|
|
this.idleTimer = null;
|
|
}
|
|
this.idleTimer = window.setTimeout(() => {
|
|
if (!this.isIdle) {
|
|
this.isIdle = true;
|
|
this.idleStateChanged();
|
|
}
|
|
}, IdleTimeout);
|
|
}
|
|
|
|
private idleStateChanged() {
|
|
if (this.isIdle) {
|
|
this.notificationsService.disconnectFromInactivity();
|
|
} else {
|
|
this.notificationsService.reconnectFromActivity();
|
|
}
|
|
}
|
|
|
|
private async openModal<T>(type: Type<T>, ref: ViewContainerRef) {
|
|
this.modalService.closeAll();
|
|
|
|
[this.modal] = await this.modalService.openViewRef(type, ref);
|
|
|
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
|
this.modal.onClosed.subscribe(() => {
|
|
this.modal = null;
|
|
});
|
|
}
|
|
|
|
private routeToVault(action: string, cipherType: CipherType) {
|
|
if (!this.router.url.includes("vault")) {
|
|
// 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(["/vault"], {
|
|
queryParams: {
|
|
action: action,
|
|
addType: cipherType,
|
|
},
|
|
replaceUrl: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
private async checkForSystemTimeout(timeout: VaultTimeout): Promise<void> {
|
|
const accounts = await firstValueFrom(this.accountService.accounts$);
|
|
for (const userId in accounts) {
|
|
if (userId == null) {
|
|
continue;
|
|
}
|
|
const options = await this.getVaultTimeoutOptions(userId);
|
|
if (options[0] === timeout) {
|
|
options[1] === "logOut"
|
|
? await this.logOut("vaultTimeout", userId as UserId)
|
|
: await this.lockService.lock(userId as UserId);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async getVaultTimeoutOptions(userId: string): Promise<[VaultTimeout, string]> {
|
|
const timeout = await firstValueFrom(
|
|
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId),
|
|
);
|
|
const action = await firstValueFrom(
|
|
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId),
|
|
);
|
|
return [timeout, action];
|
|
}
|
|
|
|
// Mark an account's clean up as started
|
|
private startAccountCleanUp(userId: string): void {
|
|
this.accountCleanUpInProgress[userId] = true;
|
|
}
|
|
|
|
// Mark an account's clean up as finished
|
|
private finishAccountCleanUp(userId: string): void {
|
|
this.accountCleanUpInProgress[userId] = false;
|
|
}
|
|
|
|
// Check if an account's clean up is in progress
|
|
private isAccountCleanUpInProgress(userId: string): boolean {
|
|
return this.accountCleanUpInProgress[userId] === true;
|
|
}
|
|
|
|
// Process the sso callback links
|
|
private processDeepLink(urlString: string) {
|
|
const url = new URL(urlString);
|
|
const code = url.searchParams.get("code");
|
|
const receivedState = url.searchParams.get("state");
|
|
let message = "";
|
|
|
|
if (code === null) {
|
|
return;
|
|
}
|
|
|
|
if (urlString.indexOf("bitwarden://duo-callback") === 0) {
|
|
message = "duoCallback";
|
|
} else if (receivedState === null) {
|
|
return;
|
|
}
|
|
|
|
if (urlString.indexOf("bitwarden://import-callback-lp") === 0) {
|
|
message = "importCallbackLastPass";
|
|
} else if (urlString.indexOf(DESKTOP_SSO_CALLBACK) === 0) {
|
|
message = "ssoCallback";
|
|
}
|
|
|
|
this.messagingService.send(message, { code: code, state: receivedState });
|
|
}
|
|
|
|
private async deleteAccount() {
|
|
const userIsManaged = await firstValueFrom(
|
|
this.accountService.activeAccount$.pipe(
|
|
getUserId,
|
|
switchMap((userId) => this.organizationService.organizations$(userId)),
|
|
map((orgs) => orgs.some((o) => o.userIsManagedByOrganization === true)),
|
|
),
|
|
);
|
|
|
|
if (userIsManaged) {
|
|
await this.dialogService.openSimpleDialog({
|
|
title: { key: "cannotDeleteAccount" },
|
|
content: { key: "cannotDeleteAccountDesc" },
|
|
cancelButtonText: null,
|
|
acceptButtonText: { key: "close" },
|
|
type: "danger",
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
DeleteAccountComponent.open(this.dialogService);
|
|
}
|
|
}
|