mirror of
https://github.com/bitwarden/browser
synced 2026-01-31 00:33:33 +00:00
Merge branch 'main' into ps/PM-14166-add-brave-vivaldi
This commit is contained in:
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
@@ -197,7 +197,6 @@
|
||||
"nx",
|
||||
"oo7",
|
||||
"oslog",
|
||||
"parse5",
|
||||
"pin-project",
|
||||
"pkg",
|
||||
"postcss",
|
||||
|
||||
14
.github/workflows/build-desktop.yml
vendored
14
.github/workflows/build-desktop.yml
vendored
@@ -209,7 +209,7 @@ jobs:
|
||||
- name: Set up environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install pkg-config libxss-dev rpm musl-dev musl-tools flatpak flatpak-builder
|
||||
sudo apt-get -y install pkg-config libxss-dev rpm flatpak flatpak-builder
|
||||
|
||||
- name: Set up Snap
|
||||
run: sudo snap install snapcraft --classic
|
||||
@@ -262,12 +262,10 @@ jobs:
|
||||
env:
|
||||
PKG_CONFIG_ALLOW_CROSS: true
|
||||
PKG_CONFIG_ALL_STATIC: true
|
||||
TARGET: musl
|
||||
# Note: It is important that we use the release build because some compute heavy
|
||||
# operations such as key derivation for oo7 on linux are too slow in debug mode
|
||||
# operations such as key derivation for oo7 on linux are too slow in debug mode
|
||||
run: |
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
node build.js --target=x86_64-unknown-linux-musl --release
|
||||
node build.js --release
|
||||
|
||||
- name: Build application
|
||||
run: npm run dist:lin
|
||||
@@ -367,7 +365,7 @@ jobs:
|
||||
- name: Set up environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install pkg-config libxss-dev rpm musl-dev musl-tools flatpak flatpak-builder squashfs-tools ruby ruby-dev rubygems build-essential
|
||||
sudo apt-get -y install pkg-config libxss-dev rpm flatpak flatpak-builder squashfs-tools ruby ruby-dev rubygems build-essential
|
||||
sudo gem install --no-document fpm
|
||||
|
||||
- name: Set up Snap
|
||||
@@ -427,12 +425,10 @@ jobs:
|
||||
env:
|
||||
PKG_CONFIG_ALLOW_CROSS: true
|
||||
PKG_CONFIG_ALL_STATIC: true
|
||||
TARGET: musl
|
||||
# Note: It is important that we use the release build because some compute heavy
|
||||
# operations such as key derivation for oo7 on linux are too slow in debug mode
|
||||
run: |
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
node build.js --target=aarch64-unknown-linux-musl --release
|
||||
node build.js --release
|
||||
|
||||
- name: Check index.d.ts generated
|
||||
if: github.event_name == 'pull_request' && steps.cache.outputs.cache-hit != 'true'
|
||||
|
||||
2
.github/workflows/publish-web.yml
vendored
2
.github/workflows/publish-web.yml
vendored
@@ -187,6 +187,8 @@ jobs:
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: self-host
|
||||
|
||||
- name: Trigger Bitwarden lite build
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
|
||||
- name: Trigger test-all workflow in browser-interactions-testing
|
||||
if: steps.changed-files.outputs.monitored == 'true'
|
||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
repository: "bitwarden/browser-interactions-testing"
|
||||
|
||||
@@ -1475,6 +1475,9 @@
|
||||
"selectFile": {
|
||||
"message": "Select a file"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"maxFileSize": {
|
||||
"message": "Maximum file size is 500 MB."
|
||||
},
|
||||
@@ -3249,9 +3252,6 @@
|
||||
"copyCustomFieldNameNotUnique": {
|
||||
"message": "No unique identifier found."
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Organization name"
|
||||
},
|
||||
@@ -5888,6 +5888,45 @@
|
||||
"cardNumberLabel": {
|
||||
"message": "Card number"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector":{
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified":{
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified":{
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
@@ -5937,5 +5976,53 @@
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<button
|
||||
*ngIf="currentAccount$ | async as currentAccount; else defaultButton"
|
||||
type="button"
|
||||
class="tw-rounded-full hover:tw-outline hover:tw-outline-1 hover:tw-outline-offset-1 hover:tw-outline-primary-600"
|
||||
class="tw-rounded-full hover:tw-outline hover:tw-outline-1 hover:tw-outline-primary-600"
|
||||
(click)="currentAccountClicked()"
|
||||
>
|
||||
<span class="tw-sr-only"> {{ "bitwardenAccount" | i18n }} {{ currentAccount.email }}</span>
|
||||
|
||||
@@ -102,6 +102,36 @@ describe("ExtensionLoginComponentService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("redirectToSsoLoginWithOrganizationSsoIdentifier", () => {
|
||||
it("launches SSO browser window with correct Url", async () => {
|
||||
const email = "test@bitwarden.com";
|
||||
const state = "testState";
|
||||
const expectedState = "testState:clientId=browser";
|
||||
const codeVerifier = "testCodeVerifier";
|
||||
const codeChallenge = "testCodeChallenge";
|
||||
const orgSsoIdentifier = "org-sso-identifier";
|
||||
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
|
||||
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
|
||||
|
||||
await service.redirectToSsoLoginWithOrganizationSsoIdentifier(email, orgSsoIdentifier);
|
||||
|
||||
expect(ssoUrlService.buildSsoUrl).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(expectedState);
|
||||
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
|
||||
expect(platformUtilsService.launchUri).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("showBackButton", () => {
|
||||
it("sets showBackButton in extensionAnonLayoutWrapperDataService", () => {
|
||||
service.showBackButton(true);
|
||||
|
||||
@@ -47,6 +47,7 @@ export class ExtensionLoginComponentService
|
||||
email: string,
|
||||
state: string,
|
||||
codeChallenge: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
@@ -60,6 +61,7 @@ export class ExtensionLoginComponentService
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
|
||||
this.platformUtilsService.launchUri(webAppSsoUrl);
|
||||
|
||||
@@ -3,9 +3,7 @@ import { DeviceManagementComponentServiceAbstraction } from "@bitwarden/angular/
|
||||
/**
|
||||
* Browser extension implementation of the device management component service
|
||||
*/
|
||||
export class ExtensionDeviceManagementComponentService
|
||||
implements DeviceManagementComponentServiceAbstraction
|
||||
{
|
||||
export class ExtensionDeviceManagementComponentService implements DeviceManagementComponentServiceAbstraction {
|
||||
/**
|
||||
* Don't show header information in browser extension client
|
||||
*/
|
||||
|
||||
@@ -120,9 +120,7 @@ export type BrowserFido2ParentWindowReference = chrome.tabs.Tab;
|
||||
* Browser implementation of the {@link Fido2UserInterfaceService}.
|
||||
* The user interface is implemented as a popout and the service uses the browser's messaging API to communicate with it.
|
||||
*/
|
||||
export class BrowserFido2UserInterfaceService
|
||||
implements Fido2UserInterfaceServiceAbstraction<BrowserFido2ParentWindowReference>
|
||||
{
|
||||
export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction<BrowserFido2ParentWindowReference> {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
async newSession(
|
||||
|
||||
@@ -129,7 +129,12 @@ export class AutofillInlineMenuContainer {
|
||||
}
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const isExtensionProtocol = /^[a-z]+(-[a-z]+)?-extension:$/i.test(urlObj.protocol);
|
||||
const extensionProtocols = new Set([
|
||||
"chrome-extension:",
|
||||
"moz-extension:",
|
||||
"safari-web-extension:",
|
||||
]);
|
||||
const isExtensionProtocol = extensionProtocols.has(urlObj.protocol);
|
||||
|
||||
if (!isExtensionProtocol) {
|
||||
return false;
|
||||
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
OverlayNotificationsExtensionMessageHandlers,
|
||||
} from "../abstractions/overlay-notifications-content.service";
|
||||
|
||||
export class OverlayNotificationsContentService
|
||||
implements OverlayNotificationsContentServiceInterface
|
||||
{
|
||||
export class OverlayNotificationsContentService implements OverlayNotificationsContentServiceInterface {
|
||||
private notificationBarRootElement: HTMLElement | null = null;
|
||||
private notificationBarElement: HTMLElement | null = null;
|
||||
private notificationBarIframeElement: HTMLIFrameElement | null = null;
|
||||
|
||||
@@ -16,9 +16,7 @@ import {
|
||||
} from "./autofill-constants";
|
||||
import AutofillService from "./autofill.service";
|
||||
|
||||
export class InlineMenuFieldQualificationService
|
||||
implements InlineMenuFieldQualificationServiceInterface
|
||||
{
|
||||
export class InlineMenuFieldQualificationService implements InlineMenuFieldQualificationServiceInterface {
|
||||
private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);
|
||||
private excludedAutofillFieldTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes);
|
||||
private usernameFieldTypes = new Set(["text", "email", "number", "tel"]);
|
||||
|
||||
@@ -841,10 +841,7 @@ export default class MainBackground {
|
||||
);
|
||||
|
||||
this.pinService = new PinService(
|
||||
this.accountService,
|
||||
this.encryptService,
|
||||
this.kdfConfigService,
|
||||
this.keyGenerationService,
|
||||
this.logService,
|
||||
this.keyService,
|
||||
this.sdkService,
|
||||
@@ -1112,7 +1109,7 @@ export default class MainBackground {
|
||||
this.collectionService,
|
||||
this.keyService,
|
||||
this.encryptService,
|
||||
this.pinService,
|
||||
this.keyGenerationService,
|
||||
this.accountService,
|
||||
this.restrictedItemTypesService,
|
||||
);
|
||||
@@ -1120,7 +1117,7 @@ export default class MainBackground {
|
||||
this.individualVaultExportService = new IndividualVaultExportService(
|
||||
this.folderService,
|
||||
this.cipherService,
|
||||
this.pinService,
|
||||
this.keyGenerationService,
|
||||
this.keyService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
@@ -1134,7 +1131,7 @@ export default class MainBackground {
|
||||
this.organizationVaultExportService = new OrganizationVaultExportService(
|
||||
this.cipherService,
|
||||
this.exportApiService,
|
||||
this.pinService,
|
||||
this.keyGenerationService,
|
||||
this.keyService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
|
||||
@@ -48,7 +48,11 @@ export class ForegroundBrowserBiometricsService extends BiometricsService {
|
||||
result: BiometricsStatus;
|
||||
error: string;
|
||||
}>(BiometricsCommands.GetBiometricsStatusForUser, { userId: id });
|
||||
return response.result;
|
||||
if (response != null) {
|
||||
return response.result;
|
||||
} else {
|
||||
return BiometricsStatus.DesktopDisconnected;
|
||||
}
|
||||
}
|
||||
|
||||
async getShouldAutopromptNow(): Promise<boolean> {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<popup-page>
|
||||
<popup-header slot="header" pageTitle="{{ 'removeMasterPassword' | i18n }}">
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
@if (loading) {
|
||||
<div class="tw-text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
} @else {
|
||||
<p>{{ "removeMasterPasswordForOrganizationUserKeyConnector" | i18n }}</p>
|
||||
<p class="tw-mb-0">{{ "organizationName" | i18n }}:</p>
|
||||
<p class="tw-text-muted tw-mb-6">{{ organization.name }}</p>
|
||||
<p class="tw-mb-0">{{ "keyConnectorDomain" | i18n }}:</p>
|
||||
<p class="tw-text-muted tw-mb-6">{{ organization.keyConnectorUrl }}</p>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
block
|
||||
(click)="convert()"
|
||||
[disabled]="action"
|
||||
class="tw-mb-2"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
*ngIf="continuing"
|
||||
></i>
|
||||
{{ "removeMasterPassword" | i18n }}
|
||||
</button>
|
||||
|
||||
<button type="button" bitButton block (click)="leave()" [disabled]="action">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
*ngIf="leaving"
|
||||
></i>
|
||||
{{ "leaveOrganization" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</popup-page>
|
||||
@@ -1,14 +0,0 @@
|
||||
// FIXME (PM-22628): angular imports are forbidden in background
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
// 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-remove-password",
|
||||
templateUrl: "remove-password.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}
|
||||
@@ -22,7 +22,7 @@ export type NavButton = {
|
||||
templateUrl: "popup-tab-navigation.component.html",
|
||||
imports: [CommonModule, LinkModule, RouterModule, JslibModule, IconModule],
|
||||
host: {
|
||||
class: "tw-block tw-h-full tw-w-full tw-flex tw-flex-col",
|
||||
class: "tw-block tw-size-full tw-flex tw-flex-col",
|
||||
},
|
||||
})
|
||||
export class PopupTabNavigationComponent {
|
||||
|
||||
@@ -43,7 +43,11 @@ import {
|
||||
TwoFactorAuthGuard,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
|
||||
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
|
||||
import {
|
||||
LockComponent,
|
||||
ConfirmKeyConnectorDomainComponent,
|
||||
RemovePasswordComponent,
|
||||
} from "@bitwarden/key-management-ui";
|
||||
|
||||
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
|
||||
import { AuthExtensionRoute } from "../auth/popup/constants/auth-extension-route.constant";
|
||||
@@ -59,7 +63,6 @@ import { NotificationsSettingsComponent } from "../autofill/popup/settings/notif
|
||||
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
|
||||
import { PhishingWarning } from "../dirt/phishing-detection/popup/phishing-warning.component";
|
||||
import { ProtectedByComponent } from "../dirt/phishing-detection/popup/protected-by-component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import BrowserPopupUtils from "../platform/browser/browser-popup-utils";
|
||||
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
|
||||
import { RouteCacheOptions } from "../platform/services/popup-view-cache-background.service";
|
||||
@@ -188,9 +191,22 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "remove-password",
|
||||
component: RemovePasswordComponent,
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: RemovePasswordComponent,
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "verifyYourOrganization",
|
||||
},
|
||||
showBackButton: false,
|
||||
pageIcon: LockIcon,
|
||||
} satisfies ExtensionAnonLayoutWrapperData,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "view-cipher",
|
||||
@@ -646,7 +662,7 @@ const routes: Routes = [
|
||||
component: ConfirmKeyConnectorDomainComponent,
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "confirmKeyConnectorDomain",
|
||||
key: "verifyYourOrganization",
|
||||
},
|
||||
showBackButton: true,
|
||||
pageIcon: DomainIcon,
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
</bit-callout>
|
||||
</div>
|
||||
} @else {
|
||||
<div [@routerTransition]="getRouteElevation(outlet)">
|
||||
<router-outlet #outlet="outlet"></router-outlet>
|
||||
<!-- eslint-disable-next-line -->
|
||||
<div class="tw-h-screen tw-w-screen">
|
||||
<div [@routerTransition]="getRouteElevation(outlet)" class="tw-size-full">
|
||||
<router-outlet #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
<bit-toast-container></bit-toast-container>
|
||||
</div>
|
||||
<bit-toast-container></bit-toast-container>
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import { CurrentAccountComponent } from "../auth/popup/account-switching/current
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { PopOutComponent } from "../platform/popup/components/pop-out.component";
|
||||
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
|
||||
@@ -85,13 +84,7 @@ import "../platform/popup/locales";
|
||||
CalloutModule,
|
||||
LinkModule,
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
ColorPasswordPipe,
|
||||
ColorPasswordCountPipe,
|
||||
TabsV2Component,
|
||||
RemovePasswordComponent,
|
||||
],
|
||||
declarations: [AppComponent, ColorPasswordPipe, ColorPasswordCountPipe, TabsV2Component],
|
||||
exports: [CalloutModule],
|
||||
providers: [CurrencyPipe, DatePipe],
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
@@ -1,453 +0,0 @@
|
||||
@import "variables.scss";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
min-height: 600px;
|
||||
height: 100%;
|
||||
|
||||
&.body-sm {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
&.body-xs {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
&.body-xxs {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
&.body-3xs {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
&.body-full {
|
||||
min-height: unset;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
& body {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: $font-family-sans-serif;
|
||||
font-size: $font-size-base;
|
||||
line-height: $line-height-base;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 380px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
min-height: inherit;
|
||||
overflow: hidden;
|
||||
color: $text-color;
|
||||
background-color: $background-color;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("textColor");
|
||||
background-color: themed("backgroundColor");
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: $font-family-sans-serif;
|
||||
font-size: $font-size-base;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
a:not(popup-page a, popup-tab-navigation a) {
|
||||
text-decoration: none;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("primaryColor");
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
@include themify($themes) {
|
||||
color: darken(themed("primaryColor"), 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input:not(bit-form-field input, bit-search input, input[bitcheckbox]),
|
||||
select:not(bit-form-field select),
|
||||
textarea:not(bit-form-field textarea) {
|
||||
@include themify($themes) {
|
||||
color: themed("textColor");
|
||||
background-color: themed("inputBackgroundColor");
|
||||
}
|
||||
}
|
||||
|
||||
input:not(input[bitcheckbox]),
|
||||
select,
|
||||
textarea,
|
||||
button:not(bit-chip-select button) {
|
||||
font-size: $font-size-base;
|
||||
font-family: $font-family-sans-serif;
|
||||
}
|
||||
|
||||
input[type*="date"] {
|
||||
@include themify($themes) {
|
||||
color-scheme: themed("dateInputColorScheme");
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-calendar-picker-indicator {
|
||||
@include themify($themes) {
|
||||
filter: themed("webkitCalendarPickerFilter");
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-calendar-picker-indicator:hover {
|
||||
@include themify($themes) {
|
||||
filter: themed("webkitCalendarPickerHoverFilter");
|
||||
}
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.35rem;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
app-root > div {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main::-webkit-scrollbar,
|
||||
cdk-virtual-scroll-viewport::-webkit-scrollbar,
|
||||
.vault-select::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
main::-webkit-scrollbar-track,
|
||||
.vault-select::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
cdk-virtual-scroll-viewport::-webkit-scrollbar-track {
|
||||
@include themify($themes) {
|
||||
background-color: themed("backgroundColor");
|
||||
}
|
||||
}
|
||||
|
||||
main::-webkit-scrollbar-thumb,
|
||||
cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb,
|
||||
.vault-select::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
margin-right: 1px;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: themed("scrollbarColor");
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
background-color: themed("scrollbarHoverColor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header:not(bit-callout header, bit-dialog header, popup-page header) {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
|
||||
&:not(.no-theme) {
|
||||
border-bottom: 1px solid #000000;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("headerColor");
|
||||
background-color: themed("headerBackgroundColor");
|
||||
border-bottom-color: themed("headerBorderColor");
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.header-content > .right,
|
||||
.header-content > .right > .right {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-width: -webkit-min-content; /* Workaround to Chrome bug */
|
||||
.header-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
app-avatar {
|
||||
max-height: 30px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.login-center {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
app-pop-out > button,
|
||||
div > button:not(app-current-account button):not(.home-acc-switcher-btn),
|
||||
div > a {
|
||||
border: none;
|
||||
padding: 0 10px;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
white-space: pre;
|
||||
|
||||
&:not(.home-acc-switcher-btn):hover,
|
||||
&:not(.home-acc-switcher-btn):focus {
|
||||
@include themify($themes) {
|
||||
background-color: themed("headerBackgroundHoverColor");
|
||||
color: themed("headerColor");
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.65;
|
||||
cursor: default !important;
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
i + span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
app-pop-out {
|
||||
display: flex;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.search {
|
||||
padding: 7px 10px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
.bwi {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 20px;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("headerInputPlaceholderColor");
|
||||
}
|
||||
}
|
||||
|
||||
input:not(bit-form-field input) {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: none;
|
||||
padding: 5px 10px 5px 30px;
|
||||
border-radius: $border-radius;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: themed("headerInputBackgroundColor");
|
||||
color: themed("headerInputColor");
|
||||
}
|
||||
|
||||
&::selection {
|
||||
@include themify($themes) {
|
||||
// explicitly set text selection to invert foreground/background
|
||||
background-color: themed("headerInputColor");
|
||||
color: themed("headerInputBackgroundColor");
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-radius: $border-radius;
|
||||
outline: none;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: themed("headerInputBackgroundFocusColor");
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
@include themify($themes) {
|
||||
color: themed("headerInputPlaceholderColor");
|
||||
}
|
||||
}
|
||||
/** make the cancel button visible in both dark/light themes **/
|
||||
&[type="search"]::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
background-repeat: no-repeat;
|
||||
mask-image: url("../images/close-button-white.svg");
|
||||
-webkit-mask-image: url("../images/close-button-white.svg");
|
||||
@include themify($themes) {
|
||||
background-color: themed("headerInputColor");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left + .search,
|
||||
.left + .sr-only + .search {
|
||||
padding-left: 0;
|
||||
|
||||
.bwi {
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.search + .right {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 15px 5px;
|
||||
}
|
||||
|
||||
app-root {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: themed("backgroundColor");
|
||||
}
|
||||
}
|
||||
|
||||
main:not(popup-page main):not(auth-anon-layout main) {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: themed("backgroundColor");
|
||||
}
|
||||
|
||||
&.no-header {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&.flex {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: calc(100% - 44px);
|
||||
}
|
||||
}
|
||||
|
||||
.center-content,
|
||||
.no-items,
|
||||
.full-loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.no-items,
|
||||
.full-loading-spinner {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.no-items-image {
|
||||
@include themify($themes) {
|
||||
content: url("../images/search-desktop" + themed("svgSuffix"));
|
||||
}
|
||||
}
|
||||
|
||||
.bwi {
|
||||
margin-bottom: 10px;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("disabledIconColor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cdk-virtual-scroll
|
||||
.cdk-virtual-scroll-viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.cdk-virtual-scroll-content-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,620 +0,0 @@
|
||||
@import "variables.scss";
|
||||
|
||||
.box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
margin: 0 10px 5px 10px;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("headingColor");
|
||||
}
|
||||
}
|
||||
|
||||
.box-content {
|
||||
@include themify($themes) {
|
||||
background-color: themed("backgroundColor");
|
||||
border-color: themed("borderColor");
|
||||
}
|
||||
|
||||
&.box-content-padded {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
&.condensed .box-content-row,
|
||||
.box-content-row.condensed {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
&.no-hover .box-content-row,
|
||||
.box-content-row.no-hover {
|
||||
&:hover,
|
||||
&:focus {
|
||||
@include themify($themes) {
|
||||
background-color: themed("boxBackgroundColor") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.single-line .box-content-row,
|
||||
.box-content-row.single-line {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
&.row-top-padding {
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.box-footer {
|
||||
margin: 0 5px 5px 5px;
|
||||
padding: 0 10px 5px 10px;
|
||||
font-size: $font-size-small;
|
||||
|
||||
button.btn {
|
||||
font-size: $font-size-small;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button.btn.primary {
|
||||
font-size: $font-size-base;
|
||||
padding: 7px 15px;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
border-color: themed("borderHoverColor") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("mutedColor");
|
||||
}
|
||||
}
|
||||
|
||||
&.list {
|
||||
margin: 10px 0 20px 0;
|
||||
.box-content {
|
||||
.virtual-scroll-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.box-content-row {
|
||||
text-decoration: none;
|
||||
border-radius: $border-radius;
|
||||
// background-color: $background-color;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("textColor");
|
||||
background-color: themed("boxBackgroundColor");
|
||||
}
|
||||
|
||||
&.padded {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
&.no-hover {
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
background-color: themed("boxBackgroundColor") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.active {
|
||||
@include themify($themes) {
|
||||
background-color: themed("listItemBackgroundHoverColor");
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-left: 5px solid #000000;
|
||||
padding-left: 5px;
|
||||
|
||||
@include themify($themes) {
|
||||
border-left-color: themed("mutedColor");
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
.row-btn {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.text:not(.no-ellipsis),
|
||||
.detail {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.row-main {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: normal;
|
||||
|
||||
.row-main-content {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.single-line {
|
||||
.box-content-row {
|
||||
display: flex;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
margin: 5px;
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box-content-row {
|
||||
display: block;
|
||||
padding: 5px 10px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-radius: $border-radius;
|
||||
margin: 3px 5px;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: themed("boxBackgroundColor");
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&:before {
|
||||
border: none;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.override-last:last-child:before {
|
||||
border-bottom: 1px solid #000000;
|
||||
@include themify($themes) {
|
||||
border-bottom-color: themed("boxBorderColor");
|
||||
}
|
||||
}
|
||||
|
||||
&.last:last-child:before {
|
||||
border-bottom: 1px solid #000000;
|
||||
@include themify($themes) {
|
||||
border-bottom-color: themed("boxBorderColor");
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.active {
|
||||
@include themify($themes) {
|
||||
background-color: themed("boxBackgroundHoverColor");
|
||||
}
|
||||
}
|
||||
|
||||
&.pre {
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
&.pre-wrap {
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.row-label,
|
||||
label {
|
||||
font-size: $font-size-small;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("mutedColor");
|
||||
}
|
||||
|
||||
.sub-label {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-label {
|
||||
font-size: $font-size-small;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 5px;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("mutedColor");
|
||||
}
|
||||
|
||||
> a {
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.text,
|
||||
.detail {
|
||||
display: block;
|
||||
text-align: left;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("textColor");
|
||||
}
|
||||
}
|
||||
|
||||
.detail {
|
||||
font-size: $font-size-small;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("mutedColor");
|
||||
}
|
||||
}
|
||||
|
||||
.img-right,
|
||||
.txt-right {
|
||||
float: right;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.row-main {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&.box-content-row-flex,
|
||||
.box-content-row-flex,
|
||||
&.box-content-row-checkbox,
|
||||
&.box-content-row-link,
|
||||
&.box-content-row-input,
|
||||
&.box-content-row-slider,
|
||||
&.box-content-row-multi {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
word-break: break-all;
|
||||
|
||||
&.box-content-row-word-break {
|
||||
word-break: normal;
|
||||
}
|
||||
}
|
||||
|
||||
&.box-content-row-multi {
|
||||
input:not([type="checkbox"]) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input + label.sr-only + select {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
> a,
|
||||
> button {
|
||||
padding: 8px 8px 8px 4px;
|
||||
margin: 0;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("dangerColor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.box-content-row-multi,
|
||||
&.box-content-row-newmulti {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
&.box-content-row-newmulti {
|
||||
@include themify($themes) {
|
||||
color: themed("primaryColor");
|
||||
}
|
||||
}
|
||||
|
||||
&.box-content-row-checkbox,
|
||||
&.box-content-row-link,
|
||||
&.box-content-row-input,
|
||||
&.box-content-row-slider {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
margin: 5px;
|
||||
|
||||
label,
|
||||
.row-label {
|
||||
font-size: $font-size-base;
|
||||
display: block;
|
||||
width: initial;
|
||||
margin-bottom: 0;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("textColor");
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
@include themify($themes) {
|
||||
color: themed("mutedColor");
|
||||
}
|
||||
}
|
||||
|
||||
> input {
|
||||
margin: 0 0 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
> * {
|
||||
margin-right: 15px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.box-content-row-checkbox-left {
|
||||
justify-content: flex-start;
|
||||
|
||||
> input {
|
||||
margin: 0 15px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.box-content-row-input {
|
||||
label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: right;
|
||||
|
||||
&[type="number"] {
|
||||
max-width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.box-content-row-slider {
|
||||
input[type="range"] {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
input:not([type="checkbox"]):not([type="radio"]),
|
||||
textarea {
|
||||
border: none;
|
||||
width: 100%;
|
||||
background-color: transparent !important;
|
||||
|
||||
&::-webkit-input-placeholder {
|
||||
@include themify($themes) {
|
||||
color: themed("inputPlaceholderColor");
|
||||
}
|
||||
}
|
||||
|
||||
&:not([type="file"]):focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
border: 1px solid #000000;
|
||||
border-radius: $border-radius;
|
||||
padding: 7px 4px;
|
||||
|
||||
@include themify($themes) {
|
||||
border-color: themed("inputBorderColor");
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
margin-left: 5px;
|
||||
|
||||
&.action-buttons-fixed {
|
||||
align-self: start;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.row-btn {
|
||||
cursor: pointer;
|
||||
padding: 10px 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("boxRowButtonColor");
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
@include themify($themes) {
|
||||
color: themed("boxRowButtonHoverColor");
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&[disabled] {
|
||||
@include themify($themes) {
|
||||
color: themed("disabledIconColor");
|
||||
opacity: themed("disabledBoxOpacity");
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
color: themed("disabledIconColor");
|
||||
opacity: themed("disabledBoxOpacity");
|
||||
}
|
||||
}
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-pad .row-btn {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.box-draggable-row) {
|
||||
.action-buttons .row-btn:last-child {
|
||||
margin-right: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.box-draggable-row {
|
||||
&.box-content-row-checkbox {
|
||||
input[type="checkbox"] + .drag-handle {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: move;
|
||||
padding: 10px 2px 10px 8px;
|
||||
user-select: none;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("mutedColor");
|
||||
}
|
||||
}
|
||||
|
||||
&.cdk-drag-preview {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.8;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: themed("boxBackgroundColor");
|
||||
}
|
||||
}
|
||||
|
||||
select.field-type {
|
||||
margin: 5px 0 0 25px;
|
||||
width: calc(100% - 25px);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 34px;
|
||||
margin-left: -5px;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("mutedColor");
|
||||
}
|
||||
|
||||
&.icon-small {
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: $border-radius;
|
||||
max-height: 20px;
|
||||
max-width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: flex;
|
||||
height: 5px;
|
||||
overflow: hidden;
|
||||
margin: 5px -15px -10px;
|
||||
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
background-color: $brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
input {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0 0 0 5px;
|
||||
flex-grow: 1;
|
||||
font-size: $font-size-base;
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("textColor");
|
||||
}
|
||||
}
|
||||
|
||||
&.align-start {
|
||||
align-items: start;
|
||||
margin-top: 10px;
|
||||
|
||||
label {
|
||||
margin-top: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.truncate {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
form {
|
||||
.box {
|
||||
.box-content {
|
||||
.box-content-row {
|
||||
&.no-hover {
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
background-color: themed("transparentColor") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
@import "variables.scss";
|
||||
|
||||
.btn {
|
||||
border-radius: $border-radius;
|
||||
padding: 7px 15px;
|
||||
border: 1px solid #000000;
|
||||
font-size: $font-size-base;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: themed("buttonBackgroundColor");
|
||||
border-color: themed("buttonBorderColor");
|
||||
color: themed("buttonColor");
|
||||
}
|
||||
|
||||
&.primary {
|
||||
@include themify($themes) {
|
||||
color: themed("buttonPrimaryColor");
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
@include themify($themes) {
|
||||
color: themed("buttonDangerColor");
|
||||
}
|
||||
}
|
||||
|
||||
&.callout-half {
|
||||
font-weight: bold;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
&:hover:not([disabled]) {
|
||||
cursor: pointer;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: darken(themed("buttonBackgroundColor"), 1.5%);
|
||||
border-color: darken(themed("buttonBorderColor"), 17%);
|
||||
color: darken(themed("buttonColor"), 10%);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
@include themify($themes) {
|
||||
color: darken(themed("buttonPrimaryColor"), 6%);
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
@include themify($themes) {
|
||||
color: darken(themed("buttonDangerColor"), 6%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus:not([disabled]) {
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: darken(themed("buttonBackgroundColor"), 6%);
|
||||
border-color: darken(themed("buttonBorderColor"), 25%);
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.65;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
&.block {
|
||||
display: block;
|
||||
width: calc(100% - 10px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&.link,
|
||||
&.neutral {
|
||||
border: none !important;
|
||||
background: none !important;
|
||||
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
.btn {
|
||||
&:focus {
|
||||
outline: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.box-content-row {
|
||||
display: block;
|
||||
width: calc(100% - 10px);
|
||||
text-align: left;
|
||||
border-color: none;
|
||||
|
||||
@include themify($themes) {
|
||||
background-color: themed("boxBackgroundColor");
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.login-buttons {
|
||||
.btn.block {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
@import "variables.scss";
|
||||
|
||||
html.browser_safari {
|
||||
&.safari_height_fix {
|
||||
body {
|
||||
height: 360px !important;
|
||||
|
||||
&.body-xs {
|
||||
height: 300px !important;
|
||||
}
|
||||
|
||||
&.body-full {
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
.search .bwi {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.left + .search .bwi {
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
&.login-page {
|
||||
padding-top: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
app-root {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #000000;
|
||||
}
|
||||
|
||||
&.theme_light app-root {
|
||||
border-color: #777777;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
.row {
|
||||
display: flex;
|
||||
margin: 0 -15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.col {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
padding: 0 15px;
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
@import "variables.scss";
|
||||
|
||||
small,
|
||||
.small {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
@include themify($themes) {
|
||||
background-color: themed("primaryColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
@include themify($themes) {
|
||||
background-color: themed("successColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
@include themify($themes) {
|
||||
background-color: themed("dangerColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
@include themify($themes) {
|
||||
background-color: themed("infoColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
@include themify($themes) {
|
||||
background-color: themed("warningColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
@include themify($themes) {
|
||||
color: themed("primaryColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-success {
|
||||
@include themify($themes) {
|
||||
color: themed("successColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
@include themify($themes) {
|
||||
color: themed("mutedColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-default {
|
||||
@include themify($themes) {
|
||||
color: themed("textColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
@include themify($themes) {
|
||||
color: themed("dangerColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-info {
|
||||
@include themify($themes) {
|
||||
color: themed("infoColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
@include themify($themes) {
|
||||
color: themed("warningColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.font-weight-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p.lead {
|
||||
font-size: $font-size-large;
|
||||
margin-bottom: 20px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.flex-right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.flex-bottom {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.display-block {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.monospaced {
|
||||
font-family: $font-family-monospace;
|
||||
}
|
||||
|
||||
.show-whitespace {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.img-responsive {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.img-rounded {
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
.select-index-top {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
:not(:focus) > .exists-only-on-parent-focus {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.password-wrapper {
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.password-number {
|
||||
@include themify($themes) {
|
||||
color: themed("passwordNumberColor");
|
||||
}
|
||||
}
|
||||
|
||||
.password-special {
|
||||
@include themify($themes) {
|
||||
color: themed("passwordSpecialColor");
|
||||
}
|
||||
}
|
||||
|
||||
.password-character {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 36px;
|
||||
font-weight: 600;
|
||||
|
||||
&:nth-child(odd) {
|
||||
@include themify($themes) {
|
||||
background-color: themed("backgroundColor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.password-count {
|
||||
white-space: nowrap;
|
||||
font-size: 8px;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("passwordCountText") !important;
|
||||
}
|
||||
}
|
||||
|
||||
#duo-frame {
|
||||
background: url("../images/loading.svg") 0 0 no-repeat;
|
||||
width: 100%;
|
||||
height: 470px;
|
||||
margin-bottom: -10px;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
#web-authn-frame {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
|
||||
iframe {
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
body.linux-webauthn {
|
||||
width: 485px !important;
|
||||
#web-authn-frame {
|
||||
iframe {
|
||||
width: 375px;
|
||||
margin: 0 55px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app-root > #loading {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
color: $text-muted;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("mutedColor");
|
||||
}
|
||||
}
|
||||
|
||||
app-vault-icon,
|
||||
.app-vault-icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
margin: 0 auto;
|
||||
width: 142px;
|
||||
height: 21px;
|
||||
background-size: 142px 21px;
|
||||
background-repeat: no-repeat;
|
||||
@include themify($themes) {
|
||||
background-image: url("../images/logo-" + themed("logoSuffix") + "@2x.png");
|
||||
}
|
||||
@media (min-width: 219px) {
|
||||
width: 189px;
|
||||
height: 28px;
|
||||
background-size: 189px 28px;
|
||||
}
|
||||
@media (min-width: 314px) {
|
||||
width: 284px;
|
||||
height: 43px;
|
||||
background-size: 284px 43px;
|
||||
}
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.draggable {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
input[type="password"]::-ms-reveal {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
|
||||
&.flex-grow {
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text selection styles
|
||||
// Set explicit selection styles (assumes primary accent color has sufficient
|
||||
// contrast against the background, so its inversion is also still readable)
|
||||
// and suppress user selection for most elements (to make it more app-like)
|
||||
|
||||
:not(bit-form-field input)::selection {
|
||||
@include themify($themes) {
|
||||
color: themed("backgroundColor");
|
||||
background-color: themed("primaryAccentColor");
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
label,
|
||||
a,
|
||||
button,
|
||||
p,
|
||||
img,
|
||||
.box-header,
|
||||
.box-footer,
|
||||
.callout,
|
||||
.row-label,
|
||||
.modal-title,
|
||||
.overlay-container {
|
||||
user-select: none;
|
||||
|
||||
&.user-select {
|
||||
user-select: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* tweak for inconsistent line heights in cipher view */
|
||||
.box-footer button,
|
||||
.box-footer a {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// Workaround for slow performance on external monitors on Chrome + MacOS
|
||||
// See: https://bugs.chromium.org/p/chromium/issues/detail?id=971701#c64
|
||||
@keyframes redraw {
|
||||
0% {
|
||||
opacity: 0.99;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
html.force_redraw {
|
||||
animation: redraw 1s linear infinite;
|
||||
}
|
||||
|
||||
/* override for vault icon in browser (pre extension refresh) */
|
||||
app-vault-icon:not(app-vault-list-items-container app-vault-icon) > div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
float: left;
|
||||
height: 36px;
|
||||
width: 34px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
@import "variables.scss";
|
||||
|
||||
app-home {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.center-content {
|
||||
margin-top: -50px;
|
||||
height: calc(100% + 50px);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 284px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
p.lead {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.btn + .btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
button.settings-icon {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("mutedColor");
|
||||
}
|
||||
|
||||
&:not(:hover):not(:focus) {
|
||||
span {
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
|
||||
@include themify($themes) {
|
||||
color: themed("primaryColor");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.body-sm,
|
||||
body.body-xs {
|
||||
app-home {
|
||||
.center-content {
|
||||
margin-top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
p.lead {
|
||||
margin: 15px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body.body-full {
|
||||
app-home {
|
||||
.center-content {
|
||||
margin-top: -80px;
|
||||
height: calc(100% + 80px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.createAccountLink {
|
||||
padding: 30px 10px 0 10px;
|
||||
}
|
||||
|
||||
.remember-email-check {
|
||||
padding-top: 18px;
|
||||
padding-left: 10px;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.login-buttons > button {
|
||||
margin: 15px 0 15px 0;
|
||||
}
|
||||
|
||||
.useBrowserlink {
|
||||
margin-left: 5px;
|
||||
margin-top: 20px;
|
||||
|
||||
span {
|
||||
font-weight: 700;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
}
|
||||
|
||||
.fido2-browser-selector-dropdown {
|
||||
@include themify($themes) {
|
||||
background-color: themed("boxBackgroundColor");
|
||||
}
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
box-shadow:
|
||||
0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||
0 3px 1px -2px rgba(0, 0, 0, 0.12),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
|
||||
.fido2-browser-selector-dropdown-item {
|
||||
@include themify($themes) {
|
||||
color: themed("textColor") !important;
|
||||
}
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0px 15px 0px 5px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
background-color: themed("listItemBackgroundHoverColor") !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Temporary fix for avatar, will not be required once we migrate to tailwind preflight **/
|
||||
bit-avatar svg {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
@import "variables.scss";
|
||||
|
||||
@each $mfaType in $mfaTypes {
|
||||
.mfaType#{$mfaType} {
|
||||
content: url("../images/two-factor/" + $mfaType + ".png");
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.mfaType1 {
|
||||
@include themify($themes) {
|
||||
content: url("../images/two-factor/1" + themed("mfaLogoSuffix"));
|
||||
max-width: 100px;
|
||||
max-height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
.mfaType7 {
|
||||
@include themify($themes) {
|
||||
content: url("../images/two-factor/7" + themed("mfaLogoSuffix"));
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,50 @@
|
||||
@import "../../../../../libs/angular/src/scss/bwicons/styles/style.scss";
|
||||
@import "variables.scss";
|
||||
@import "../../../../../libs/angular/src/scss/icons.scss";
|
||||
@import "base.scss";
|
||||
@import "grid.scss";
|
||||
@import "box.scss";
|
||||
@import "buttons.scss";
|
||||
@import "misc.scss";
|
||||
@import "environment.scss";
|
||||
@import "pages.scss";
|
||||
@import "plugins.scss";
|
||||
@import "@angular/cdk/overlay-prebuilt.css";
|
||||
@import "../../../../../libs/components/src/multi-select/scss/bw.theme";
|
||||
|
||||
.cdk-virtual-scroll-content-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// MFA Types for logo styling with no dark theme alternative
|
||||
$mfaTypes: 0, 2, 3, 4, 6;
|
||||
|
||||
@each $mfaType in $mfaTypes {
|
||||
.mfaType#{$mfaType} {
|
||||
content: url("../images/two-factor/" + $mfaType + ".png");
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.mfaType0 {
|
||||
content: url("../images/two-factor/0.png");
|
||||
max-width: 100px;
|
||||
max-height: 45px;
|
||||
}
|
||||
|
||||
.mfaType1 {
|
||||
max-width: 100px;
|
||||
max-height: 45px;
|
||||
|
||||
&:is(.theme_light *) {
|
||||
content: url("../images/two-factor/1.png");
|
||||
}
|
||||
|
||||
&:is(.theme_dark *) {
|
||||
content: url("../images/two-factor/1-w.png");
|
||||
}
|
||||
}
|
||||
|
||||
.mfaType7 {
|
||||
max-width: 100px;
|
||||
|
||||
&:is(.theme_light *) {
|
||||
content: url("../images/two-factor/7.png");
|
||||
}
|
||||
|
||||
&:is(.theme_dark *) {
|
||||
content: url("../images/two-factor/7-w.png");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,104 @@
|
||||
@import "../../../../../libs/components/src/tw-theme.css";
|
||||
@import "../../../../../libs/components/src/tw-theme-preflight.css";
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
overflow: hidden;
|
||||
min-height: 600px;
|
||||
height: 100%;
|
||||
|
||||
&.body-sm {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
&.body-xs {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
&.body-xxs {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
&.body-3xs {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
&.body-full {
|
||||
min-height: unset;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
& body {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html.browser_safari {
|
||||
&.safari_height_fix {
|
||||
body {
|
||||
height: 360px !important;
|
||||
|
||||
&.body-xs {
|
||||
height: 300px !important;
|
||||
}
|
||||
|
||||
&.body-full {
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app-root {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #000000;
|
||||
}
|
||||
|
||||
&.theme_light app-root {
|
||||
border-color: #777777;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
width: 380px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
min-height: inherit;
|
||||
overflow: hidden;
|
||||
@apply tw-bg-background-alt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for slow performance on external monitors on Chrome + MacOS
|
||||
* See: https://bugs.chromium.org/p/chromium/issues/detail?id=971701#c64
|
||||
*/
|
||||
@keyframes redraw {
|
||||
0% {
|
||||
opacity: 0.99;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
html.force_redraw {
|
||||
animation: redraw 1s linear infinite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text selection style:
|
||||
* suppress user selection for most elements (to make it more app-like)
|
||||
*/
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
label,
|
||||
a,
|
||||
button,
|
||||
p,
|
||||
img {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/** Safari Support */
|
||||
@@ -19,4 +119,59 @@
|
||||
html:not(.browser_safari) .tw-styled-scrollbar {
|
||||
scrollbar-color: rgb(var(--color-secondary-500)) rgb(var(--color-background-alt));
|
||||
}
|
||||
|
||||
#duo-frame {
|
||||
background: url("../images/loading.svg") 0 0 no-repeat;
|
||||
width: 100%;
|
||||
height: 470px;
|
||||
margin-bottom: -10px;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
#web-authn-frame {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
|
||||
iframe {
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
body.linux-webauthn {
|
||||
width: 485px !important;
|
||||
#web-authn-frame {
|
||||
iframe {
|
||||
width: 375px;
|
||||
margin: 0 55px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app-root > #loading {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@apply tw-text-muted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text selection style:
|
||||
* Set explicit selection styles (assumes primary accent color has sufficient
|
||||
* contrast against the background, so its inversion is also still readable)
|
||||
*/
|
||||
:not(bit-form-field input)::selection {
|
||||
@apply tw-text-contrast;
|
||||
@apply tw-bg-primary-700;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,178 +1,42 @@
|
||||
$dark-icon-themes: "theme_dark";
|
||||
/**
|
||||
* DEPRECATED: DO NOT MODIFY OR USE!
|
||||
*/
|
||||
|
||||
$dark-icon-themes: "theme_dark";
|
||||
|
||||
$font-family-sans-serif: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
$font-size-base: 16px;
|
||||
$font-size-large: 18px;
|
||||
$font-size-xlarge: 22px;
|
||||
$font-size-xxlarge: 28px;
|
||||
$font-size-small: 12px;
|
||||
$text-color: #000000;
|
||||
$border-color: #f0f0f0;
|
||||
$border-color-dark: #ddd;
|
||||
$list-item-hover: #fbfbfb;
|
||||
$list-icon-color: #767679;
|
||||
$disabled-box-opacity: 1;
|
||||
$border-radius: 6px;
|
||||
$line-height-base: 1.42857143;
|
||||
$icon-hover-color: lighten($text-color, 50%);
|
||||
|
||||
$mfaTypes: 0, 2, 3, 4, 6;
|
||||
|
||||
$gray: #555;
|
||||
$gray-light: #777;
|
||||
$text-muted: $gray-light;
|
||||
|
||||
$brand-primary: #175ddc;
|
||||
$brand-danger: #c83522;
|
||||
$brand-success: #017e45;
|
||||
$brand-info: #555555;
|
||||
$brand-warning: #8b6609;
|
||||
$brand-primary-accent: #1252a3;
|
||||
|
||||
$background-color: #f0f0f0;
|
||||
|
||||
$box-background-color: white;
|
||||
$box-background-hover-color: $list-item-hover;
|
||||
$box-border-color: $border-color;
|
||||
$border-color-alt: #c3c5c7;
|
||||
|
||||
$button-border-color: darken($border-color-dark, 12%);
|
||||
$button-background-color: white;
|
||||
$button-color: lighten($text-color, 40%);
|
||||
$button-color-primary: darken($brand-primary, 8%);
|
||||
$button-color-danger: darken($brand-danger, 10%);
|
||||
|
||||
$code-color: #c01176;
|
||||
$code-color-dark: #f08dc7;
|
||||
|
||||
$themes: (
|
||||
light: (
|
||||
textColor: $text-color,
|
||||
hoverColorTransparent: rgba($text-color, 0.15),
|
||||
borderColor: $border-color-dark,
|
||||
backgroundColor: $background-color,
|
||||
borderColorAlt: $border-color-alt,
|
||||
backgroundColorAlt: #ffffff,
|
||||
scrollbarColor: rgba(100, 100, 100, 0.2),
|
||||
scrollbarHoverColor: rgba(100, 100, 100, 0.4),
|
||||
boxBackgroundColor: $box-background-color,
|
||||
boxBackgroundHoverColor: $box-background-hover-color,
|
||||
boxBorderColor: $box-border-color,
|
||||
tabBackgroundColor: #ffffff,
|
||||
tabBackgroundHoverColor: $list-item-hover,
|
||||
headerColor: #ffffff,
|
||||
headerBackgroundColor: $brand-primary,
|
||||
headerBackgroundHoverColor: rgba(255, 255, 255, 0.1),
|
||||
headerBorderColor: $brand-primary,
|
||||
headerInputBackgroundColor: darken($brand-primary, 8%),
|
||||
headerInputBackgroundFocusColor: darken($brand-primary, 10%),
|
||||
headerInputColor: #ffffff,
|
||||
headerInputPlaceholderColor: lighten($brand-primary, 35%),
|
||||
listItemBackgroundHoverColor: $list-item-hover,
|
||||
disabledIconColor: $list-icon-color,
|
||||
disabledBoxOpacity: $disabled-box-opacity,
|
||||
headingColor: $gray-light,
|
||||
labelColor: $gray-light,
|
||||
mutedColor: $text-muted,
|
||||
totpStrokeColor: $brand-primary,
|
||||
boxRowButtonColor: $brand-primary,
|
||||
boxRowButtonHoverColor: darken($brand-primary, 10%),
|
||||
inputBorderColor: darken($border-color-dark, 7%),
|
||||
inputBackgroundColor: #ffffff,
|
||||
inputPlaceholderColor: lighten($gray-light, 35%),
|
||||
buttonBackgroundColor: $button-background-color,
|
||||
buttonBorderColor: $button-border-color,
|
||||
buttonColor: $button-color,
|
||||
buttonPrimaryColor: $button-color-primary,
|
||||
buttonDangerColor: $button-color-danger,
|
||||
primaryColor: $brand-primary,
|
||||
primaryAccentColor: $brand-primary-accent,
|
||||
dangerColor: $brand-danger,
|
||||
successColor: $brand-success,
|
||||
infoColor: $brand-info,
|
||||
warningColor: $brand-warning,
|
||||
logoSuffix: "dark",
|
||||
mfaLogoSuffix: ".png",
|
||||
passwordNumberColor: #007fde,
|
||||
passwordSpecialColor: #c40800,
|
||||
passwordCountText: #212529,
|
||||
calloutBorderColor: $border-color-dark,
|
||||
calloutBackgroundColor: $box-background-color,
|
||||
toastTextColor: #ffffff,
|
||||
svgSuffix: "-light.svg",
|
||||
transparentColor: rgba(0, 0, 0, 0),
|
||||
dateInputColorScheme: light,
|
||||
// https://stackoverflow.com/a/53336754
|
||||
webkitCalendarPickerFilter: invert(46%) sepia(69%) saturate(6397%) hue-rotate(211deg)
|
||||
brightness(85%) contrast(103%),
|
||||
// light has no hover so use same color
|
||||
webkitCalendarPickerHoverFilter: invert(46%) sepia(69%) saturate(6397%) hue-rotate(211deg)
|
||||
brightness(85%) contrast(103%),
|
||||
codeColor: $code-color,
|
||||
),
|
||||
dark: (
|
||||
textColor: #ffffff,
|
||||
hoverColorTransparent: rgba($text-color, 0.15),
|
||||
borderColor: #161c26,
|
||||
backgroundColor: #161c26,
|
||||
borderColorAlt: #6e788a,
|
||||
backgroundColorAlt: #2f343d,
|
||||
scrollbarColor: #6e788a,
|
||||
scrollbarHoverColor: #8d94a5,
|
||||
boxBackgroundColor: #2f343d,
|
||||
boxBackgroundHoverColor: #3c424e,
|
||||
boxBorderColor: #4c525f,
|
||||
tabBackgroundColor: #2f343d,
|
||||
tabBackgroundHoverColor: #3c424e,
|
||||
headerColor: #ffffff,
|
||||
headerBackgroundColor: #2f343d,
|
||||
headerBackgroundHoverColor: #3c424e,
|
||||
headerBorderColor: #161c26,
|
||||
headerInputBackgroundColor: #3c424e,
|
||||
headerInputBackgroundFocusColor: #4c525f,
|
||||
headerInputColor: #ffffff,
|
||||
headerInputPlaceholderColor: #bac0ce,
|
||||
listItemBackgroundHoverColor: #3c424e,
|
||||
disabledIconColor: #bac0ce,
|
||||
disabledBoxOpacity: 0.5,
|
||||
headingColor: #bac0ce,
|
||||
labelColor: #bac0ce,
|
||||
mutedColor: #bac0ce,
|
||||
totpStrokeColor: #4c525f,
|
||||
boxRowButtonColor: #bac0ce,
|
||||
boxRowButtonHoverColor: #ffffff,
|
||||
inputBorderColor: #4c525f,
|
||||
inputBackgroundColor: #2f343d,
|
||||
inputPlaceholderColor: #bac0ce,
|
||||
buttonBackgroundColor: #3c424e,
|
||||
buttonBorderColor: #4c525f,
|
||||
buttonColor: #bac0ce,
|
||||
buttonPrimaryColor: #6f9df1,
|
||||
buttonDangerColor: #ff8d85,
|
||||
primaryColor: #6f9df1,
|
||||
primaryAccentColor: #6f9df1,
|
||||
dangerColor: #ff8d85,
|
||||
successColor: #52e07c,
|
||||
infoColor: #a4b0c6,
|
||||
warningColor: #ffeb66,
|
||||
logoSuffix: "white",
|
||||
mfaLogoSuffix: "-w.png",
|
||||
passwordNumberColor: #6f9df1,
|
||||
passwordSpecialColor: #ff8d85,
|
||||
passwordCountText: #ffffff,
|
||||
calloutBorderColor: #4c525f,
|
||||
calloutBackgroundColor: #3c424e,
|
||||
toastTextColor: #1f242e,
|
||||
svgSuffix: "-dark.svg",
|
||||
transparentColor: rgba(0, 0, 0, 0),
|
||||
dateInputColorScheme: dark,
|
||||
// https://stackoverflow.com/a/53336754 - must prepend brightness(0) saturate(100%) to dark themed date inputs
|
||||
webkitCalendarPickerFilter: brightness(0) saturate(100%) invert(86%) sepia(19%) saturate(152%)
|
||||
hue-rotate(184deg) brightness(87%) contrast(93%),
|
||||
webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(100%) sepia(0%)
|
||||
saturate(0%) hue-rotate(93deg) brightness(103%) contrast(103%),
|
||||
codeColor: $code-color-dark,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ import {
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
BiometricsService,
|
||||
@@ -743,7 +744,7 @@ const safeProviders: SafeProvider[] = [
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [JslibServicesModule],
|
||||
imports: [JslibServicesModule, GeneratorServicesModule],
|
||||
declarations: [],
|
||||
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function
|
||||
providers: safeProviders,
|
||||
|
||||
@@ -12,5 +12,6 @@ config.content = [
|
||||
"../../libs/vault/src/**/*.{html,ts}",
|
||||
"../../libs/pricing/src/**/*.{html,ts}",
|
||||
];
|
||||
config.corePlugins.preflight = true;
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -113,20 +113,14 @@ export class LoginCommand {
|
||||
} else if (options.sso != null && this.canInteract) {
|
||||
// If the optional Org SSO Identifier isn't provided, the option value is `true`.
|
||||
const orgSsoIdentifier = options.sso === true ? null : options.sso;
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
const ssoPromptData = await this.makeSsoPromptData();
|
||||
ssoCodeVerifier = ssoPromptData.ssoCodeVerifier;
|
||||
try {
|
||||
const ssoParams = await this.openSsoPrompt(codeChallenge, state, orgSsoIdentifier);
|
||||
const ssoParams = await this.openSsoPrompt(
|
||||
ssoPromptData.codeChallenge,
|
||||
ssoPromptData.state,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
ssoCode = ssoParams.ssoCode;
|
||||
orgIdentifier = ssoParams.orgIdentifier;
|
||||
} catch {
|
||||
@@ -231,9 +225,43 @@ export class LoginCommand {
|
||||
new PasswordLoginCredentials(email, password, twoFactor),
|
||||
);
|
||||
}
|
||||
|
||||
// Begin Acting on initial AuthResult
|
||||
|
||||
if (response.requiresEncryptionKeyMigration) {
|
||||
return Response.error(this.i18nService.t("legacyEncryptionUnsupported"));
|
||||
}
|
||||
|
||||
// Opting for not checking feature flag since the server will not respond with
|
||||
// SsoOrganizationIdentifier if the feature flag is not enabled.
|
||||
if (response.requiresSso && this.canInteract) {
|
||||
const ssoPromptData = await this.makeSsoPromptData();
|
||||
ssoCodeVerifier = ssoPromptData.ssoCodeVerifier;
|
||||
try {
|
||||
const ssoParams = await this.openSsoPrompt(
|
||||
ssoPromptData.codeChallenge,
|
||||
ssoPromptData.state,
|
||||
response.ssoOrganizationIdentifier,
|
||||
);
|
||||
ssoCode = ssoParams.ssoCode;
|
||||
orgIdentifier = ssoParams.orgIdentifier;
|
||||
if (ssoCode != null && ssoCodeVerifier != null) {
|
||||
response = await this.loginStrategyService.logIn(
|
||||
new SsoLoginCredentials(
|
||||
ssoCode,
|
||||
ssoCodeVerifier,
|
||||
this.ssoRedirectUri,
|
||||
orgIdentifier,
|
||||
undefined, // email to look up 2FA token not required as CLI can't remember 2FA token
|
||||
twoFactor,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
return Response.badRequest("Something went wrong. Try again.");
|
||||
}
|
||||
}
|
||||
|
||||
if (response.requiresTwoFactor) {
|
||||
const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null);
|
||||
if (twoFactorProviders.length === 0) {
|
||||
@@ -279,6 +307,10 @@ export class LoginCommand {
|
||||
if (twoFactorToken == null && selectedProvider.type === TwoFactorProviderType.Email) {
|
||||
const emailReq = new TwoFactorEmailRequest();
|
||||
emailReq.email = await this.loginStrategyService.getEmail();
|
||||
// if the user was logging in with SSO, we need to include the SSO session token
|
||||
if (response.ssoEmail2FaSessionToken != null) {
|
||||
emailReq.ssoEmail2FaSessionToken = response.ssoEmail2FaSessionToken;
|
||||
}
|
||||
emailReq.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
|
||||
await this.twoFactorApiService.postTwoFactorEmail(emailReq);
|
||||
}
|
||||
@@ -324,6 +356,7 @@ export class LoginCommand {
|
||||
response = await this.loginStrategyService.logInNewDeviceVerification(newDeviceToken);
|
||||
}
|
||||
|
||||
// We check response two factor again here since MFA could fail based on the logic on ln 226
|
||||
if (response.requiresTwoFactor) {
|
||||
return Response.error("Login failed.");
|
||||
}
|
||||
@@ -692,6 +725,27 @@ export class LoginCommand {
|
||||
};
|
||||
}
|
||||
|
||||
/// Generate SSO prompt data: code verifier, code challenge, and state
|
||||
private async makeSsoPromptData(): Promise<{
|
||||
ssoCodeVerifier: string;
|
||||
codeChallenge: string;
|
||||
state: string;
|
||||
}> {
|
||||
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);
|
||||
return { ssoCodeVerifier, codeChallenge, state };
|
||||
}
|
||||
|
||||
private async openSsoPrompt(
|
||||
codeChallenge: string,
|
||||
state: string,
|
||||
|
||||
@@ -492,10 +492,7 @@ export class ServiceContainer {
|
||||
|
||||
const pinStateService = new PinStateService(this.stateProvider);
|
||||
this.pinService = new PinService(
|
||||
this.accountService,
|
||||
this.encryptService,
|
||||
this.kdfConfigService,
|
||||
this.keyGenerationService,
|
||||
this.logService,
|
||||
this.keyService,
|
||||
this.sdkService,
|
||||
@@ -908,7 +905,7 @@ export class ServiceContainer {
|
||||
this.collectionService,
|
||||
this.keyService,
|
||||
this.encryptService,
|
||||
this.pinService,
|
||||
this.keyGenerationService,
|
||||
this.accountService,
|
||||
this.restrictedItemTypesService,
|
||||
);
|
||||
@@ -916,7 +913,7 @@ export class ServiceContainer {
|
||||
this.individualExportService = new IndividualVaultExportService(
|
||||
this.folderService,
|
||||
this.cipherService,
|
||||
this.pinService,
|
||||
this.keyGenerationService,
|
||||
this.keyService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
@@ -930,7 +927,7 @@ export class ServiceContainer {
|
||||
this.organizationExportService = new OrganizationVaultExportService(
|
||||
this.cipherService,
|
||||
this.vaultExportApiService,
|
||||
this.pinService,
|
||||
this.keyGenerationService,
|
||||
this.keyService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
|
||||
394
apps/desktop/desktop_native/Cargo.lock
generated
394
apps/desktop/desktop_native/Cargo.lock
generated
@@ -2,21 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
@@ -114,9 +99,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.94"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
@@ -138,14 +123,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df"
|
||||
checksum = "da0986d5b4f0802160191ad75f8d33ada000558757db3defb70299ca95d9fcbd"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tokio",
|
||||
@@ -347,23 +332,8 @@ dependencies = [
|
||||
"mockall",
|
||||
"serial_test",
|
||||
"tracing",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-targets 0.52.6",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -457,7 +427,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -501,6 +471,12 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@@ -509,9 +485,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
@@ -556,9 +532,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.46"
|
||||
version = "1.2.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
|
||||
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -614,7 +590,7 @@ dependencies = [
|
||||
"hex",
|
||||
"oo7",
|
||||
"pbkdf2",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"rusqlite",
|
||||
"security-framework",
|
||||
"serde",
|
||||
@@ -623,7 +599,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"verifysign",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -709,9 +685,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
@@ -770,16 +746,6 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.5.0"
|
||||
@@ -867,7 +833,7 @@ dependencies = [
|
||||
"memsec",
|
||||
"oo7",
|
||||
"pin-project",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"scopeguard",
|
||||
"secmem-proc",
|
||||
"security-framework",
|
||||
@@ -877,13 +843,13 @@ dependencies = [
|
||||
"sha2",
|
||||
"ssh-key",
|
||||
"sysinfo",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"typenum",
|
||||
"widestring",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
"windows-future",
|
||||
"zbus",
|
||||
"zbus_polkit",
|
||||
@@ -1409,17 +1375,11 @@ dependencies = [
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "goblin"
|
||||
@@ -1499,14 +1459,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "homedir"
|
||||
version = "0.3.4"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2"
|
||||
checksum = "68df315d2857b2d8d2898be54a85e1d001bbbe0dbb5f8ef847b48dd3a23c4527"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"nix 0.29.0",
|
||||
"nix",
|
||||
"widestring",
|
||||
"windows 0.57.0",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1663,6 +1623,16 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -1685,7 +1655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.53.3",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1841,15 +1811,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.3"
|
||||
@@ -1889,32 +1850,33 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "2.16.17"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3"
|
||||
checksum = "f1b74e3dce5230795bb4d2821b941706dee733c7308752507254b0497f39cad7"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ctor 0.2.9",
|
||||
"napi-derive",
|
||||
"ctor",
|
||||
"napi-build",
|
||||
"napi-sys",
|
||||
"once_cell",
|
||||
"nohash-hasher",
|
||||
"rustc-hash",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.2.0"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03acbfa4f156a32188bfa09b86dc11a431b5725253fc1fc6f6df5bed273382c4"
|
||||
checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "2.16.13"
|
||||
version = "3.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
|
||||
checksum = "7552d5a579b834614bbd496db5109f1b9f1c758f08224b0dee1e408333adf0d0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"convert_case",
|
||||
"ctor",
|
||||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1923,40 +1885,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "1.0.75"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
|
||||
checksum = "5f6a81ac7486b70f2532a289603340862c06eea5a1e650c1ffeda2ce1238516a"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"semver",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "2.4.0"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
|
||||
checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
@@ -1970,6 +1918,12 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -2173,15 +2127,6 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -2190,9 +2135,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "oo7"
|
||||
version = "0.4.3"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb23d3ec3527d65a83be1c1795cb883c52cfa57147d42acc797127df56fc489"
|
||||
checksum = "e3299dd401feaf1d45afd8fd1c0586f10fcfb22f244bb9afa942cec73503b89d"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"ashpd",
|
||||
@@ -2208,7 +2153,7 @@ dependencies = [
|
||||
"num",
|
||||
"num-bigint-dig",
|
||||
"pbkdf2",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
@@ -2548,7 +2493,7 @@ dependencies = [
|
||||
name = "process_isolation"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"ctor 0.5.0",
|
||||
"ctor",
|
||||
"desktop_core",
|
||||
"libc",
|
||||
"tracing",
|
||||
@@ -2591,9 +2536,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
@@ -2660,19 +2605,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2748,10 +2681,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
@@ -2798,6 +2731,12 @@ dependencies = [
|
||||
"rustix 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
@@ -2870,8 +2809,8 @@ dependencies = [
|
||||
"libc",
|
||||
"rustix 1.0.7",
|
||||
"rustix-linux-procfs",
|
||||
"thiserror 2.0.12",
|
||||
"windows 0.61.1",
|
||||
"thiserror 2.0.17",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3068,12 +3007,12 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.9"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
|
||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3197,7 +3136,7 @@ dependencies = [
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3239,11 +3178,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.12",
|
||||
"thiserror-impl 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3259,9 +3198,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3289,11 +3228,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.0"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
@@ -3303,14 +3241,14 @@ dependencies = [
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3319,9 +3257,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.13"
|
||||
version = "0.7.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
|
||||
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -3680,6 +3618,17 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -3745,6 +3694,51 @@ dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.10"
|
||||
@@ -3852,16 +3846,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
|
||||
dependencies = [
|
||||
"windows-core 0.57.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.1"
|
||||
@@ -3869,7 +3853,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.61.0",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics",
|
||||
@@ -3881,19 +3865,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
|
||||
dependencies = [
|
||||
"windows-implement 0.57.0",
|
||||
"windows-interface 0.57.0",
|
||||
"windows-result 0.1.2",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3902,8 +3874,8 @@ version = "0.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.0",
|
||||
"windows-interface 0.59.1",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
@@ -3915,21 +3887,10 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-core",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
@@ -3941,17 +3902,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
@@ -3981,7 +3931,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-core",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
@@ -3996,15 +3946,6 @@ dependencies = [
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
@@ -4262,8 +4203,8 @@ name = "windows_plugin_authenticator"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4434,9 +4375,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.11.0"
|
||||
version = "5.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7"
|
||||
checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
@@ -4452,14 +4393,15 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"nix 0.30.1",
|
||||
"nix",
|
||||
"ordered-stream",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys 0.60.2",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
@@ -4468,9 +4410,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.11.0"
|
||||
version = "5.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca"
|
||||
checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
|
||||
@@ -21,13 +21,13 @@ publish = false
|
||||
[workspace.dependencies]
|
||||
aes = "=0.8.4"
|
||||
aes-gcm = "=0.10.3"
|
||||
anyhow = "=1.0.94"
|
||||
anyhow = "=1.0.100"
|
||||
arboard = { version = "=3.6.1", default-features = false }
|
||||
ashpd = "=0.11.0"
|
||||
ashpd = "=0.12.0"
|
||||
base64 = "=0.22.1"
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" }
|
||||
byteorder = "=1.5.0"
|
||||
bytes = "=1.10.1"
|
||||
bytes = "=1.11.0"
|
||||
cbc = "=0.1.2"
|
||||
chacha20poly1305 = "=0.10.1"
|
||||
core-foundation = "=0.10.1"
|
||||
@@ -37,18 +37,18 @@ ed25519 = "=2.2.3"
|
||||
embed_plist = "=1.2.2"
|
||||
futures = "=0.3.31"
|
||||
hex = "=0.4.3"
|
||||
homedir = "=0.3.4"
|
||||
homedir = "=0.3.6"
|
||||
interprocess = "=2.2.1"
|
||||
libc = "=0.2.178"
|
||||
linux-keyutils = "=0.2.4"
|
||||
memsec = "=0.7.0"
|
||||
napi = "=2.16.17"
|
||||
napi-build = "=2.2.0"
|
||||
napi-derive = "=2.16.13"
|
||||
oo7 = "=0.4.3"
|
||||
napi = "=3.3.0"
|
||||
napi-build = "=2.2.3"
|
||||
napi-derive = "=3.2.5"
|
||||
oo7 = "=0.5.0"
|
||||
pin-project = "=1.1.10"
|
||||
pkcs8 = "=0.10.2"
|
||||
rand = "=0.9.1"
|
||||
rand = "=0.9.2"
|
||||
rsa = "=0.9.6"
|
||||
russh-cryptovec = "=0.7.3"
|
||||
scopeguard = "=1.2.0"
|
||||
@@ -61,9 +61,9 @@ sha2 = "=0.10.8"
|
||||
ssh-encoding = "=0.2.0"
|
||||
ssh-key = { version = "=0.6.7", default-features = false }
|
||||
sysinfo = "=0.37.2"
|
||||
thiserror = "=2.0.12"
|
||||
tokio = "=1.45.0"
|
||||
tokio-util = "=0.7.13"
|
||||
thiserror = "=2.0.17"
|
||||
tokio = "=1.48.0"
|
||||
tokio-util = "=0.7.17"
|
||||
tracing = "=0.1.41"
|
||||
tracing-subscriber = { version = "=0.3.20", features = [
|
||||
"fmt",
|
||||
@@ -77,7 +77,7 @@ windows = "=0.61.1"
|
||||
windows-core = "=0.61.0"
|
||||
windows-future = "=0.2.0"
|
||||
windows-registry = "=0.6.1"
|
||||
zbus = "=5.11.0"
|
||||
zbus = "=5.12.0"
|
||||
zbus_polkit = "=5.0.0"
|
||||
zeroizing-alloc = "=0.1.0"
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ const rustTargetsMap = {
|
||||
"aarch64-pc-windows-msvc": { nodeArch: 'arm64', platform: 'win32' },
|
||||
"x86_64-apple-darwin": { nodeArch: 'x64', platform: 'darwin' },
|
||||
"aarch64-apple-darwin": { nodeArch: 'arm64', platform: 'darwin' },
|
||||
'x86_64-unknown-linux-musl': { nodeArch: 'x64', platform: 'linux' },
|
||||
'aarch64-unknown-linux-musl': { nodeArch: 'arm64', platform: 'linux' },
|
||||
'x86_64-unknown-linux-gnu': { nodeArch: 'x64', platform: 'linux' },
|
||||
'aarch64-unknown-linux-gnu': { nodeArch: 'arm64', platform: 'linux' },
|
||||
}
|
||||
|
||||
// Ensure the dist directory exists
|
||||
|
||||
@@ -7,9 +7,9 @@ pub struct NativeImporterMetadata {
|
||||
/// Identifies the importer
|
||||
pub id: String,
|
||||
/// Describes the strategies used to obtain imported data
|
||||
pub loaders: Vec<&'static str>,
|
||||
pub loaders: Vec<String>,
|
||||
/// Identifies the instructions for the importer
|
||||
pub instructions: &'static str,
|
||||
pub instructions: String,
|
||||
}
|
||||
|
||||
/// Returns a map of supported importers based on the current platform.
|
||||
@@ -36,9 +36,9 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
|
||||
PLATFORM_SUPPORTED_BROWSERS.iter().map(|b| b.name).collect();
|
||||
|
||||
for (id, browser_name) in IMPORTERS {
|
||||
let mut loaders: Vec<&'static str> = vec!["file"];
|
||||
let mut loaders: Vec<String> = vec!["file".to_string()];
|
||||
if supported.contains(browser_name) {
|
||||
loaders.push("chromium");
|
||||
loaders.push("chromium".to_string());
|
||||
}
|
||||
|
||||
if installed_browsers.contains(&browser_name.to_string()) {
|
||||
@@ -47,7 +47,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
|
||||
NativeImporterMetadata {
|
||||
id: id.to_string(),
|
||||
loaders,
|
||||
instructions: "chromium",
|
||||
instructions: "chromium".to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -79,12 +79,9 @@ mod tests {
|
||||
map.keys().cloned().collect()
|
||||
}
|
||||
|
||||
fn get_loaders(
|
||||
map: &HashMap<String, NativeImporterMetadata>,
|
||||
id: &str,
|
||||
) -> HashSet<&'static str> {
|
||||
fn get_loaders(map: &HashMap<String, NativeImporterMetadata>, id: &str) -> HashSet<String> {
|
||||
map.get(id)
|
||||
.map(|m| m.loaders.iter().copied().collect::<HashSet<_>>())
|
||||
.map(|m| m.loaders.iter().cloned().collect::<HashSet<_>>())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -107,7 +104,7 @@ mod tests {
|
||||
for (key, meta) in map.iter() {
|
||||
assert_eq!(&meta.id, key);
|
||||
assert_eq!(meta.instructions, "chromium");
|
||||
assert!(meta.loaders.contains(&"file"));
|
||||
assert!(meta.loaders.contains(&"file".to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +144,7 @@ mod tests {
|
||||
for (key, meta) in map.iter() {
|
||||
assert_eq!(&meta.id, key);
|
||||
assert_eq!(meta.instructions, "chromium");
|
||||
assert!(meta.loaders.contains(&"file"));
|
||||
assert!(meta.loaders.contains(&"file".to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +180,7 @@ mod tests {
|
||||
for (key, meta) in map.iter() {
|
||||
assert_eq!(&meta.id, key);
|
||||
assert_eq!(meta.instructions, "chromium");
|
||||
assert!(meta.loaders.contains(&"file"));
|
||||
assert!(meta.loaders.contains(&"file".to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
431
apps/desktop/desktop_native/napi/index.d.ts
vendored
431
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -1,125 +1,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
export declare namespace passwords {
|
||||
/** The error message returned when a password is not found during retrieval or deletion. */
|
||||
export const PASSWORD_NOT_FOUND: string
|
||||
/**
|
||||
* Fetch the stored password from the keychain.
|
||||
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
|
||||
*/
|
||||
export function getPassword(service: string, account: string): Promise<string>
|
||||
/**
|
||||
* Save the password to the keychain. Adds an entry if none exists otherwise updates the
|
||||
* existing entry.
|
||||
*/
|
||||
export function setPassword(service: string, account: string, password: string): Promise<void>
|
||||
/**
|
||||
* Delete the stored password from the keychain.
|
||||
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
|
||||
*/
|
||||
export function deletePassword(service: string, account: string): Promise<void>
|
||||
/** Checks if the os secure storage is available */
|
||||
export function isAvailable(): Promise<boolean>
|
||||
}
|
||||
export declare namespace biometrics {
|
||||
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
|
||||
export function available(): Promise<boolean>
|
||||
export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise<string>
|
||||
/**
|
||||
* Retrieves the biometric secret for the given service and account.
|
||||
* Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist.
|
||||
*/
|
||||
export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise<string>
|
||||
/**
|
||||
* Derives key material from biometric data. Returns a string encoded with a
|
||||
* base64 encoded key and the base64 encoded challenge used to create it
|
||||
* separated by a `|` character.
|
||||
*
|
||||
* If the iv is provided, it will be used as the challenge. Otherwise a random challenge will
|
||||
* be generated.
|
||||
*
|
||||
* `format!("<key_base64>|<iv_base64>")`
|
||||
*/
|
||||
export function deriveKeyMaterial(iv?: string | undefined | null): Promise<OsDerivedKey>
|
||||
export interface KeyMaterial {
|
||||
osKeyPartB64: string
|
||||
clientKeyPartB64?: string
|
||||
}
|
||||
export interface OsDerivedKey {
|
||||
keyB64: string
|
||||
ivB64: string
|
||||
}
|
||||
}
|
||||
export declare namespace biometrics_v2 {
|
||||
export function initBiometricSystem(): BiometricLockSystem
|
||||
export function authenticate(biometricLockSystem: BiometricLockSystem, hwnd: Buffer, message: string): Promise<boolean>
|
||||
export function authenticateAvailable(biometricLockSystem: BiometricLockSystem): Promise<boolean>
|
||||
export function enrollPersistent(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
|
||||
export function provideKey(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
|
||||
export function unlock(biometricLockSystem: BiometricLockSystem, userId: string, hwnd: Buffer): Promise<Buffer>
|
||||
export function unlockAvailable(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
|
||||
export function hasPersistent(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
|
||||
export function unenroll(biometricLockSystem: BiometricLockSystem, userId: string): Promise<void>
|
||||
export class BiometricLockSystem { }
|
||||
}
|
||||
export declare namespace clipboards {
|
||||
export function read(): Promise<string>
|
||||
export function write(text: string, password: boolean): Promise<void>
|
||||
}
|
||||
export declare namespace sshagent {
|
||||
export interface PrivateKey {
|
||||
privateKey: string
|
||||
name: string
|
||||
cipherId: string
|
||||
}
|
||||
export interface SshKey {
|
||||
privateKey: string
|
||||
publicKey: string
|
||||
keyFingerprint: string
|
||||
}
|
||||
export interface SshUiRequest {
|
||||
cipherId?: string
|
||||
isList: boolean
|
||||
processName: string
|
||||
isForwarding: boolean
|
||||
namespace?: string
|
||||
}
|
||||
export function serve(callback: (err: Error | null, arg: SshUiRequest) => any): Promise<SshAgentState>
|
||||
export function stop(agentState: SshAgentState): void
|
||||
export function isRunning(agentState: SshAgentState): boolean
|
||||
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
|
||||
export function lock(agentState: SshAgentState): void
|
||||
export function clearKeys(agentState: SshAgentState): void
|
||||
export class SshAgentState { }
|
||||
}
|
||||
export declare namespace processisolations {
|
||||
export function disableCoredumps(): Promise<void>
|
||||
export function isCoreDumpingDisabled(): Promise<boolean>
|
||||
export function isolateProcess(): Promise<void>
|
||||
}
|
||||
export declare namespace powermonitors {
|
||||
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
||||
export function isLockMonitorAvailable(): Promise<boolean>
|
||||
}
|
||||
export declare namespace windows_registry {
|
||||
export function createKey(key: string, subkey: string, value: string): Promise<void>
|
||||
export function deleteKey(key: string, subkey: string): Promise<void>
|
||||
}
|
||||
export declare namespace ipc {
|
||||
export interface IpcMessage {
|
||||
clientId: number
|
||||
kind: IpcMessageType
|
||||
message?: string
|
||||
}
|
||||
export const enum IpcMessageType {
|
||||
Connected = 0,
|
||||
Disconnected = 1,
|
||||
Message = 2
|
||||
}
|
||||
export class IpcServer {
|
||||
/* eslint-disable */
|
||||
export declare namespace autofill {
|
||||
export class AutofillIpcServer {
|
||||
/**
|
||||
* Create and start the IPC server without blocking.
|
||||
*
|
||||
@@ -127,34 +9,43 @@ export declare namespace ipc {
|
||||
* connection and must be the same for both the server and client. @param callback
|
||||
* This function will be called whenever a message is received from a client.
|
||||
*/
|
||||
static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise<IpcServer>
|
||||
static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void, nativeStatusCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void): Promise<AutofillIpcServer>
|
||||
/** Return the path to the IPC server. */
|
||||
getPath(): string
|
||||
/** Stop the IPC server. */
|
||||
stop(): void
|
||||
/**
|
||||
* Send a message over the IPC server to all the connected clients
|
||||
*
|
||||
* @return The number of clients that the message was sent to. Note that the number of
|
||||
* messages actually received may be less, as some clients could disconnect before
|
||||
* receiving the message.
|
||||
*/
|
||||
send(message: string): number
|
||||
completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number
|
||||
completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number
|
||||
completeError(clientId: number, sequenceNumber: number, error: string): number
|
||||
}
|
||||
}
|
||||
export declare namespace autostart {
|
||||
export function setAutostart(autostart: boolean, params: Array<string>): Promise<void>
|
||||
}
|
||||
export declare namespace autofill {
|
||||
export function runCommand(value: string): Promise<string>
|
||||
export const enum UserVerification {
|
||||
Preferred = 'preferred',
|
||||
Required = 'required',
|
||||
Discouraged = 'discouraged'
|
||||
export interface NativeStatus {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
export interface Position {
|
||||
x: number
|
||||
y: number
|
||||
export interface PasskeyAssertionRequest {
|
||||
rpId: string
|
||||
clientDataHash: Array<number>
|
||||
userVerification: UserVerification
|
||||
allowedCredentials: Array<Array<number>>
|
||||
windowXy: Position
|
||||
}
|
||||
export interface PasskeyAssertionResponse {
|
||||
rpId: string
|
||||
userHandle: Array<number>
|
||||
signature: Array<number>
|
||||
clientDataHash: Array<number>
|
||||
authenticatorData: Array<number>
|
||||
credentialId: Array<number>
|
||||
}
|
||||
export interface PasskeyAssertionWithoutUserInterfaceRequest {
|
||||
rpId: string
|
||||
credentialId: Array<number>
|
||||
userName: string
|
||||
userHandle: Array<number>
|
||||
recordIdentifier?: string
|
||||
clientDataHash: Array<number>
|
||||
userVerification: UserVerification
|
||||
windowXy: Position
|
||||
}
|
||||
export interface PasskeyRegistrationRequest {
|
||||
rpId: string
|
||||
@@ -172,71 +63,77 @@ export declare namespace autofill {
|
||||
credentialId: Array<number>
|
||||
attestationObject: Array<number>
|
||||
}
|
||||
export interface PasskeyAssertionRequest {
|
||||
rpId: string
|
||||
clientDataHash: Array<number>
|
||||
userVerification: UserVerification
|
||||
allowedCredentials: Array<Array<number>>
|
||||
windowXy: Position
|
||||
export interface Position {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
export interface PasskeyAssertionWithoutUserInterfaceRequest {
|
||||
rpId: string
|
||||
credentialId: Array<number>
|
||||
userName: string
|
||||
userHandle: Array<number>
|
||||
recordIdentifier?: string
|
||||
clientDataHash: Array<number>
|
||||
userVerification: UserVerification
|
||||
windowXy: Position
|
||||
}
|
||||
export interface NativeStatus {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
export interface PasskeyAssertionResponse {
|
||||
rpId: string
|
||||
userHandle: Array<number>
|
||||
signature: Array<number>
|
||||
clientDataHash: Array<number>
|
||||
authenticatorData: Array<number>
|
||||
credentialId: Array<number>
|
||||
}
|
||||
export class IpcServer {
|
||||
/**
|
||||
* Create and start the IPC server without blocking.
|
||||
*
|
||||
* @param name The endpoint name to listen on. This name uniquely identifies the IPC
|
||||
* connection and must be the same for both the server and client. @param callback
|
||||
* This function will be called whenever a message is received from a client.
|
||||
*/
|
||||
static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void, nativeStatusCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void): Promise<IpcServer>
|
||||
/** Return the path to the IPC server. */
|
||||
getPath(): string
|
||||
/** Stop the IPC server. */
|
||||
stop(): void
|
||||
completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number
|
||||
completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number
|
||||
completeError(clientId: number, sequenceNumber: number, error: string): number
|
||||
export function runCommand(value: string): Promise<string>
|
||||
export const enum UserVerification {
|
||||
Preferred = 'preferred',
|
||||
Required = 'required',
|
||||
Discouraged = 'discouraged'
|
||||
}
|
||||
}
|
||||
export declare namespace passkey_authenticator {
|
||||
export function register(): void
|
||||
|
||||
export declare namespace autostart {
|
||||
export function setAutostart(autostart: boolean, params: Array<string>): Promise<void>
|
||||
}
|
||||
export declare namespace logging {
|
||||
export const enum LogLevel {
|
||||
Trace = 0,
|
||||
Debug = 1,
|
||||
Info = 2,
|
||||
Warn = 3,
|
||||
Error = 4
|
||||
|
||||
export declare namespace autotype {
|
||||
export function getForegroundWindowTitle(): string
|
||||
export function typeInput(input: Array<number>, keyboardShortcut: Array<string>): void
|
||||
}
|
||||
|
||||
export declare namespace biometrics {
|
||||
export function available(): Promise<boolean>
|
||||
/**
|
||||
* Derives key material from biometric data. Returns a string encoded with a
|
||||
* base64 encoded key and the base64 encoded challenge used to create it
|
||||
* separated by a `|` character.
|
||||
*
|
||||
* If the iv is provided, it will be used as the challenge. Otherwise a random challenge will
|
||||
* be generated.
|
||||
*
|
||||
* `format!("<key_base64>|<iv_base64>")`
|
||||
*/
|
||||
export function deriveKeyMaterial(iv?: string | undefined | null): Promise<OsDerivedKey>
|
||||
/**
|
||||
* Retrieves the biometric secret for the given service and account.
|
||||
* Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist.
|
||||
*/
|
||||
export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise<string>
|
||||
export interface KeyMaterial {
|
||||
osKeyPartB64: string
|
||||
clientKeyPartB64?: string
|
||||
}
|
||||
export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void
|
||||
export interface OsDerivedKey {
|
||||
keyB64: string
|
||||
ivB64: string
|
||||
}
|
||||
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
|
||||
export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise<string>
|
||||
}
|
||||
|
||||
export declare namespace biometrics_v2 {
|
||||
export class BiometricLockSystem {
|
||||
|
||||
}
|
||||
export function authenticate(biometricLockSystem: BiometricLockSystem, hwnd: Buffer, message: string): Promise<boolean>
|
||||
export function authenticateAvailable(biometricLockSystem: BiometricLockSystem): Promise<boolean>
|
||||
export function enrollPersistent(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
|
||||
export function hasPersistent(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
|
||||
export function initBiometricSystem(): BiometricLockSystem
|
||||
export function provideKey(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
|
||||
export function unenroll(biometricLockSystem: BiometricLockSystem, userId: string): Promise<void>
|
||||
export function unlock(biometricLockSystem: BiometricLockSystem, userId: string, hwnd: Buffer): Promise<Buffer>
|
||||
export function unlockAvailable(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
|
||||
}
|
||||
|
||||
export declare namespace chromium_importer {
|
||||
export interface ProfileInfo {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
|
||||
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
|
||||
export function getMetadata(): Record<string, NativeImporterMetadata>
|
||||
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
||||
export interface Login {
|
||||
url: string
|
||||
username: string
|
||||
@@ -257,12 +154,130 @@ export declare namespace chromium_importer {
|
||||
loaders: Array<string>
|
||||
instructions: string
|
||||
}
|
||||
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
|
||||
export function getMetadata(): Record<string, NativeImporterMetadata>
|
||||
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
|
||||
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
||||
export interface ProfileInfo {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
export declare namespace autotype {
|
||||
export function getForegroundWindowTitle(): string
|
||||
export function typeInput(input: Array<number>, keyboardShortcut: Array<string>): void
|
||||
|
||||
export declare namespace clipboards {
|
||||
export function read(): Promise<string>
|
||||
export function write(text: string, password: boolean): Promise<void>
|
||||
}
|
||||
|
||||
export declare namespace ipc {
|
||||
export class NativeIpcServer {
|
||||
/**
|
||||
* Create and start the IPC server without blocking.
|
||||
*
|
||||
* @param name The endpoint name to listen on. This name uniquely identifies the IPC
|
||||
* connection and must be the same for both the server and client. @param callback
|
||||
* This function will be called whenever a message is received from a client.
|
||||
*/
|
||||
static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise<NativeIpcServer>
|
||||
/** Return the path to the IPC server. */
|
||||
getPath(): string
|
||||
/** Stop the IPC server. */
|
||||
stop(): void
|
||||
/**
|
||||
* Send a message over the IPC server to all the connected clients
|
||||
*
|
||||
* @return The number of clients that the message was sent to. Note that the number of
|
||||
* messages actually received may be less, as some clients could disconnect before
|
||||
* receiving the message.
|
||||
*/
|
||||
send(message: string): number
|
||||
}
|
||||
export interface IpcMessage {
|
||||
clientId: number
|
||||
kind: IpcMessageType
|
||||
message?: string
|
||||
}
|
||||
export const enum IpcMessageType {
|
||||
Connected = 0,
|
||||
Disconnected = 1,
|
||||
Message = 2
|
||||
}
|
||||
}
|
||||
|
||||
export declare namespace logging {
|
||||
export function initNapiLog(jsLogFn: ((err: Error | null, arg0: LogLevel, arg1: string) => any)): void
|
||||
export const enum LogLevel {
|
||||
Trace = 0,
|
||||
Debug = 1,
|
||||
Info = 2,
|
||||
Warn = 3,
|
||||
Error = 4
|
||||
}
|
||||
}
|
||||
|
||||
export declare namespace passkey_authenticator {
|
||||
export function register(): void
|
||||
}
|
||||
|
||||
export declare namespace passwords {
|
||||
/**
|
||||
* Delete the stored password from the keychain.
|
||||
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
|
||||
*/
|
||||
export function deletePassword(service: string, account: string): Promise<void>
|
||||
/**
|
||||
* Fetch the stored password from the keychain.
|
||||
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
|
||||
*/
|
||||
export function getPassword(service: string, account: string): Promise<string>
|
||||
/** Checks if the os secure storage is available */
|
||||
export function isAvailable(): Promise<boolean>
|
||||
/** The error message returned when a password is not found during retrieval or deletion. */
|
||||
export const PASSWORD_NOT_FOUND: string
|
||||
/**
|
||||
* Save the password to the keychain. Adds an entry if none exists otherwise updates the
|
||||
* existing entry.
|
||||
*/
|
||||
export function setPassword(service: string, account: string, password: string): Promise<void>
|
||||
}
|
||||
|
||||
export declare namespace powermonitors {
|
||||
export function isLockMonitorAvailable(): Promise<boolean>
|
||||
export function onLock(callback: ((err: Error | null, ) => any)): Promise<void>
|
||||
}
|
||||
|
||||
export declare namespace processisolations {
|
||||
export function disableCoredumps(): Promise<void>
|
||||
export function isCoreDumpingDisabled(): Promise<boolean>
|
||||
export function isolateProcess(): Promise<void>
|
||||
}
|
||||
|
||||
export declare namespace sshagent {
|
||||
export class SshAgentState {
|
||||
|
||||
}
|
||||
export function clearKeys(agentState: SshAgentState): void
|
||||
export function isRunning(agentState: SshAgentState): boolean
|
||||
export function lock(agentState: SshAgentState): void
|
||||
export interface PrivateKey {
|
||||
privateKey: string
|
||||
name: string
|
||||
cipherId: string
|
||||
}
|
||||
export function serve(callback: ((err: Error | null, arg: SshUiRequest) => Promise<boolean>)): Promise<SshAgentState>
|
||||
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
|
||||
export interface SshKey {
|
||||
privateKey: string
|
||||
publicKey: string
|
||||
keyFingerprint: string
|
||||
}
|
||||
export interface SshUiRequest {
|
||||
cipherId?: string
|
||||
isList: boolean
|
||||
processName: string
|
||||
isForwarding: boolean
|
||||
namespace?: string
|
||||
}
|
||||
export function stop(agentState: SshAgentState): void
|
||||
}
|
||||
|
||||
export declare namespace windows_registry {
|
||||
export function createKey(key: string, subkey: string, value: string): Promise<void>
|
||||
export function deleteKey(key: string, subkey: string): Promise<void>
|
||||
}
|
||||
|
||||
@@ -82,20 +82,20 @@ switch (platform) {
|
||||
switch (arch) {
|
||||
case "x64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-x64-musl.node", "desktop_napi.linux-x64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-x64-musl",
|
||||
["desktop_napi.linux-x64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-x64-gnu",
|
||||
);
|
||||
break;
|
||||
case "arm64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-arm64-musl.node", "desktop_napi.linux-arm64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm64-musl",
|
||||
["desktop_napi.linux-arm64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm64-gnu",
|
||||
);
|
||||
break;
|
||||
case "arm":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-arm-musl.node", "desktop_napi.linux-arm-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm-musl",
|
||||
["desktop_napi.linux-arm-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm-gnu",
|
||||
);
|
||||
localFileExisted = existsSync(join(__dirname, "desktop_napi.linux-arm-gnueabihf.node"));
|
||||
try {
|
||||
|
||||
@@ -3,27 +3,23 @@
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "napi build --platform --js false",
|
||||
"build": "napi build --platform --no-js",
|
||||
"test": "cargo test"
|
||||
},
|
||||
"author": "",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "2.18.4"
|
||||
"@napi-rs/cli": "3.2.0"
|
||||
},
|
||||
"napi": {
|
||||
"name": "desktop_napi",
|
||||
"triples": {
|
||||
"defaults": true,
|
||||
"additional": [
|
||||
"x86_64-unknown-linux-musl",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"i686-pc-windows-msvc",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"aarch64-pc-windows-msvc"
|
||||
]
|
||||
}
|
||||
"binaryName": "desktop_napi",
|
||||
"targets": [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"i686-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ pub mod sshagent {
|
||||
|
||||
use napi::{
|
||||
bindgen_prelude::Promise,
|
||||
threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction},
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
};
|
||||
use tokio::{self, sync::Mutex};
|
||||
use tracing::error;
|
||||
@@ -326,13 +326,15 @@ pub mod sshagent {
|
||||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||||
#[napi]
|
||||
pub async fn serve(
|
||||
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>,
|
||||
callback: ThreadsafeFunction<SshUIRequest, Promise<bool>>,
|
||||
) -> napi::Result<SshAgentState> {
|
||||
let (auth_request_tx, mut auth_request_rx) =
|
||||
tokio::sync::mpsc::channel::<desktop_core::ssh_agent::SshAgentUIRequest>(32);
|
||||
let (auth_response_tx, auth_response_rx) =
|
||||
tokio::sync::broadcast::channel::<(u32, bool)>(32);
|
||||
let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx));
|
||||
// Wrap callback in Arc so it can be shared across spawned tasks
|
||||
let callback = Arc::new(callback);
|
||||
tokio::spawn(async move {
|
||||
let _ = auth_response_rx;
|
||||
|
||||
@@ -342,42 +344,50 @@ pub mod sshagent {
|
||||
tokio::spawn(async move {
|
||||
let auth_response_tx_arc = cloned_response_tx_arc;
|
||||
let callback = cloned_callback;
|
||||
let promise_result: Result<Promise<bool>, napi::Error> = callback
|
||||
.call_async(Ok(SshUIRequest {
|
||||
// In NAPI v3, obtain the JS callback return as a Promise<boolean> and await it
|
||||
// in Rust
|
||||
let (tx, rx) = std::sync::mpsc::channel::<Promise<bool>>();
|
||||
let status = callback.call_with_return_value(
|
||||
Ok(SshUIRequest {
|
||||
cipher_id: request.cipher_id,
|
||||
is_list: request.is_list,
|
||||
process_name: request.process_name,
|
||||
is_forwarding: request.is_forwarding,
|
||||
namespace: request.namespace,
|
||||
}))
|
||||
.await;
|
||||
match promise_result {
|
||||
Ok(promise_result) => match promise_result.await {
|
||||
Ok(result) => {
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request.request_id, result))
|
||||
.expect("should be able to send auth response to agent");
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "Calling UI callback promise was rejected");
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request.request_id, false))
|
||||
.expect("should be able to send auth response to agent");
|
||||
}),
|
||||
ThreadsafeFunctionCallMode::Blocking,
|
||||
move |ret: Result<Promise<bool>, napi::Error>, _env| {
|
||||
if let Ok(p) = ret {
|
||||
let _ = tx.send(p);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
error!(error = %e, "Calling UI callback could not create promise");
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request.request_id, false))
|
||||
.expect("should be able to send auth response to agent");
|
||||
);
|
||||
|
||||
let result = if status == napi::Status::Ok {
|
||||
match rx.recv() {
|
||||
Ok(promise) => match promise.await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(error = %e, "UI callback promise rejected");
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!(error = %e, "Failed to receive UI callback promise");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!(error = ?status, "Calling UI callback failed");
|
||||
false
|
||||
};
|
||||
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request.request_id, result))
|
||||
.expect("should be able to send auth response to agent");
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -465,14 +475,12 @@ pub mod processisolations {
|
||||
#[napi]
|
||||
pub mod powermonitors {
|
||||
use napi::{
|
||||
threadsafe_function::{
|
||||
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
},
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
tokio,
|
||||
};
|
||||
|
||||
#[napi]
|
||||
pub async fn on_lock(callback: ThreadsafeFunction<(), CalleeHandled>) -> napi::Result<()> {
|
||||
pub async fn on_lock(callback: ThreadsafeFunction<()>) -> napi::Result<()> {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32);
|
||||
desktop_core::powermonitor::on_lock(tx)
|
||||
.await
|
||||
@@ -511,9 +519,7 @@ pub mod windows_registry {
|
||||
#[napi]
|
||||
pub mod ipc {
|
||||
use desktop_core::ipc::server::{Message, MessageType};
|
||||
use napi::threadsafe_function::{
|
||||
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
};
|
||||
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
|
||||
|
||||
#[napi(object)]
|
||||
pub struct IpcMessage {
|
||||
@@ -550,12 +556,12 @@ pub mod ipc {
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct IpcServer {
|
||||
pub struct NativeIpcServer {
|
||||
server: desktop_core::ipc::server::Server,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl IpcServer {
|
||||
impl NativeIpcServer {
|
||||
/// Create and start the IPC server without blocking.
|
||||
///
|
||||
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC
|
||||
@@ -566,7 +572,7 @@ pub mod ipc {
|
||||
pub async fn listen(
|
||||
name: String,
|
||||
#[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")]
|
||||
callback: ThreadsafeFunction<IpcMessage, ErrorStrategy::CalleeHandled>,
|
||||
callback: ThreadsafeFunction<IpcMessage>,
|
||||
) -> napi::Result<Self> {
|
||||
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
|
||||
tokio::spawn(async move {
|
||||
@@ -583,7 +589,7 @@ pub mod ipc {
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(IpcServer { server })
|
||||
Ok(NativeIpcServer { server })
|
||||
}
|
||||
|
||||
/// Return the path to the IPC server.
|
||||
@@ -630,8 +636,9 @@ pub mod autostart {
|
||||
#[napi]
|
||||
pub mod autofill {
|
||||
use desktop_core::ipc::server::{Message, MessageType};
|
||||
use napi::threadsafe_function::{
|
||||
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
use napi::{
|
||||
bindgen_prelude::FnArgs,
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use tracing::error;
|
||||
@@ -746,14 +753,14 @@ pub mod autofill {
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct IpcServer {
|
||||
pub struct AutofillIpcServer {
|
||||
server: desktop_core::ipc::server::Server,
|
||||
}
|
||||
|
||||
// FIXME: Remove unwraps! They panic and terminate the whole application.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[napi]
|
||||
impl IpcServer {
|
||||
impl AutofillIpcServer {
|
||||
/// Create and start the IPC server without blocking.
|
||||
///
|
||||
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC
|
||||
@@ -769,30 +776,24 @@ pub mod autofill {
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void"
|
||||
)]
|
||||
registration_callback: ThreadsafeFunction<
|
||||
(u32, u32, PasskeyRegistrationRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
FnArgs<(u32, u32, PasskeyRegistrationRequest)>,
|
||||
>,
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void"
|
||||
)]
|
||||
assertion_callback: ThreadsafeFunction<
|
||||
(u32, u32, PasskeyAssertionRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
FnArgs<(u32, u32, PasskeyAssertionRequest)>,
|
||||
>,
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void"
|
||||
)]
|
||||
assertion_without_user_interface_callback: ThreadsafeFunction<
|
||||
(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
FnArgs<(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest)>,
|
||||
>,
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void"
|
||||
)]
|
||||
native_status_callback: ThreadsafeFunction<
|
||||
(u32, u32, NativeStatus),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
>,
|
||||
native_status_callback: ThreadsafeFunction<(u32, u32, NativeStatus)>,
|
||||
) -> napi::Result<Self> {
|
||||
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
|
||||
tokio::spawn(async move {
|
||||
@@ -817,7 +818,7 @@ pub mod autofill {
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map(|value| (client_id, msg.sequence_number, value).into())
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
|
||||
assertion_callback
|
||||
@@ -836,7 +837,7 @@ pub mod autofill {
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map(|value| (client_id, msg.sequence_number, value).into())
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
|
||||
assertion_without_user_interface_callback
|
||||
@@ -854,7 +855,7 @@ pub mod autofill {
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map(|value| (client_id, msg.sequence_number, value).into())
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
registration_callback
|
||||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
@@ -894,7 +895,7 @@ pub mod autofill {
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(IpcServer { server })
|
||||
Ok(AutofillIpcServer { server })
|
||||
}
|
||||
|
||||
/// Return the path to the IPC server.
|
||||
@@ -987,8 +988,9 @@ pub mod logging {
|
||||
|
||||
use std::{fmt::Write, sync::OnceLock};
|
||||
|
||||
use napi::threadsafe_function::{
|
||||
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
use napi::{
|
||||
bindgen_prelude::FnArgs,
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
};
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::{
|
||||
@@ -999,7 +1001,7 @@ pub mod logging {
|
||||
Layer,
|
||||
};
|
||||
|
||||
struct JsLogger(OnceLock<ThreadsafeFunction<(LogLevel, String), CalleeHandled>>);
|
||||
struct JsLogger(OnceLock<ThreadsafeFunction<FnArgs<(LogLevel, String)>>>);
|
||||
static JS_LOGGER: JsLogger = JsLogger(OnceLock::new());
|
||||
|
||||
#[napi]
|
||||
@@ -1071,13 +1073,13 @@ pub mod logging {
|
||||
let msg = (event.metadata().level().into(), buffer);
|
||||
|
||||
if let Some(logger) = JS_LOGGER.0.get() {
|
||||
let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
let _ = logger.call(Ok(msg.into()), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) {
|
||||
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<FnArgs<(LogLevel, String)>>) {
|
||||
let _ = JS_LOGGER.0.set(js_log_fn);
|
||||
|
||||
let filter = EnvFilter::builder()
|
||||
@@ -1140,8 +1142,8 @@ pub mod chromium_importer {
|
||||
#[napi(object)]
|
||||
pub struct NativeImporterMetadata {
|
||||
pub id: String,
|
||||
pub loaders: Vec<&'static str>,
|
||||
pub instructions: &'static str,
|
||||
pub loaders: Vec<String>,
|
||||
pub instructions: String,
|
||||
}
|
||||
|
||||
impl From<_LoginImportResult> for LoginImportResult {
|
||||
@@ -1218,7 +1220,7 @@ pub mod chromium_importer {
|
||||
#[napi]
|
||||
pub mod autotype {
|
||||
#[napi]
|
||||
pub fn get_foreground_window_title() -> napi::Result<String, napi::Status> {
|
||||
pub fn get_foreground_window_title() -> napi::Result<String> {
|
||||
autotype::get_foreground_window_title().map_err(|_| {
|
||||
napi::Error::from_reason(
|
||||
"Autotype Error: failed to get foreground window title".to_string(),
|
||||
|
||||
@@ -14,8 +14,8 @@ tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
cc = "=1.2.46"
|
||||
glob = "=0.3.2"
|
||||
cc = "=1.2.49"
|
||||
glob = "=0.3.3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.19.1",
|
||||
"@types/node": "22.19.2",
|
||||
"typescript": "5.4.2"
|
||||
}
|
||||
},
|
||||
@@ -117,9 +117,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
|
||||
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
|
||||
"version": "22.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz",
|
||||
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.19.1",
|
||||
"@types/node": "22.19.2",
|
||||
"typescript": "5.4.2"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
|
||||
@@ -42,14 +42,17 @@ import {
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
|
||||
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
|
||||
import {
|
||||
LockComponent,
|
||||
ConfirmKeyConnectorDomainComponent,
|
||||
RemovePasswordComponent,
|
||||
} from "@bitwarden/key-management-ui";
|
||||
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { reactiveUnlockVaultGuard } from "../autofill/guards/reactive-vault-guard";
|
||||
import { Fido2CreateComponent } from "../autofill/modal/credentials/fido2-create.component";
|
||||
import { Fido2ExcludedCiphersComponent } from "../autofill/modal/credentials/fido2-excluded-ciphers.component";
|
||||
import { Fido2VaultComponent } from "../autofill/modal/credentials/fido2-vault.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
|
||||
import { VaultComponent } from "../vault/app/vault-v3/vault.component";
|
||||
|
||||
@@ -117,11 +120,6 @@ const routes: Routes = [
|
||||
component: SendComponent,
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
{
|
||||
path: "remove-password",
|
||||
component: RemovePasswordComponent,
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
{
|
||||
path: "fido2-assertion",
|
||||
component: Fido2VaultComponent,
|
||||
@@ -327,13 +325,24 @@ const routes: Routes = [
|
||||
pageIcon: LockIcon,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "remove-password",
|
||||
component: RemovePasswordComponent,
|
||||
canActivate: [authGuard],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "verifyYourOrganization",
|
||||
},
|
||||
pageIcon: LockIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "confirm-key-connector-domain",
|
||||
component: ConfirmKeyConnectorDomainComponent,
|
||||
canActivate: [],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "confirmKeyConnectorDomain",
|
||||
key: "verifyYourOrganization",
|
||||
},
|
||||
pageIcon: DomainIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
|
||||
@@ -15,7 +15,6 @@ import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { LoginModule } from "../auth/login/login.module";
|
||||
import { SshAgentService } from "../autofill/services/ssh-agent.service";
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module";
|
||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
|
||||
|
||||
@@ -50,7 +49,6 @@ import { SharedModule } from "./shared/shared.module";
|
||||
ColorPasswordCountPipe,
|
||||
HeaderComponent,
|
||||
PremiumComponent,
|
||||
RemovePasswordComponent,
|
||||
SearchComponent,
|
||||
],
|
||||
providers: [SshAgentService],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n"></bit-nav-logo>
|
||||
|
||||
<bit-nav-item icon="bwi-vault" [text]="'vault' | i18n" route="new-vault"></bit-nav-item>
|
||||
<bit-nav-item icon="bwi-send" [text]="'send' | i18n" route="new-sends"></bit-nav-item>
|
||||
<app-send-filters-nav></app-send-filters-nav>
|
||||
</app-side-nav>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
@@ -5,8 +6,18 @@ import { mock } from "jest-mock-extended";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
|
||||
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
|
||||
|
||||
import { DesktopLayoutComponent } from "./desktop-layout.component";
|
||||
|
||||
// Mock the child component to isolate DesktopLayoutComponent testing
|
||||
@Component({
|
||||
selector: "app-send-filters-nav",
|
||||
template: "",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockSendFiltersNavComponent {}
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
@@ -34,7 +45,12 @@ describe("DesktopLayoutComponent", () => {
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
.overrideComponent(DesktopLayoutComponent, {
|
||||
remove: { imports: [SendFiltersNavComponent] },
|
||||
add: { imports: [MockSendFiltersNavComponent] },
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DesktopLayoutComponent);
|
||||
component = fixture.componentInstance;
|
||||
@@ -58,4 +74,11 @@ describe("DesktopLayoutComponent", () => {
|
||||
|
||||
expect(ngContent).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders send filters navigation component", () => {
|
||||
const compiled = fixture.nativeElement;
|
||||
const sendFiltersNav = compiled.querySelector("app-send-filters-nav");
|
||||
|
||||
expect(sendFiltersNav).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,13 +5,22 @@ import { PasswordManagerLogo } from "@bitwarden/assets/svg";
|
||||
import { LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
|
||||
|
||||
import { DesktopSideNavComponent } from "./desktop-side-nav.component";
|
||||
|
||||
// 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-layout",
|
||||
imports: [RouterModule, I18nPipe, LayoutComponent, NavigationModule, DesktopSideNavComponent],
|
||||
imports: [
|
||||
RouterModule,
|
||||
I18nPipe,
|
||||
LayoutComponent,
|
||||
NavigationModule,
|
||||
DesktopSideNavComponent,
|
||||
SendFiltersNavComponent,
|
||||
],
|
||||
templateUrl: "./desktop-layout.component.html",
|
||||
})
|
||||
export class DesktopLayoutComponent {
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
} from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
@@ -102,6 +103,7 @@ import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/s
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
KdfConfigService,
|
||||
@@ -166,12 +168,12 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: BiometricsService,
|
||||
useClass: RendererBiometricsService,
|
||||
deps: [],
|
||||
deps: [TokenService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DesktopBiometricsService,
|
||||
useClass: RendererBiometricsService,
|
||||
deps: [],
|
||||
deps: [TokenService],
|
||||
}),
|
||||
safeProvider(NativeMessagingService),
|
||||
safeProvider(BiometricMessageHandlerService),
|
||||
@@ -499,7 +501,7 @@ const safeProviders: SafeProvider[] = [
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [JslibServicesModule],
|
||||
imports: [JslibServicesModule, GeneratorServicesModule],
|
||||
declarations: [],
|
||||
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function
|
||||
providers: safeProviders,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<bit-nav-group
|
||||
icon="bwi-send"
|
||||
[text]="'send' | i18n"
|
||||
route="new-sends"
|
||||
(click)="selectTypeAndNavigate()"
|
||||
>
|
||||
<bit-nav-item
|
||||
icon="bwi-send"
|
||||
[text]="'allSends' | i18n"
|
||||
(click)="selectTypeAndNavigate(null); $event.stopPropagation()"
|
||||
[forceActiveStyles]="activeSendType() === null"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-file-text"
|
||||
[text]="'sendTypeText' | i18n"
|
||||
(click)="selectTypeAndNavigate(SendType.Text); $event.stopPropagation()"
|
||||
[forceActiveStyles]="activeSendType() === SendType.Text"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-file"
|
||||
[text]="'sendTypeFile' | i18n"
|
||||
(click)="selectTypeAndNavigate(SendType.File); $event.stopPropagation()"
|
||||
[forceActiveStyles]="activeSendType() === SendType.File"
|
||||
></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
@@ -0,0 +1,204 @@
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { Router, provideRouter } from "@angular/router";
|
||||
import { RouterTestingHarness } from "@angular/router/testing";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { SendListFiltersService } from "@bitwarden/send-ui";
|
||||
|
||||
import { SendFiltersNavComponent } from "./send-filters-nav.component";
|
||||
|
||||
@Component({ template: "", changeDetection: ChangeDetectionStrategy.OnPush })
|
||||
class DummyComponent {}
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: true,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
describe("SendFiltersNavComponent", () => {
|
||||
let component: SendFiltersNavComponent;
|
||||
let fixture: ComponentFixture<SendFiltersNavComponent>;
|
||||
let harness: RouterTestingHarness;
|
||||
let filterFormValueSubject: BehaviorSubject<{ sendType: SendType | null }>;
|
||||
let mockSendListFiltersService: Partial<SendListFiltersService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
filterFormValueSubject = new BehaviorSubject<{ sendType: SendType | null }>({
|
||||
sendType: null,
|
||||
});
|
||||
|
||||
mockSendListFiltersService = {
|
||||
filterForm: {
|
||||
value: { sendType: null },
|
||||
valueChanges: filterFormValueSubject.asObservable(),
|
||||
patchValue: jest.fn((value) => {
|
||||
mockSendListFiltersService.filterForm.value = {
|
||||
...mockSendListFiltersService.filterForm.value,
|
||||
...value,
|
||||
};
|
||||
filterFormValueSubject.next(mockSendListFiltersService.filterForm.value);
|
||||
}),
|
||||
} as any,
|
||||
filters$: filterFormValueSubject.asObservable(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SendFiltersNavComponent, NavigationModule],
|
||||
providers: [
|
||||
provideRouter([
|
||||
{ path: "vault", component: DummyComponent },
|
||||
{ path: "new-sends", component: DummyComponent },
|
||||
]),
|
||||
{
|
||||
provide: SendListFiltersService,
|
||||
useValue: mockSendListFiltersService,
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn((key) => key),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
// Create harness and navigate to initial route
|
||||
harness = await RouterTestingHarness.create("/vault");
|
||||
|
||||
// Create the component fixture separately (not a routed component)
|
||||
fixture = TestBed.createComponent(SendFiltersNavComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("creates component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders bit-nav-group with Send icon and text", () => {
|
||||
const compiled = fixture.nativeElement;
|
||||
const navGroup = compiled.querySelector("bit-nav-group");
|
||||
|
||||
expect(navGroup).toBeTruthy();
|
||||
expect(navGroup.getAttribute("icon")).toBe("bwi-send");
|
||||
});
|
||||
|
||||
it("component exposes SendType enum for template", () => {
|
||||
expect(component["SendType"]).toBe(SendType);
|
||||
});
|
||||
|
||||
describe("isSendRouteActive", () => {
|
||||
it("returns true when on /new-sends route", async () => {
|
||||
await harness.navigateByUrl("/new-sends");
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["isSendRouteActive"]()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when not on /new-sends route", () => {
|
||||
expect(component["isSendRouteActive"]()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("activeSendType", () => {
|
||||
it("returns the active send type when on send route and filter type is set", async () => {
|
||||
await harness.navigateByUrl("/new-sends");
|
||||
mockSendListFiltersService.filterForm.value = { sendType: SendType.Text };
|
||||
filterFormValueSubject.next({ sendType: SendType.Text });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["activeSendType"]()).toBe(SendType.Text);
|
||||
});
|
||||
|
||||
it("returns undefined when not on send route", () => {
|
||||
mockSendListFiltersService.filterForm.value = { sendType: SendType.Text };
|
||||
filterFormValueSubject.next({ sendType: SendType.Text });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["activeSendType"]()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns null when on send route but no type is selected", async () => {
|
||||
await harness.navigateByUrl("/new-sends");
|
||||
mockSendListFiltersService.filterForm.value = { sendType: null };
|
||||
filterFormValueSubject.next({ sendType: null });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["activeSendType"]()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectTypeAndNavigate", () => {
|
||||
it("clears the sendType filter when called with no parameter", async () => {
|
||||
await component["selectTypeAndNavigate"]();
|
||||
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("updates filter form with Text type", async () => {
|
||||
await component["selectTypeAndNavigate"](SendType.Text);
|
||||
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: SendType.Text,
|
||||
});
|
||||
});
|
||||
|
||||
it("updates filter form with File type", async () => {
|
||||
await component["selectTypeAndNavigate"](SendType.File);
|
||||
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: SendType.File,
|
||||
});
|
||||
});
|
||||
|
||||
it("navigates to /new-sends when not on send route", async () => {
|
||||
expect(harness.routeNativeElement?.textContent).toBeDefined();
|
||||
|
||||
await component["selectTypeAndNavigate"](SendType.Text);
|
||||
|
||||
const currentUrl = TestBed.inject(Router).url;
|
||||
expect(currentUrl).toBe("/new-sends");
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: SendType.Text,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not navigate when already on send route (component is reactive)", async () => {
|
||||
await harness.navigateByUrl("/new-sends");
|
||||
const router = TestBed.inject(Router);
|
||||
const navigateSpy = jest.spyOn(router, "navigate");
|
||||
|
||||
await component["selectTypeAndNavigate"](SendType.Text);
|
||||
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: SendType.Text,
|
||||
});
|
||||
});
|
||||
|
||||
it("navigates when clearing filter from different route", async () => {
|
||||
await component["selectTypeAndNavigate"](); // No parameter = clear filter
|
||||
|
||||
const currentUrl = TestBed.inject(Router).url;
|
||||
expect(currentUrl).toBe("/new-sends");
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, computed, inject } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import { filter, map, startWith } from "rxjs";
|
||||
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { SendListFiltersService } from "@bitwarden/send-ui";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
/**
|
||||
* Navigation component that renders Send filter options in the sidebar.
|
||||
* Fully reactive using signals - no manual subscriptions or method-based computed values.
|
||||
* - Parent "Send" nav-group clears filter (shows all sends)
|
||||
* - Child "Text"/"File" items set filter to specific type
|
||||
* - Active states computed reactively from filter signal + route signal
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-send-filters-nav",
|
||||
templateUrl: "./send-filters-nav.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, NavigationModule, I18nPipe],
|
||||
})
|
||||
export class SendFiltersNavComponent {
|
||||
protected readonly SendType = SendType;
|
||||
private readonly filtersService = inject(SendListFiltersService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly currentFilter = toSignal(this.filtersService.filters$);
|
||||
|
||||
// Track whether current route is the send route
|
||||
private readonly isSendRouteActive = toSignal(
|
||||
this.router.events.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map((event) => (event as NavigationEnd).urlAfterRedirects.includes("/new-sends")),
|
||||
startWith(this.router.url.includes("/new-sends")),
|
||||
),
|
||||
{ initialValue: this.router.url.includes("/new-sends") },
|
||||
);
|
||||
|
||||
// Computed: Active send type (null when on send route with no filter, undefined when not on send route)
|
||||
protected readonly activeSendType = computed(() => {
|
||||
return this.isSendRouteActive() ? this.currentFilter()?.sendType : undefined;
|
||||
});
|
||||
|
||||
// Update send filter and navigate to /new-sends (only if not already there - send-v2 component reacts to filter changes)
|
||||
protected async selectTypeAndNavigate(type?: SendType): Promise<void> {
|
||||
this.filtersService.filterForm.patchValue({ sendType: type !== undefined ? type : null });
|
||||
|
||||
if (!this.router.url.includes("/new-sends")) {
|
||||
await this.router.navigate(["/new-sends"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ChangeDetectorRef } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
@@ -15,6 +19,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { SendListFiltersService } from "@bitwarden/send-ui";
|
||||
|
||||
import * as utils from "../../../utils";
|
||||
import { SearchBarService } from "../../layout/search/search-bar.service";
|
||||
@@ -35,6 +40,8 @@ describe("SendV2Component", () => {
|
||||
let broadcasterService: MockProxy<BroadcasterService>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let sendListFiltersService: SendListFiltersService;
|
||||
let changeDetectorRef: MockProxy<ChangeDetectorRef>;
|
||||
|
||||
beforeEach(async () => {
|
||||
sendService = mock<SendService>();
|
||||
@@ -42,6 +49,13 @@ describe("SendV2Component", () => {
|
||||
broadcasterService = mock<BroadcasterService>();
|
||||
accountService = mock<AccountService>();
|
||||
policyService = mock<PolicyService>();
|
||||
changeDetectorRef = mock<ChangeDetectorRef>();
|
||||
|
||||
// Create real SendListFiltersService with mocked dependencies
|
||||
const formBuilder = new FormBuilder();
|
||||
const i18nService = mock<I18nService>();
|
||||
i18nService.t.mockImplementation((key: string) => key);
|
||||
sendListFiltersService = new SendListFiltersService(i18nService, formBuilder);
|
||||
|
||||
// Mock sendViews$ observable
|
||||
sendService.sendViews$ = of([]);
|
||||
@@ -51,6 +65,10 @@ describe("SendV2Component", () => {
|
||||
accountService.activeAccount$ = of({ id: "test-user-id" } as any);
|
||||
policyService.policyAppliesToUser$ = jest.fn().mockReturnValue(of(false));
|
||||
|
||||
// Mock SearchService methods needed by base component
|
||||
const mockSearchService = mock<SearchService>();
|
||||
mockSearchService.isSearchable.mockResolvedValue(false);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SendV2Component],
|
||||
providers: [
|
||||
@@ -59,7 +77,7 @@ describe("SendV2Component", () => {
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{ provide: EnvironmentService, useValue: mock<EnvironmentService>() },
|
||||
{ provide: BroadcasterService, useValue: broadcasterService },
|
||||
{ provide: SearchService, useValue: mock<SearchService>() },
|
||||
{ provide: SearchService, useValue: mockSearchService },
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
{ provide: SearchBarService, useValue: searchBarService },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
@@ -67,6 +85,8 @@ describe("SendV2Component", () => {
|
||||
{ provide: DialogService, useValue: mock<DialogService>() },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: SendListFiltersService, useValue: sendListFiltersService },
|
||||
{ provide: ChangeDetectorRef, useValue: changeDetectorRef },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -331,7 +351,6 @@ describe("SendV2Component", () => {
|
||||
describe("load", () => {
|
||||
it("sets loading states correctly", async () => {
|
||||
jest.spyOn(component, "search").mockResolvedValue();
|
||||
jest.spyOn(component, "selectAll");
|
||||
|
||||
expect(component.loaded).toBeFalsy();
|
||||
|
||||
@@ -341,14 +360,17 @@ describe("SendV2Component", () => {
|
||||
expect(component.loaded).toBe(true);
|
||||
});
|
||||
|
||||
it("calls selectAll when onSuccessfulLoad is not set", async () => {
|
||||
it("sets up sendViews$ subscription", async () => {
|
||||
const mockSends = [new SendView(), new SendView()];
|
||||
sendService.sendViews$ = of(mockSends);
|
||||
jest.spyOn(component, "search").mockResolvedValue();
|
||||
jest.spyOn(component, "selectAll");
|
||||
component.onSuccessfulLoad = null;
|
||||
|
||||
await component.load();
|
||||
|
||||
expect(component.selectAll).toHaveBeenCalled();
|
||||
// Give observable time to emit
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
expect(component.sends).toEqual(mockSends);
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLoad when it is set", async () => {
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit, OnDestroy, ViewChild, NgZone, ChangeDetectorRef } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { mergeMap } from "rxjs";
|
||||
import { mergeMap, Subscription } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
|
||||
@@ -14,11 +15,13 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
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 { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { SendListFiltersService } from "@bitwarden/send-ui";
|
||||
|
||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||
import { SearchBarService } from "../../layout/search/search-bar.service";
|
||||
@@ -55,6 +58,9 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
// Tracks the current UI state: viewing list (None), adding new Send (Add), or editing existing Send (Edit)
|
||||
action: Action = Action.None;
|
||||
|
||||
// Subscription for sendViews$ cleanup
|
||||
private sendViewsSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
sendService: SendService,
|
||||
i18nService: I18nService,
|
||||
@@ -71,6 +77,7 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
toastService: ToastService,
|
||||
accountService: AccountService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private sendListFiltersService: SendListFiltersService,
|
||||
) {
|
||||
super(
|
||||
sendService,
|
||||
@@ -88,12 +95,17 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
);
|
||||
|
||||
// Listen to search bar changes and update the Send list filter
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.searchBarService.searchText$.subscribe((searchText) => {
|
||||
this.searchBarService.searchText$.pipe(takeUntilDestroyed()).subscribe((searchText) => {
|
||||
this.searchText = searchText;
|
||||
this.searchTextChanged();
|
||||
setTimeout(() => this.cdr.detectChanges(), 250);
|
||||
});
|
||||
|
||||
// Listen to filter changes from sidebar navigation
|
||||
this.sendListFiltersService.filterForm.valueChanges
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((filters) => {
|
||||
this.applySendTypeFilter(filters);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize the component: enable search bar, subscribe to sync events, and load Send items
|
||||
@@ -103,6 +115,10 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
|
||||
await super.ngOnInit();
|
||||
|
||||
// Read current filter synchronously to avoid race condition on navigation
|
||||
const currentFilter = this.sendListFiltersService.filterForm.value;
|
||||
this.applySendTypeFilter(currentFilter);
|
||||
|
||||
// Listen for sync completion events to refresh the Send list
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
@@ -118,8 +134,18 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
await this.load();
|
||||
}
|
||||
|
||||
// Apply send type filter to display: centralized logic for initial load and filter changes
|
||||
private applySendTypeFilter(filters: Partial<{ sendType: SendType | null }>): void {
|
||||
if (filters.sendType === null || filters.sendType === undefined) {
|
||||
this.selectAll();
|
||||
} else {
|
||||
this.selectType(filters.sendType);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up subscriptions and disable search bar when component is destroyed
|
||||
ngOnDestroy() {
|
||||
this.sendViewsSubscription?.unsubscribe();
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
this.searchBarService.setEnabled(false);
|
||||
}
|
||||
@@ -130,7 +156,12 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
// Note: The filter parameter is ignored in this implementation for desktop-specific behavior.
|
||||
async load(filter: (send: SendView) => boolean = null) {
|
||||
this.loading = true;
|
||||
this.sendService.sendViews$
|
||||
|
||||
// Recreate subscription on each load (required for sync refresh)
|
||||
// Manual cleanup in ngOnDestroy is intentional - load() is called multiple times
|
||||
this.sendViewsSubscription?.unsubscribe();
|
||||
|
||||
this.sendViewsSubscription = this.sendService.sendViews$
|
||||
.pipe(
|
||||
mergeMap(async (sends) => {
|
||||
this.sends = sends;
|
||||
@@ -143,9 +174,6 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
.subscribe();
|
||||
if (this.onSuccessfulLoad != null) {
|
||||
await this.onSuccessfulLoad();
|
||||
} else {
|
||||
// Default action
|
||||
this.selectAll();
|
||||
}
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
|
||||
@@ -136,6 +136,7 @@ describe("DesktopLoginComponentService", () => {
|
||||
codeChallenge,
|
||||
state,
|
||||
email,
|
||||
undefined,
|
||||
);
|
||||
} else {
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
|
||||
@@ -145,4 +146,55 @@ describe("DesktopLoginComponentService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("redirectToSsoLoginWithOrganizationSsoIdentifier", () => {
|
||||
// Array of all permutations of isAppImage and isDev
|
||||
const permutations = [
|
||||
[true, false], // Case 1: isAppImage true
|
||||
[false, true], // Case 2: isDev true
|
||||
[true, true], // Case 3: all true
|
||||
[false, false], // Case 4: all false
|
||||
];
|
||||
|
||||
permutations.forEach(([isAppImage, isDev]) => {
|
||||
it("calls redirectToSso with orgSsoIdentifier", async () => {
|
||||
(global as any).ipc.platform.isAppImage = isAppImage;
|
||||
(global as any).ipc.platform.isDev = isDev;
|
||||
|
||||
const email = "test@bitwarden.com";
|
||||
const state = "testState";
|
||||
const codeVerifier = "testCodeVerifier";
|
||||
const codeChallenge = "testCodeChallenge";
|
||||
const orgSsoIdentifier = "orgSsoId";
|
||||
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
|
||||
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
|
||||
|
||||
await service.redirectToSsoLoginWithOrganizationSsoIdentifier(email, orgSsoIdentifier);
|
||||
|
||||
if (isAppImage || isDev) {
|
||||
expect(ipc.platform.localhostCallbackService.openSsoPrompt).toHaveBeenCalledWith(
|
||||
codeChallenge,
|
||||
state,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
} else {
|
||||
expect(ssoUrlService.buildSsoUrl).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
|
||||
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
|
||||
expect(platformUtilsService.launchUri).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,11 +48,12 @@ export class DesktopLoginComponentService
|
||||
email: string,
|
||||
state: string,
|
||||
codeChallenge: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> {
|
||||
// For platforms that cannot support a protocol-based (e.g. bitwarden://) callback, we use a localhost callback
|
||||
// Otherwise, we launch the SSO component in a browser window and wait for the callback
|
||||
if (ipc.platform.isAppImage || ipc.platform.isDev) {
|
||||
await this.initiateSsoThroughLocalhostCallback(email, state, codeChallenge);
|
||||
await this.initiateSsoThroughLocalhostCallback(email, state, codeChallenge, orgSsoIdentifier);
|
||||
} else {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
@@ -66,6 +67,7 @@ export class DesktopLoginComponentService
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
|
||||
this.platformUtilsService.launchUri(ssoWebAppUrl);
|
||||
@@ -76,9 +78,15 @@ export class DesktopLoginComponentService
|
||||
email: string,
|
||||
state: string,
|
||||
challenge: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(challenge, state, email);
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(
|
||||
challenge,
|
||||
state,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (err) {
|
||||
|
||||
@@ -37,7 +37,7 @@ export class MainSshAgentService {
|
||||
init() {
|
||||
// handle sign request passing to UI
|
||||
sshagent
|
||||
.serve(async (err: Error, sshUiRequest: sshagent.SshUiRequest) => {
|
||||
.serve(async (err: Error | null, sshUiRequest: sshagent.SshUiRequest): Promise<boolean> => {
|
||||
// clear all old (> SIGN_TIMEOUT) requests
|
||||
this.requestResponses = this.requestResponses.filter(
|
||||
(response) => response.timestamp > new Date(Date.now() - this.SIGN_TIMEOUT),
|
||||
|
||||
@@ -43,9 +43,7 @@ export type NativeWindowObject = {
|
||||
windowXy?: { x: number; y: number };
|
||||
};
|
||||
|
||||
export class DesktopFido2UserInterfaceService
|
||||
implements Fido2UserInterfaceServiceAbstraction<NativeWindowObject>
|
||||
{
|
||||
export class DesktopFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction<NativeWindowObject> {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private cipherService: CipherService,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
@@ -13,6 +15,10 @@ import { DesktopBiometricsService } from "./desktop.biometrics.service";
|
||||
*/
|
||||
@Injectable()
|
||||
export class RendererBiometricsService extends DesktopBiometricsService {
|
||||
constructor(private tokenService: TokenService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async authenticateWithBiometrics(): Promise<boolean> {
|
||||
return await ipc.keyManagement.biometric.authenticateWithBiometrics();
|
||||
}
|
||||
@@ -31,6 +37,10 @@ export class RendererBiometricsService extends DesktopBiometricsService {
|
||||
}
|
||||
|
||||
async getBiometricsStatusForUser(id: UserId): Promise<BiometricsStatus> {
|
||||
if ((await firstValueFrom(this.tokenService.hasAccessToken$(id))) === false) {
|
||||
return BiometricsStatus.NotEnabledInConnectedDesktopApp;
|
||||
}
|
||||
|
||||
return await ipc.keyManagement.biometric.getBiometricsStatusForUser(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<div id="remove-password-page" *ngIf="!loading">
|
||||
<div class="content">
|
||||
<h1>{{ "removeMasterPassword" | i18n }}</h1>
|
||||
<p>{{ "removeMasterPasswordForOrganizationUserKeyConnector" | i18n }}</p>
|
||||
<p class="tw-mb-0">{{ "organizationName" | i18n }}:</p>
|
||||
<p class="tw-text-muted tw-mb-6">{{ organization.name }}</p>
|
||||
<p class="tw-mb-0">{{ "keyConnectorDomain" | i18n }}:</p>
|
||||
<p class="tw-text-muted tw-mb-6">{{ organization.keyConnectorUrl }}</p>
|
||||
<div class="buttons">
|
||||
<button type="submit" class="btn primary block" [disabled]="action" (click)="convert()">
|
||||
<b [hidden]="continuing">{{ "removeMasterPassword" | i18n }}</b>
|
||||
<i class="bwi bwi-spinner bwi-spin" [hidden]="!continuing" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn secondary block" [disabled]="action" (click)="leave()">
|
||||
<b [hidden]="leaving">{{ "leaveOrganization" | i18n }}</b>
|
||||
<i class="bwi bwi-spinner bwi-spin" [hidden]="!leaving" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
// 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-remove-password",
|
||||
templateUrl: "remove-password.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}
|
||||
@@ -708,6 +708,9 @@
|
||||
"addAttachment": {
|
||||
"message": "Add attachment"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Fix encryption"
|
||||
},
|
||||
@@ -2634,9 +2637,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Master password removed"
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Organization name"
|
||||
},
|
||||
@@ -4334,6 +4334,45 @@
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector":{
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified":{
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified":{
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
@@ -4383,5 +4422,53 @@
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { isDev } from "../utils";
|
||||
import { WindowMain } from "./window.main";
|
||||
|
||||
export class NativeMessagingMain {
|
||||
private ipcServer: ipc.IpcServer | null;
|
||||
private ipcServer: ipc.NativeIpcServer | null;
|
||||
private connected: number[] = [];
|
||||
|
||||
constructor(
|
||||
@@ -78,7 +78,7 @@ export class NativeMessagingMain {
|
||||
this.ipcServer.stop();
|
||||
}
|
||||
|
||||
this.ipcServer = await ipc.IpcServer.listen("bw", (error, msg) => {
|
||||
this.ipcServer = await ipc.NativeIpcServer.listen("bw", (error, msg) => {
|
||||
switch (msg.kind) {
|
||||
case ipc.IpcMessageType.Connected: {
|
||||
this.connected.push(msg.clientId);
|
||||
|
||||
@@ -21,7 +21,7 @@ export type RunCommandParams<C extends CommandDefinition> = {
|
||||
export type RunCommandResult<C extends CommandDefinition> = C["output"];
|
||||
|
||||
export class NativeAutofillMain {
|
||||
private ipcServer: autofill.IpcServer | null;
|
||||
private ipcServer?: autofill.AutofillIpcServer;
|
||||
private messageBuffer: BufferedMessage[] = [];
|
||||
private listenerReady = false;
|
||||
|
||||
@@ -70,13 +70,13 @@ export class NativeAutofillMain {
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer = await autofill.IpcServer.listen(
|
||||
this.ipcServer = await autofill.AutofillIpcServer.listen(
|
||||
"af",
|
||||
// RegistrationCallback
|
||||
(error, clientId, sequenceNumber, request) => {
|
||||
if (error) {
|
||||
this.logService.error("autofill.IpcServer.registration", error);
|
||||
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
|
||||
this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
|
||||
return;
|
||||
}
|
||||
this.safeSend("autofill.passkeyRegistration", {
|
||||
@@ -89,7 +89,7 @@ export class NativeAutofillMain {
|
||||
(error, clientId, sequenceNumber, request) => {
|
||||
if (error) {
|
||||
this.logService.error("autofill.IpcServer.assertion", error);
|
||||
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
|
||||
this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
|
||||
return;
|
||||
}
|
||||
this.safeSend("autofill.passkeyAssertion", {
|
||||
@@ -102,7 +102,7 @@ export class NativeAutofillMain {
|
||||
(error, clientId, sequenceNumber, request) => {
|
||||
if (error) {
|
||||
this.logService.error("autofill.IpcServer.assertion", error);
|
||||
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
|
||||
this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
|
||||
return;
|
||||
}
|
||||
this.safeSend("autofill.passkeyAssertionWithoutUserInterface", {
|
||||
@@ -115,7 +115,7 @@ export class NativeAutofillMain {
|
||||
(error, clientId, sequenceNumber, status) => {
|
||||
if (error) {
|
||||
this.logService.error("autofill.IpcServer.nativeStatus", error);
|
||||
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
|
||||
this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
|
||||
return;
|
||||
}
|
||||
this.safeSend("autofill.nativeStatus", {
|
||||
@@ -137,19 +137,19 @@ export class NativeAutofillMain {
|
||||
ipcMain.on("autofill.completePasskeyRegistration", (event, data) => {
|
||||
this.logService.debug("autofill.completePasskeyRegistration", data);
|
||||
const { clientId, sequenceNumber, response } = data;
|
||||
this.ipcServer.completeRegistration(clientId, sequenceNumber, response);
|
||||
this.ipcServer?.completeRegistration(clientId, sequenceNumber, response);
|
||||
});
|
||||
|
||||
ipcMain.on("autofill.completePasskeyAssertion", (event, data) => {
|
||||
this.logService.debug("autofill.completePasskeyAssertion", data);
|
||||
const { clientId, sequenceNumber, response } = data;
|
||||
this.ipcServer.completeAssertion(clientId, sequenceNumber, response);
|
||||
this.ipcServer?.completeAssertion(clientId, sequenceNumber, response);
|
||||
});
|
||||
|
||||
ipcMain.on("autofill.completeError", (event, data) => {
|
||||
this.logService.debug("autofill.completeError", data);
|
||||
const { clientId, sequenceNumber, error } = data;
|
||||
this.ipcServer.completeError(clientId, sequenceNumber, String(error));
|
||||
this.ipcServer?.completeError(clientId, sequenceNumber, String(error));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -108,8 +108,13 @@ const ephemeralStore = {
|
||||
};
|
||||
|
||||
const localhostCallbackService = {
|
||||
openSsoPrompt: (codeChallenge: string, state: string, email: string): Promise<void> => {
|
||||
return ipcRenderer.invoke("openSsoPrompt", { codeChallenge, state, email });
|
||||
openSsoPrompt: (
|
||||
codeChallenge: string,
|
||||
state: string,
|
||||
email: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> => {
|
||||
return ipcRenderer.invoke("openSsoPrompt", { codeChallenge, state, email, orgSsoIdentifier });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -25,20 +25,25 @@ export class SSOLocalhostCallbackService {
|
||||
private messagingService: MessageSender,
|
||||
private ssoUrlService: SsoUrlService,
|
||||
) {
|
||||
ipcMain.handle("openSsoPrompt", async (event, { codeChallenge, state, email }) => {
|
||||
// Close any existing server before starting new one
|
||||
if (this.currentServer) {
|
||||
await this.closeCurrentServer();
|
||||
}
|
||||
ipcMain.handle(
|
||||
"openSsoPrompt",
|
||||
async (event, { codeChallenge, state, email, orgSsoIdentifier }) => {
|
||||
// Close any existing server before starting new one
|
||||
if (this.currentServer) {
|
||||
await this.closeCurrentServer();
|
||||
}
|
||||
|
||||
return this.openSsoPrompt(codeChallenge, state, email).then(({ ssoCode, recvState }) => {
|
||||
this.messagingService.send("ssoCallback", {
|
||||
code: ssoCode,
|
||||
state: recvState,
|
||||
redirectUri: this.ssoRedirectUri,
|
||||
});
|
||||
});
|
||||
});
|
||||
return this.openSsoPrompt(codeChallenge, state, email, orgSsoIdentifier).then(
|
||||
({ ssoCode, recvState }) => {
|
||||
this.messagingService.send("ssoCallback", {
|
||||
code: ssoCode,
|
||||
state: recvState,
|
||||
redirectUri: this.ssoRedirectUri,
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async closeCurrentServer(): Promise<void> {
|
||||
@@ -58,6 +63,7 @@ export class SSOLocalhostCallbackService {
|
||||
codeChallenge: string,
|
||||
state: string,
|
||||
email: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<{ ssoCode: string; recvState: string }> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
|
||||
@@ -121,6 +127,7 @@ export class SSOLocalhostCallbackService {
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
|
||||
// Set up error handler before attempting to listen
|
||||
|
||||
@@ -35,13 +35,10 @@ import { OrganizationUserResetPasswordEntry } from "./organization-user-reset-pa
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class OrganizationUserResetPasswordService
|
||||
implements
|
||||
UserKeyRotationKeyRecoveryProvider<
|
||||
OrganizationUserResetPasswordWithIdRequest,
|
||||
OrganizationUserResetPasswordEntry
|
||||
>
|
||||
{
|
||||
export class OrganizationUserResetPasswordService implements UserKeyRotationKeyRecoveryProvider<
|
||||
OrganizationUserResetPasswordWithIdRequest,
|
||||
OrganizationUserResetPasswordEntry
|
||||
> {
|
||||
constructor(
|
||||
private keyService: KeyService,
|
||||
private encryptService: EncryptService,
|
||||
|
||||
@@ -61,8 +61,11 @@ export class WebLoginComponentService
|
||||
email: string,
|
||||
state: string,
|
||||
codeChallenge: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> {
|
||||
await this.router.navigate(["/sso"]);
|
||||
await this.router.navigate(["/sso"], {
|
||||
queryParams: { identifier: orgSsoIdentifier },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,9 +39,7 @@ import { WebAuthnLoginAdminApiService } from "./webauthn-login-admin-api.service
|
||||
/**
|
||||
* Service for managing WebAuthnLogin credentials.
|
||||
*/
|
||||
export class WebauthnLoginAdminService
|
||||
implements UserKeyRotationDataProvider<WebauthnRotateCredentialRequest>
|
||||
{
|
||||
export class WebauthnLoginAdminService implements UserKeyRotationDataProvider<WebauthnRotateCredentialRequest> {
|
||||
static readonly MaxCredentialCount = 5;
|
||||
|
||||
private navigatorCredentials: CredentialsContainer;
|
||||
|
||||
@@ -45,13 +45,10 @@ import { EmergencyAccessGranteeDetailsResponse } from "../response/emergency-acc
|
||||
import { EmergencyAccessApiService } from "./emergency-access-api.service";
|
||||
|
||||
@Injectable()
|
||||
export class EmergencyAccessService
|
||||
implements
|
||||
UserKeyRotationKeyRecoveryProvider<
|
||||
EmergencyAccessWithIdRequest,
|
||||
GranteeEmergencyAccessWithPublicKey
|
||||
>
|
||||
{
|
||||
export class EmergencyAccessService implements UserKeyRotationKeyRecoveryProvider<
|
||||
EmergencyAccessWithIdRequest,
|
||||
GranteeEmergencyAccessWithPublicKey
|
||||
> {
|
||||
constructor(
|
||||
private emergencyAccessApiService: EmergencyAccessApiService,
|
||||
private apiService: ApiService,
|
||||
|
||||
@@ -38,12 +38,7 @@
|
||||
{{ i.amount | currency: "$" }}
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
sub?.customerDiscount?.appliesTo?.includes(i.productId);
|
||||
else calculateElse
|
||||
"
|
||||
>
|
||||
<ng-container *ngIf="isSecretsManagerTrial(); else calculateElse">
|
||||
{{ "freeForOneYear" | i18n }}
|
||||
</ng-container>
|
||||
<ng-template #calculateElse>
|
||||
@@ -52,7 +47,7 @@
|
||||
{{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }}
|
||||
</span>
|
||||
<span
|
||||
*ngIf="customerDiscount?.percentOff && !isSecretsManagerTrial()"
|
||||
*ngIf="customerDiscount?.percentOff"
|
||||
class="tw-line-through !tw-text-muted"
|
||||
>{{
|
||||
calculateTotalAppliedDiscount(i.quantity * i.amount) | currency: "$"
|
||||
|
||||
@@ -403,11 +403,13 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
}
|
||||
|
||||
isSecretsManagerTrial(): boolean {
|
||||
return (
|
||||
const isSmStandalone = this.sub?.customerDiscount?.id === "sm-standalone";
|
||||
const appliesToProduct =
|
||||
this.sub?.subscription?.items?.some((item) =>
|
||||
this.sub?.customerDiscount?.appliesTo?.includes(item.productId),
|
||||
) ?? false
|
||||
);
|
||||
) ?? false;
|
||||
|
||||
return isSmStandalone && appliesToProduct;
|
||||
}
|
||||
|
||||
closeChangePlan() {
|
||||
|
||||
@@ -112,6 +112,7 @@ import {
|
||||
} from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
KdfConfigService,
|
||||
@@ -484,7 +485,7 @@ const safeProviders: SafeProvider[] = [
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [CommonModule, JslibServicesModule],
|
||||
imports: [CommonModule, JslibServicesModule, GeneratorServicesModule],
|
||||
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function
|
||||
providers: safeProviders,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<h2 class="tw-mt-6 tw-mb-2 tw-pb-2.5">{{ "dataRecoveryTitle" | i18n }}</h2>
|
||||
|
||||
<div class="tw-max-w-lg">
|
||||
<p bitTypography="body1" class="tw-mb-4">
|
||||
{{ "dataRecoveryDescription" | i18n }}
|
||||
</p>
|
||||
|
||||
@if (!diagnosticsCompleted() && !recoveryCompleted()) {
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[bitAction]="runDiagnostics"
|
||||
class="tw-mb-6"
|
||||
>
|
||||
{{ "runDiagnostics" | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
<div class="tw-space-y-3 tw-mb-6">
|
||||
@for (step of steps(); track $index) {
|
||||
@if (
|
||||
($index === 0 && hasStarted()) ||
|
||||
($index > 0 &&
|
||||
(steps()[$index - 1].status === StepStatus.Completed ||
|
||||
steps()[$index - 1].status === StepStatus.Failed))
|
||||
) {
|
||||
<div class="tw-flex tw-items-start tw-gap-3">
|
||||
<div class="tw-mt-1">
|
||||
@if (step.status === StepStatus.Failed) {
|
||||
<i class="bwi bwi-close tw-text-danger" aria-hidden="true"></i>
|
||||
} @else if (step.status === StepStatus.Completed) {
|
||||
<i class="bwi bwi-check tw-text-success" aria-hidden="true"></i>
|
||||
} @else if (step.status === StepStatus.InProgress) {
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-primary-600" aria-hidden="true"></i>
|
||||
} @else {
|
||||
<i class="bwi bwi-circle tw-text-secondary-300" aria-hidden="true"></i>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
[class.tw-text-danger]="step.status === StepStatus.Failed"
|
||||
[class.tw-text-success]="step.status === StepStatus.Completed"
|
||||
[class.tw-text-primary-600]="step.status === StepStatus.InProgress"
|
||||
[class.tw-font-semibold]="step.status === StepStatus.InProgress"
|
||||
[class.tw-text-secondary-500]="step.status === StepStatus.NotStarted"
|
||||
>
|
||||
{{ step.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (diagnosticsCompleted()) {
|
||||
<div class="tw-flex tw-gap-3">
|
||||
@if (hasIssues() && !recoveryCompleted()) {
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="status() === StepStatus.InProgress"
|
||||
[bitAction]="runRecovery"
|
||||
>
|
||||
{{ "repairIssues" | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button type="button" bitButton buttonType="secondary" [bitAction]="saveDiagnosticLogs">
|
||||
<i class="bwi bwi-download" aria-hidden="true"></i>
|
||||
{{ "saveDiagnosticLogs" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,348 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService, UserAsymmetricKeysRegenerationService } from "@bitwarden/key-management";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { DataRecoveryComponent, StepStatus } from "./data-recovery.component";
|
||||
import { RecoveryStep, RecoveryWorkingData } from "./steps";
|
||||
|
||||
// Mock SdkLoadService
|
||||
jest.mock("@bitwarden/common/platform/abstractions/sdk/sdk-load.service", () => ({
|
||||
SdkLoadService: {
|
||||
Ready: Promise.resolve(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("DataRecoveryComponent", () => {
|
||||
let component: DataRecoveryComponent;
|
||||
let fixture: ComponentFixture<DataRecoveryComponent>;
|
||||
|
||||
// Mock Services
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let mockApiService: MockProxy<ApiService>;
|
||||
let mockAccountService: FakeAccountService;
|
||||
let mockKeyService: MockProxy<KeyService>;
|
||||
let mockFolderApiService: MockProxy<FolderApiServiceAbstraction>;
|
||||
let mockCipherEncryptService: MockProxy<CipherEncryptionService>;
|
||||
let mockDialogService: MockProxy<DialogService>;
|
||||
let mockPrivateKeyRegenerationService: MockProxy<UserAsymmetricKeysRegenerationService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockCryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let mockFileDownloadService: MockProxy<FileDownloadService>;
|
||||
|
||||
const mockUserId = "user-id" as UserId;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockI18nService = mock<I18nService>();
|
||||
mockApiService = mock<ApiService>();
|
||||
mockAccountService = mockAccountServiceWith(mockUserId);
|
||||
mockKeyService = mock<KeyService>();
|
||||
mockFolderApiService = mock<FolderApiServiceAbstraction>();
|
||||
mockCipherEncryptService = mock<CipherEncryptionService>();
|
||||
mockDialogService = mock<DialogService>();
|
||||
mockPrivateKeyRegenerationService = mock<UserAsymmetricKeysRegenerationService>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockCryptoFunctionService = mock<CryptoFunctionService>();
|
||||
mockFileDownloadService = mock<FileDownloadService>();
|
||||
|
||||
mockI18nService.t.mockImplementation((key) => `${key}_used-i18n`);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DataRecoveryComponent],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: ApiService, useValue: mockApiService },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: KeyService, useValue: mockKeyService },
|
||||
{ provide: FolderApiServiceAbstraction, useValue: mockFolderApiService },
|
||||
{ provide: CipherEncryptionService, useValue: mockCipherEncryptService },
|
||||
{ provide: DialogService, useValue: mockDialogService },
|
||||
{
|
||||
provide: UserAsymmetricKeysRegenerationService,
|
||||
useValue: mockPrivateKeyRegenerationService,
|
||||
},
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: CryptoFunctionService, useValue: mockCryptoFunctionService },
|
||||
{ provide: FileDownloadService, useValue: mockFileDownloadService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DataRecoveryComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe("Component Initialization", () => {
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should initialize with default signal values", () => {
|
||||
expect(component.status()).toBe(StepStatus.NotStarted);
|
||||
expect(component.hasStarted()).toBe(false);
|
||||
expect(component.diagnosticsCompleted()).toBe(false);
|
||||
expect(component.recoveryCompleted()).toBe(false);
|
||||
expect(component.hasIssues()).toBe(false);
|
||||
});
|
||||
|
||||
it("should initialize steps in correct order", () => {
|
||||
const steps = component.steps();
|
||||
expect(steps.length).toBe(5);
|
||||
expect(steps[0].title).toBe("recoveryStepUserInfoTitle_used-i18n");
|
||||
expect(steps[1].title).toBe("recoveryStepSyncTitle_used-i18n");
|
||||
expect(steps[2].title).toBe("recoveryStepPrivateKeyTitle_used-i18n");
|
||||
expect(steps[3].title).toBe("recoveryStepFoldersTitle_used-i18n");
|
||||
expect(steps[4].title).toBe("recoveryStepCipherTitle_used-i18n");
|
||||
});
|
||||
});
|
||||
|
||||
describe("runDiagnostics", () => {
|
||||
let mockSteps: MockProxy<RecoveryStep>[];
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock steps
|
||||
mockSteps = Array(5)
|
||||
.fill(null)
|
||||
.map(() => {
|
||||
const mockStep = mock<RecoveryStep>();
|
||||
mockStep.title = "mockStep";
|
||||
mockStep.runDiagnostics.mockResolvedValue(true);
|
||||
mockStep.canRecover.mockReturnValue(false);
|
||||
return mockStep;
|
||||
});
|
||||
|
||||
// Replace recovery steps with mocks
|
||||
component["recoverySteps"] = mockSteps;
|
||||
});
|
||||
|
||||
it("should not run if already running", async () => {
|
||||
component["status"].set(StepStatus.InProgress);
|
||||
await component.runDiagnostics();
|
||||
|
||||
expect(mockSteps[0].runDiagnostics).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should set hasStarted, isRunning and initialize workingData", async () => {
|
||||
await component.runDiagnostics();
|
||||
|
||||
expect(component.hasStarted()).toBe(true);
|
||||
expect(component["workingData"]).toBeDefined();
|
||||
expect(component["workingData"]?.userId).toBeNull();
|
||||
expect(component["workingData"]?.userKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should run diagnostics for all steps", async () => {
|
||||
await component.runDiagnostics();
|
||||
|
||||
mockSteps.forEach((step) => {
|
||||
expect(step.runDiagnostics).toHaveBeenCalledWith(
|
||||
component["workingData"],
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should mark steps as completed when diagnostics succeed", async () => {
|
||||
await component.runDiagnostics();
|
||||
|
||||
const steps = component.steps();
|
||||
steps.forEach((step) => {
|
||||
expect(step.status).toBe(StepStatus.Completed);
|
||||
});
|
||||
});
|
||||
|
||||
it("should mark steps as failed when diagnostics return false", async () => {
|
||||
mockSteps[2].runDiagnostics.mockResolvedValue(false);
|
||||
|
||||
await component.runDiagnostics();
|
||||
|
||||
const steps = component.steps();
|
||||
expect(steps[2].status).toBe(StepStatus.Failed);
|
||||
});
|
||||
|
||||
it("should mark steps as failed when diagnostics throw error", async () => {
|
||||
mockSteps[3].runDiagnostics.mockRejectedValue(new Error("Test error"));
|
||||
|
||||
await component.runDiagnostics();
|
||||
|
||||
const steps = component.steps();
|
||||
expect(steps[3].status).toBe(StepStatus.Failed);
|
||||
expect(steps[3].message).toBe("Test error");
|
||||
});
|
||||
|
||||
it("should continue diagnostics even if a step fails", async () => {
|
||||
mockSteps[1].runDiagnostics.mockRejectedValue(new Error("Step 1 failed"));
|
||||
mockSteps[3].runDiagnostics.mockResolvedValue(false);
|
||||
|
||||
await component.runDiagnostics();
|
||||
|
||||
// All steps should have been called despite failures
|
||||
mockSteps.forEach((step) => {
|
||||
expect(step.runDiagnostics).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should set hasIssues to true when a step can recover", async () => {
|
||||
mockSteps[2].runDiagnostics.mockResolvedValue(false);
|
||||
mockSteps[2].canRecover.mockReturnValue(true);
|
||||
|
||||
await component.runDiagnostics();
|
||||
|
||||
expect(component.hasIssues()).toBe(true);
|
||||
});
|
||||
|
||||
it("should set hasIssues to false when no step can recover", async () => {
|
||||
mockSteps.forEach((step) => {
|
||||
step.runDiagnostics.mockResolvedValue(true);
|
||||
step.canRecover.mockReturnValue(false);
|
||||
});
|
||||
|
||||
await component.runDiagnostics();
|
||||
|
||||
expect(component.hasIssues()).toBe(false);
|
||||
});
|
||||
|
||||
it("should set diagnosticsCompleted and status to completed when complete", async () => {
|
||||
await component.runDiagnostics();
|
||||
|
||||
expect(component.diagnosticsCompleted()).toBe(true);
|
||||
expect(component.status()).toBe(StepStatus.Completed);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runRecovery", () => {
|
||||
let mockSteps: MockProxy<RecoveryStep>[];
|
||||
let mockWorkingData: RecoveryWorkingData;
|
||||
|
||||
beforeEach(() => {
|
||||
mockWorkingData = {
|
||||
userId: mockUserId,
|
||||
userKey: null as any,
|
||||
isPrivateKeyCorrupt: false,
|
||||
encryptedPrivateKey: null,
|
||||
ciphers: [],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
mockSteps = Array(5)
|
||||
.fill(null)
|
||||
.map(() => {
|
||||
const mockStep = mock<RecoveryStep>();
|
||||
mockStep.title = "mockStep";
|
||||
mockStep.canRecover.mockReturnValue(false);
|
||||
mockStep.runRecovery.mockResolvedValue();
|
||||
mockStep.runDiagnostics.mockResolvedValue(true);
|
||||
return mockStep;
|
||||
});
|
||||
|
||||
component["recoverySteps"] = mockSteps;
|
||||
component["workingData"] = mockWorkingData;
|
||||
});
|
||||
|
||||
it("should not run if already running", async () => {
|
||||
component["status"].set(StepStatus.InProgress);
|
||||
await component.runRecovery();
|
||||
|
||||
expect(mockSteps[0].runRecovery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not run if workingData is null", async () => {
|
||||
component["workingData"] = null;
|
||||
await component.runRecovery();
|
||||
|
||||
expect(mockSteps[0].runRecovery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should only run recovery for steps that can recover", async () => {
|
||||
mockSteps[1].canRecover.mockReturnValue(true);
|
||||
mockSteps[3].canRecover.mockReturnValue(true);
|
||||
|
||||
await component.runRecovery();
|
||||
|
||||
expect(mockSteps[0].runRecovery).not.toHaveBeenCalled();
|
||||
expect(mockSteps[1].runRecovery).toHaveBeenCalled();
|
||||
expect(mockSteps[2].runRecovery).not.toHaveBeenCalled();
|
||||
expect(mockSteps[3].runRecovery).toHaveBeenCalled();
|
||||
expect(mockSteps[4].runRecovery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should set recoveryCompleted and status when successful", async () => {
|
||||
mockSteps[1].canRecover.mockReturnValue(true);
|
||||
|
||||
await component.runRecovery();
|
||||
|
||||
expect(component.recoveryCompleted()).toBe(true);
|
||||
expect(component.status()).toBe(StepStatus.Completed);
|
||||
});
|
||||
|
||||
it("should set status to failed if recovery is cancelled", async () => {
|
||||
mockSteps[1].canRecover.mockReturnValue(true);
|
||||
mockSteps[1].runRecovery.mockRejectedValue(new Error("User cancelled"));
|
||||
|
||||
await component.runRecovery();
|
||||
|
||||
expect(component.status()).toBe(StepStatus.Failed);
|
||||
expect(component.recoveryCompleted()).toBe(false);
|
||||
});
|
||||
|
||||
it("should re-run diagnostics after recovery completes", async () => {
|
||||
mockSteps[1].canRecover.mockReturnValue(true);
|
||||
|
||||
await component.runRecovery();
|
||||
|
||||
// Diagnostics should be called twice: once for initial diagnostic scan
|
||||
mockSteps.forEach((step) => {
|
||||
expect(step.runDiagnostics).toHaveBeenCalledWith(mockWorkingData, expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
it("should update hasIssues after re-running diagnostics", async () => {
|
||||
// Setup initial state with an issue
|
||||
mockSteps[1].canRecover.mockReturnValue(true);
|
||||
mockSteps[1].runDiagnostics.mockResolvedValue(false);
|
||||
|
||||
// After recovery completes, the issue should be fixed
|
||||
mockSteps[1].runRecovery.mockImplementation(() => {
|
||||
// Simulate recovery fixing the issue
|
||||
mockSteps[1].canRecover.mockReturnValue(false);
|
||||
mockSteps[1].runDiagnostics.mockResolvedValue(true);
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await component.runRecovery();
|
||||
|
||||
// Verify hasIssues is updated after re-running diagnostics
|
||||
expect(component.hasIssues()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveDiagnosticLogs", () => {
|
||||
it("should call fileDownloadService with log content", () => {
|
||||
component.saveDiagnosticLogs();
|
||||
|
||||
expect(mockFileDownloadService.download).toHaveBeenCalledWith({
|
||||
fileName: expect.stringContaining("data-recovery-logs-"),
|
||||
blobData: expect.any(String),
|
||||
blobOptions: { type: "text/plain" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should include timestamp in filename", () => {
|
||||
component.saveDiagnosticLogs();
|
||||
|
||||
const downloadCall = mockFileDownloadService.download.mock.calls[0][0];
|
||||
expect(downloadCall.fileName).toMatch(/data-recovery-logs-\d{4}-\d{2}-\d{2}T.*\.txt/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, inject, signal } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { ButtonModule, DialogService } from "@bitwarden/components";
|
||||
import { KeyService, UserAsymmetricKeysRegenerationService } from "@bitwarden/key-management";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { LogRecorder } from "./log-recorder";
|
||||
import {
|
||||
SyncStep,
|
||||
UserInfoStep,
|
||||
RecoveryStep,
|
||||
PrivateKeyStep,
|
||||
RecoveryWorkingData,
|
||||
FolderStep,
|
||||
CipherStep,
|
||||
} from "./steps";
|
||||
|
||||
export const StepStatus = Object.freeze({
|
||||
NotStarted: 0,
|
||||
InProgress: 1,
|
||||
Completed: 2,
|
||||
Failed: 3,
|
||||
} as const);
|
||||
export type StepStatus = (typeof StepStatus)[keyof typeof StepStatus];
|
||||
|
||||
interface StepState {
|
||||
title: string;
|
||||
status: StepStatus;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-data-recovery",
|
||||
templateUrl: "data-recovery.component.html",
|
||||
standalone: true,
|
||||
imports: [JslibModule, ButtonModule, CommonModule, SharedModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DataRecoveryComponent {
|
||||
protected readonly StepStatus = StepStatus;
|
||||
|
||||
private i18nService = inject(I18nService);
|
||||
private apiService = inject(ApiService);
|
||||
private accountService = inject(AccountService);
|
||||
private keyService = inject(KeyService);
|
||||
private folderApiService = inject(FolderApiServiceAbstraction);
|
||||
private cipherEncryptService = inject(CipherEncryptionService);
|
||||
private dialogService = inject(DialogService);
|
||||
private privateKeyRegenerationService = inject(UserAsymmetricKeysRegenerationService);
|
||||
private cryptoFunctionService = inject(CryptoFunctionService);
|
||||
private logService = inject(LogService);
|
||||
private fileDownloadService = inject(FileDownloadService);
|
||||
|
||||
private logger: LogRecorder = new LogRecorder(this.logService);
|
||||
private recoverySteps: RecoveryStep[] = [
|
||||
new UserInfoStep(this.accountService, this.keyService),
|
||||
new SyncStep(this.apiService),
|
||||
new PrivateKeyStep(
|
||||
this.privateKeyRegenerationService,
|
||||
this.dialogService,
|
||||
this.cryptoFunctionService,
|
||||
),
|
||||
new FolderStep(this.folderApiService, this.dialogService),
|
||||
new CipherStep(this.apiService, this.cipherEncryptService, this.dialogService),
|
||||
];
|
||||
private workingData: RecoveryWorkingData | null = null;
|
||||
|
||||
readonly status = signal<StepStatus>(StepStatus.NotStarted);
|
||||
readonly hasStarted = signal(false);
|
||||
readonly diagnosticsCompleted = signal(false);
|
||||
readonly recoveryCompleted = signal(false);
|
||||
readonly steps = signal<StepState[]>(
|
||||
this.recoverySteps.map((step) => ({
|
||||
title: this.i18nService.t(step.title),
|
||||
status: StepStatus.NotStarted,
|
||||
})),
|
||||
);
|
||||
readonly hasIssues = signal(false);
|
||||
|
||||
runDiagnostics = async () => {
|
||||
if (this.status() === StepStatus.InProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasStarted.set(true);
|
||||
this.status.set(StepStatus.InProgress);
|
||||
this.diagnosticsCompleted.set(false);
|
||||
|
||||
this.logger.record("Starting diagnostics...");
|
||||
this.workingData = {
|
||||
userId: null,
|
||||
userKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
encryptedPrivateKey: null,
|
||||
ciphers: [],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
await this.runDiagnosticsInternal();
|
||||
|
||||
this.status.set(StepStatus.Completed);
|
||||
this.diagnosticsCompleted.set(true);
|
||||
};
|
||||
|
||||
private async runDiagnosticsInternal() {
|
||||
if (!this.workingData) {
|
||||
this.logger.record("No working data available");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSteps = this.steps();
|
||||
let hasAnyFailures = false;
|
||||
|
||||
for (let i = 0; i < this.recoverySteps.length; i++) {
|
||||
const step = this.recoverySteps[i];
|
||||
currentSteps[i].status = StepStatus.InProgress;
|
||||
this.steps.set([...currentSteps]);
|
||||
|
||||
this.logger.record(`Running diagnostics for step: ${step.title}`);
|
||||
try {
|
||||
const success = await step.runDiagnostics(this.workingData, this.logger);
|
||||
currentSteps[i].status = success ? StepStatus.Completed : StepStatus.Failed;
|
||||
if (!success) {
|
||||
hasAnyFailures = true;
|
||||
}
|
||||
this.steps.set([...currentSteps]);
|
||||
this.logger.record(`Diagnostics completed for step: ${step.title}`);
|
||||
} catch (error) {
|
||||
currentSteps[i].status = StepStatus.Failed;
|
||||
currentSteps[i].message = (error as Error).message;
|
||||
this.steps.set([...currentSteps]);
|
||||
this.logger.record(
|
||||
`Diagnostics failed for step: ${step.title} with error: ${(error as Error).message}`,
|
||||
);
|
||||
hasAnyFailures = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAnyFailures) {
|
||||
this.logger.record("Diagnostics completed with errors");
|
||||
} else {
|
||||
this.logger.record("Diagnostics completed successfully");
|
||||
}
|
||||
|
||||
// Check if any recovery can be performed
|
||||
const canRecoverAnyStep = this.recoverySteps.some((step) => step.canRecover(this.workingData!));
|
||||
this.hasIssues.set(canRecoverAnyStep);
|
||||
}
|
||||
|
||||
runRecovery = async () => {
|
||||
if (this.status() === StepStatus.InProgress || !this.workingData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.status.set(StepStatus.InProgress);
|
||||
this.recoveryCompleted.set(false);
|
||||
|
||||
this.logger.record("Starting recovery process...");
|
||||
|
||||
try {
|
||||
for (let i = 0; i < this.recoverySteps.length; i++) {
|
||||
const step = this.recoverySteps[i];
|
||||
if (step.canRecover(this.workingData)) {
|
||||
this.logger.record(`Running recovery for step: ${step.title}`);
|
||||
await step.runRecovery(this.workingData, this.logger);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.record("Recovery process completed");
|
||||
this.recoveryCompleted.set(true);
|
||||
|
||||
// Re-run diagnostics after recovery
|
||||
this.logger.record("Re-running diagnostics to verify recovery...");
|
||||
await this.runDiagnosticsInternal();
|
||||
|
||||
this.status.set(StepStatus.Completed);
|
||||
} catch (error) {
|
||||
this.logger.record(`Recovery process cancelled or failed: ${(error as Error).message}`);
|
||||
this.status.set(StepStatus.Failed);
|
||||
}
|
||||
};
|
||||
|
||||
saveDiagnosticLogs = () => {
|
||||
const logs = this.logger.getLogs();
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const filename = `data-recovery-logs-${timestamp}.txt`;
|
||||
|
||||
const logContent = logs.join("\n");
|
||||
this.fileDownloadService.download({
|
||||
fileName: filename,
|
||||
blobData: logContent,
|
||||
blobOptions: { type: "text/plain" },
|
||||
});
|
||||
|
||||
this.logger.record("Diagnostic logs saved");
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
/**
|
||||
* Record logs during the data recovery process. This only keeps them in memory and does not persist them anywhere.
|
||||
*/
|
||||
export class LogRecorder {
|
||||
private logs: string[] = [];
|
||||
|
||||
constructor(private logService: LogService) {}
|
||||
|
||||
record(message: string) {
|
||||
this.logs.push(message);
|
||||
this.logService.info(`[DataRecovery] ${message}`);
|
||||
}
|
||||
|
||||
getLogs(): string[] {
|
||||
return [...this.logs];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { LogRecorder } from "../log-recorder";
|
||||
|
||||
import { RecoveryStep, RecoveryWorkingData } from "./recovery-step";
|
||||
|
||||
export class CipherStep implements RecoveryStep {
|
||||
title = "recoveryStepCipherTitle";
|
||||
|
||||
private undecryptableCipherIds: string[] = [];
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private cipherService: CipherEncryptionService,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
async runDiagnostics(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<boolean> {
|
||||
if (!workingData.userId) {
|
||||
logger.record("Missing user ID");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.undecryptableCipherIds = [];
|
||||
for (const cipher of workingData.ciphers) {
|
||||
try {
|
||||
await this.cipherService.decrypt(cipher, workingData.userId);
|
||||
} catch {
|
||||
logger.record(`Cipher ID ${cipher.id} was undecryptable`);
|
||||
this.undecryptableCipherIds.push(cipher.id);
|
||||
}
|
||||
}
|
||||
logger.record(`Found ${this.undecryptableCipherIds.length} undecryptable ciphers`);
|
||||
|
||||
return this.undecryptableCipherIds.length == 0;
|
||||
}
|
||||
|
||||
canRecover(workingData: RecoveryWorkingData): boolean {
|
||||
return this.undecryptableCipherIds.length > 0;
|
||||
}
|
||||
|
||||
async runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<void> {
|
||||
// Recovery means deleting the broken ciphers.
|
||||
if (this.undecryptableCipherIds.length === 0) {
|
||||
logger.record("No undecryptable ciphers to recover");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.record(`Showing confirmation dialog for ${this.undecryptableCipherIds.length} ciphers`);
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "recoveryDeleteCiphersTitle" },
|
||||
content: { key: "recoveryDeleteCiphersDesc" },
|
||||
acceptButtonText: { key: "ok" },
|
||||
cancelButtonText: { key: "cancel" },
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
logger.record("User cancelled cipher deletion");
|
||||
throw new Error("Cipher recovery cancelled by user");
|
||||
}
|
||||
|
||||
logger.record(`Deleting ${this.undecryptableCipherIds.length} ciphers`);
|
||||
|
||||
for (const cipherId of this.undecryptableCipherIds) {
|
||||
try {
|
||||
await this.apiService.deleteCipher(cipherId);
|
||||
logger.record(`Deleted cipher ${cipherId}`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.record(`Failed to delete cipher ${cipherId}: ${errorMessage}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
logger.record(`Successfully deleted ${this.undecryptableCipherIds.length} ciphers`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { LogRecorder } from "../log-recorder";
|
||||
|
||||
import { RecoveryStep, RecoveryWorkingData } from "./recovery-step";
|
||||
|
||||
export class FolderStep implements RecoveryStep {
|
||||
title = "recoveryStepFoldersTitle";
|
||||
|
||||
private undecryptableFolderIds: string[] = [];
|
||||
|
||||
constructor(
|
||||
private folderService: FolderApiServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
async runDiagnostics(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<boolean> {
|
||||
if (!workingData.userKey) {
|
||||
logger.record("Missing user key");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.undecryptableFolderIds = [];
|
||||
for (const folder of workingData.folders) {
|
||||
if (!folder.name?.encryptedString) {
|
||||
logger.record(`Folder ID ${folder.id} has no name`);
|
||||
this.undecryptableFolderIds.push(folder.id);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await SdkLoadService.Ready;
|
||||
PureCrypto.symmetric_decrypt_string(
|
||||
folder.name.encryptedString,
|
||||
workingData.userKey.toEncoded(),
|
||||
);
|
||||
} catch {
|
||||
logger.record(`Folder name for folder ID ${folder.id} was undecryptable`);
|
||||
this.undecryptableFolderIds.push(folder.id);
|
||||
}
|
||||
}
|
||||
logger.record(`Found ${this.undecryptableFolderIds.length} undecryptable folders`);
|
||||
|
||||
return this.undecryptableFolderIds.length == 0;
|
||||
}
|
||||
|
||||
canRecover(workingData: RecoveryWorkingData): boolean {
|
||||
return this.undecryptableFolderIds.length > 0;
|
||||
}
|
||||
|
||||
async runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<void> {
|
||||
// Recovery means deleting the broken folders.
|
||||
if (this.undecryptableFolderIds.length === 0) {
|
||||
logger.record("No undecryptable folders to recover");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!workingData.userId) {
|
||||
logger.record("Missing user ID");
|
||||
throw new Error("Missing user ID");
|
||||
}
|
||||
|
||||
logger.record(`Showing confirmation dialog for ${this.undecryptableFolderIds.length} folders`);
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "recoveryDeleteFoldersTitle" },
|
||||
content: { key: "recoveryDeleteFoldersDesc" },
|
||||
acceptButtonText: { key: "ok" },
|
||||
cancelButtonText: { key: "cancel" },
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
logger.record("User cancelled folder deletion");
|
||||
throw new Error("Folder recovery cancelled by user");
|
||||
}
|
||||
|
||||
logger.record(`Deleting ${this.undecryptableFolderIds.length} folders`);
|
||||
|
||||
for (const folderId of this.undecryptableFolderIds) {
|
||||
try {
|
||||
await this.folderService.delete(folderId, workingData.userId);
|
||||
logger.record(`Deleted folder ${folderId}`);
|
||||
} catch (error) {
|
||||
logger.record(`Failed to delete folder ${folderId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.record(`Successfully deleted ${this.undecryptableFolderIds.length} folders`);
|
||||
}
|
||||
|
||||
getUndecryptableFolderIds(): string[] {
|
||||
return this.undecryptableFolderIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from "./sync-step";
|
||||
export * from "./user-info-step";
|
||||
export * from "./recovery-step";
|
||||
export * from "./private-key-step";
|
||||
export * from "./folder-step";
|
||||
export * from "./cipher-step";
|
||||
@@ -0,0 +1,93 @@
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { UserAsymmetricKeysRegenerationService } from "@bitwarden/key-management";
|
||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { LogRecorder } from "../log-recorder";
|
||||
|
||||
import { RecoveryStep, RecoveryWorkingData } from "./recovery-step";
|
||||
|
||||
export class PrivateKeyStep implements RecoveryStep {
|
||||
title = "recoveryStepPrivateKeyTitle";
|
||||
|
||||
constructor(
|
||||
private privateKeyRegenerationService: UserAsymmetricKeysRegenerationService,
|
||||
private dialogService: DialogService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
) {}
|
||||
|
||||
async runDiagnostics(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<boolean> {
|
||||
if (!workingData.userId || !workingData.userKey) {
|
||||
logger.record("Missing user ID or user key");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the private key decrypts properly and is not somehow encrypted by a different user key / broken during key rotation.
|
||||
const encryptedPrivateKey = workingData.encryptedPrivateKey;
|
||||
if (!encryptedPrivateKey) {
|
||||
logger.record("No encrypted private key found");
|
||||
return false;
|
||||
}
|
||||
logger.record("Private key length: " + encryptedPrivateKey.length);
|
||||
let privateKey: Uint8Array;
|
||||
try {
|
||||
await SdkLoadService.Ready;
|
||||
privateKey = PureCrypto.unwrap_decapsulation_key(
|
||||
encryptedPrivateKey,
|
||||
workingData.userKey.toEncoded(),
|
||||
);
|
||||
} catch {
|
||||
logger.record("Private key was un-decryptable");
|
||||
workingData.isPrivateKeyCorrupt = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the contained private key can be parsed and the public key can be derived. If not, then the private key may be corrupt / generated with an incompatible ASN.1 representation / with incompatible padding.
|
||||
try {
|
||||
const publicKey = await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
||||
logger.record("Public key length: " + publicKey.length);
|
||||
} catch {
|
||||
logger.record("Public key could not be derived; private key is corrupt");
|
||||
workingData.isPrivateKeyCorrupt = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
canRecover(workingData: RecoveryWorkingData): boolean {
|
||||
// Only support recovery on V1 users.
|
||||
return (
|
||||
workingData.isPrivateKeyCorrupt &&
|
||||
workingData.userKey !== null &&
|
||||
workingData.userKey.inner().type === EncryptionType.AesCbc256_HmacSha256_B64
|
||||
);
|
||||
}
|
||||
|
||||
async runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<void> {
|
||||
// The recovery step is to replace the key pair. Currently, this only works if the user is not using emergency access or is part of an organization.
|
||||
// This is because this will break emergency access enrollments / organization memberships / provider memberships.
|
||||
logger.record("Showing confirmation dialog for private key replacement");
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "recoveryReplacePrivateKeyTitle" },
|
||||
content: { key: "recoveryReplacePrivateKeyDesc" },
|
||||
acceptButtonText: { key: "ok" },
|
||||
cancelButtonText: { key: "cancel" },
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
logger.record("User cancelled private key replacement");
|
||||
throw new Error("Private key recovery cancelled by user");
|
||||
}
|
||||
|
||||
logger.record("Replacing private key");
|
||||
await this.privateKeyRegenerationService.regenerateUserPublicKeyEncryptionKeyPair(
|
||||
workingData.userId!,
|
||||
);
|
||||
logger.record("Private key replaced successfully");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { WrappedPrivateKey } from "@bitwarden/common/key-management/types";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { LogRecorder } from "../log-recorder";
|
||||
|
||||
/**
|
||||
* A recovery step performs diagnostics and recovery actions on a specific domain, such as ciphers.
|
||||
*/
|
||||
export abstract class RecoveryStep {
|
||||
/** Title of the recovery step, as an i18n key. */
|
||||
abstract title: string;
|
||||
|
||||
/**
|
||||
* Runs diagnostics on the provided working data.
|
||||
* Returns true if no issues were found, false otherwise.
|
||||
*/
|
||||
abstract runDiagnostics(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Returns whether recovery can be performed
|
||||
*/
|
||||
abstract canRecover(workingData: RecoveryWorkingData): boolean;
|
||||
|
||||
/**
|
||||
* Performs recovery on the provided working data.
|
||||
*/
|
||||
abstract runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data used during the recovery process, passed between steps.
|
||||
*/
|
||||
export type RecoveryWorkingData = {
|
||||
userId: UserId | null;
|
||||
userKey: UserKey | null;
|
||||
encryptedPrivateKey: WrappedPrivateKey | null;
|
||||
isPrivateKeyCorrupt: boolean;
|
||||
ciphers: Cipher[];
|
||||
folders: Folder[];
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
import { FolderData } from "@bitwarden/common/vault/models/data/folder.data";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
|
||||
|
||||
import { LogRecorder } from "../log-recorder";
|
||||
|
||||
import { RecoveryStep, RecoveryWorkingData } from "./recovery-step";
|
||||
|
||||
export class SyncStep implements RecoveryStep {
|
||||
title = "recoveryStepSyncTitle";
|
||||
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async runDiagnostics(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<boolean> {
|
||||
// The intent of this step is to fetch the latest data from the server. Diagnostics does not
|
||||
// ever run on local data but only remote data that is recent.
|
||||
const response = await this.apiService.getSync();
|
||||
|
||||
workingData.ciphers = response.ciphers.map((c) => new Cipher(new CipherData(c)));
|
||||
logger.record(`Fetched ${workingData.ciphers.length} ciphers from server`);
|
||||
|
||||
workingData.folders = response.folders.map((f) => new Folder(new FolderData(f)));
|
||||
logger.record(`Fetched ${workingData.folders.length} folders from server`);
|
||||
|
||||
workingData.encryptedPrivateKey =
|
||||
response.profile?.accountKeys?.publicKeyEncryptionKeyPair?.wrappedPrivateKey ?? null;
|
||||
logger.record(
|
||||
`Fetched encrypted private key of length ${workingData.encryptedPrivateKey?.length ?? 0} from server`,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
canRecover(workingData: RecoveryWorkingData): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { LogRecorder } from "../log-recorder";
|
||||
|
||||
import { RecoveryStep, RecoveryWorkingData } from "./recovery-step";
|
||||
|
||||
export class UserInfoStep implements RecoveryStep {
|
||||
title = "recoveryStepUserInfoTitle";
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private keyService: KeyService,
|
||||
) {}
|
||||
|
||||
async runDiagnostics(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<boolean> {
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (!activeAccount) {
|
||||
logger.record("No active account found");
|
||||
return false;
|
||||
}
|
||||
const userId = activeAccount.id;
|
||||
workingData.userId = userId;
|
||||
logger.record(`User ID: ${userId}`);
|
||||
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
if (!userKey) {
|
||||
logger.record("No user key found");
|
||||
return false;
|
||||
}
|
||||
workingData.userKey = userKey;
|
||||
logger.record(
|
||||
`User encryption type: ${userKey.inner().type === EncryptionType.AesCbc256_HmacSha256_B64 ? "V1" : userKey.inner().type === EncryptionType.CoseEncrypt0 ? "Cose" : "Unknown"}`,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
canRecover(workingData: RecoveryWorkingData): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<div *ngIf="loading" class="tw-text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<p>{{ "removeMasterPasswordForOrganizationUserKeyConnector" | i18n }}</p>
|
||||
<p class="tw-mb-0">{{ "organizationName" | i18n }}:</p>
|
||||
<p class="tw-text-muted tw-mb-6">{{ organization.name }}</p>
|
||||
<p class="tw-mb-0">{{ "keyConnectorDomain" | i18n }}:</p>
|
||||
<p class="tw-text-muted tw-mb-6">{{ organization.keyConnectorUrl }}</p>
|
||||
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
class="tw-w-full tw-mb-2"
|
||||
[bitAction]="convert"
|
||||
[block]="true"
|
||||
>
|
||||
{{ "removeMasterPassword" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
class="tw-w-full"
|
||||
[bitAction]="leave"
|
||||
[block]="true"
|
||||
>
|
||||
{{ "leaveOrganization" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
// 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-remove-password",
|
||||
templateUrl: "remove-password.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}
|
||||
@@ -51,7 +51,7 @@ import {
|
||||
import { canAccessEmergencyAccess } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
import { LockComponent, RemovePasswordComponent } from "@bitwarden/key-management-ui";
|
||||
import { premiumInterestRedirectGuard } from "@bitwarden/web-vault/app/vault/guards/premium-interest-redirect/premium-interest-redirect.guard";
|
||||
|
||||
import { flagEnabled, Flags } from "../utils/flags";
|
||||
@@ -78,8 +78,8 @@ import { freeTrialTextResolver } from "./billing/trial-initiation/complete-trial
|
||||
import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component";
|
||||
import { RouteDataProperties } from "./core";
|
||||
import { ReportsModule } from "./dirt/reports";
|
||||
import { DataRecoveryComponent } from "./key-management/data-recovery/data-recovery.component";
|
||||
import { ConfirmKeyConnectorDomainComponent } from "./key-management/key-connector/confirm-key-connector-domain.component";
|
||||
import { RemovePasswordComponent } from "./key-management/key-connector/remove-password.component";
|
||||
import { FrontendLayoutComponent } from "./layouts/frontend-layout.component";
|
||||
import { UserLayoutComponent } from "./layouts/user-layout.component";
|
||||
import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-landing/request-sm-access.component";
|
||||
@@ -544,9 +544,9 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "removeMasterPassword",
|
||||
key: "verifyYourOrganization",
|
||||
},
|
||||
titleId: "removeMasterPassword",
|
||||
titleId: "verifyYourOrganization",
|
||||
pageIcon: LockIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
@@ -556,9 +556,9 @@ const routes: Routes = [
|
||||
canActivate: [],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "confirmKeyConnectorDomain",
|
||||
key: "verifyYourOrganization",
|
||||
},
|
||||
titleId: "confirmKeyConnectorDomain",
|
||||
titleId: "verifyYourOrganization",
|
||||
pageIcon: DomainIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
@@ -696,6 +696,12 @@ const routes: Routes = [
|
||||
path: "security",
|
||||
loadChildren: () => SecurityRoutingModule,
|
||||
},
|
||||
{
|
||||
path: "data-recovery",
|
||||
component: DataRecoveryComponent,
|
||||
canActivate: [canAccessFeature(FeatureFlag.DataRecoveryTool)],
|
||||
data: { titleId: "dataRecovery" } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "domain-rules",
|
||||
component: DomainRulesComponent,
|
||||
|
||||
@@ -7,7 +7,6 @@ import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.comp
|
||||
import { FreeBitwardenFamiliesComponent } from "../billing/members/free-bitwarden-families.component";
|
||||
import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-families.component";
|
||||
import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { HeaderModule } from "../layouts/header/header.module";
|
||||
import { OrganizationBadgeModule } from "../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "../vault/individual-vault/pipes/pipes.module";
|
||||
@@ -21,7 +20,6 @@ import { SharedModule } from "./shared.module";
|
||||
declarations: [
|
||||
RecoverDeleteComponent,
|
||||
RecoverTwoFactorComponent,
|
||||
RemovePasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
FreeBitwardenFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
@@ -31,7 +29,6 @@ import { SharedModule } from "./shared.module";
|
||||
exports: [
|
||||
RecoverDeleteComponent,
|
||||
RecoverTwoFactorComponent,
|
||||
RemovePasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
|
||||
@@ -80,13 +80,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-col-span-9">
|
||||
<div class="tw-col-span-9 tw-@container/send-table">
|
||||
<!--Listing Table-->
|
||||
<bit-table [dataSource]="dataSource" *ngIf="filteredSends && filteredSends.length">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="name" default>{{ "name" | i18n }}</th>
|
||||
<th bitCell bitSortable="deletionDate">{{ "deletionDate" | i18n }}</th>
|
||||
<th bitCell bitSortable="deletionDate" class="@lg/send-table:tw-table-cell tw-hidden">
|
||||
{{ "deletionDate" | i18n }}
|
||||
</th>
|
||||
<th bitCell>{{ "options" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
@@ -148,8 +150,14 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
<td bitCell class="tw-text-muted" (click)="editSend(s)" class="tw-cursor-pointer">
|
||||
<small bitTypography="body2" appStopProp>{{ s.deletionDate | date: "medium" }}</small>
|
||||
<td
|
||||
bitCell
|
||||
(click)="editSend(s)"
|
||||
class="tw-text-muted tw-cursor-pointer @lg/send-table:tw-table-cell tw-hidden"
|
||||
>
|
||||
<small bitTypography="body2" appStopProp>
|
||||
{{ s.deletionDate | date: "medium" }}
|
||||
</small>
|
||||
</td>
|
||||
<td bitCell class="tw-w-0 tw-text-right">
|
||||
<button
|
||||
|
||||
@@ -84,7 +84,7 @@ import {
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import { DialogRef, DialogService, ToastService, BannerComponent } from "@bitwarden/components";
|
||||
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
import {
|
||||
AddEditFolderDialogComponent,
|
||||
@@ -97,6 +97,8 @@ import {
|
||||
DecryptionFailureDialogComponent,
|
||||
DefaultCipherFormConfigService,
|
||||
PasswordRepromptService,
|
||||
VaultItemsTransferService,
|
||||
DefaultVaultItemsTransferService,
|
||||
} from "@bitwarden/vault";
|
||||
import { UnifiedUpgradePromptService } from "@bitwarden/web-vault/app/billing/individual/upgrade/services";
|
||||
import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module";
|
||||
@@ -177,12 +179,12 @@ type EmptyStateMap = Record<EmptyStateType, EmptyStateItem>;
|
||||
VaultItemsModule,
|
||||
SharedModule,
|
||||
OrganizationWarningsModule,
|
||||
BannerComponent,
|
||||
],
|
||||
providers: [
|
||||
RoutedVaultFilterService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
DefaultCipherFormConfigService,
|
||||
{ provide: VaultItemsTransferService, useClass: DefaultVaultItemsTransferService },
|
||||
],
|
||||
})
|
||||
export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestroy {
|
||||
@@ -349,6 +351,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
private premiumUpgradePromptService: PremiumUpgradePromptService,
|
||||
private autoConfirmService: AutomaticUserConfirmationService,
|
||||
private configService: ConfigService,
|
||||
private vaultItemTransferService: VaultItemsTransferService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -644,6 +647,8 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
void this.unifiedUpgradePromptService.displayUpgradePromptConditionally();
|
||||
|
||||
this.setupAutoConfirm();
|
||||
|
||||
void this.vaultItemTransferService.enforceOrganizationDataOwnership(activeUserId);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -5185,6 +5185,9 @@
|
||||
"oldAttachmentsNeedFixDesc": {
|
||||
"message": "There are old file attachments in your vault that need to be fixed before you can rotate your account's encryption key."
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"yourAccountsFingerprint": {
|
||||
"message": "Your account's fingerprint phrase",
|
||||
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
|
||||
@@ -6812,8 +6815,8 @@
|
||||
"vaultTimeoutRangeError": {
|
||||
"message": "Vault timeout is not within allowed range."
|
||||
},
|
||||
"disablePersonalVaultExport": {
|
||||
"message": "Remove individual vault export"
|
||||
"disableExport": {
|
||||
"message": "Remove export"
|
||||
},
|
||||
"disablePersonalVaultExportDescription": {
|
||||
"message": "Do not allow members to export data from their individual vault."
|
||||
@@ -7133,9 +7136,6 @@
|
||||
"invalidVerificationCode": {
|
||||
"message": "Invalid verification code"
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
|
||||
},
|
||||
"keyConnectorDomain": {
|
||||
"message": "Key Connector domain"
|
||||
},
|
||||
@@ -9487,6 +9487,9 @@
|
||||
"ssoLoginIsRequired": {
|
||||
"message": "SSO login is required"
|
||||
},
|
||||
"emailRequiredForSsoLogin": {
|
||||
"message": "Email is required for SSO"
|
||||
},
|
||||
"selectedRegionFlag": {
|
||||
"message": "Selected region flag"
|
||||
},
|
||||
@@ -12241,6 +12244,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector":{
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified":{
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified":{
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"confirmNoSelectedCriticalApplicationsTitle": {
|
||||
"message": "No critical applications are selected"
|
||||
},
|
||||
@@ -12250,6 +12292,54 @@
|
||||
"userVerificationFailed": {
|
||||
"message": "User verification failed."
|
||||
},
|
||||
"recoveryDeleteCiphersTitle": {
|
||||
"message": "Delete unrecoverable vault items"
|
||||
},
|
||||
"recoveryDeleteCiphersDesc": {
|
||||
"message": "Some of your vault items could not be recovered. Do you want to delete these unrecoverable items from your vault?"
|
||||
},
|
||||
"recoveryDeleteFoldersTitle": {
|
||||
"message": "Delete unrecoverable folders"
|
||||
},
|
||||
"recoveryDeleteFoldersDesc": {
|
||||
"message": "Some of your folders could not be recovered. Do you want to delete these unrecoverable folders from your vault?"
|
||||
},
|
||||
"recoveryReplacePrivateKeyTitle": {
|
||||
"message": "Replace encryption key"
|
||||
},
|
||||
"recoveryReplacePrivateKeyDesc": {
|
||||
"message": "Your public-key encryption key pair could not be recovered. Do you want to replace your encryption key with a new key pair? This will require you to set up existing emergency-access and organization memberships again."
|
||||
},
|
||||
"recoveryStepSyncTitle": {
|
||||
"message": "Synchronizing data"
|
||||
},
|
||||
"recoveryStepPrivateKeyTitle": {
|
||||
"message": "Verifying encryption key integrity"
|
||||
},
|
||||
"recoveryStepUserInfoTitle": {
|
||||
"message": "Verifying user information"
|
||||
},
|
||||
"recoveryStepCipherTitle": {
|
||||
"message": "Verifying vault item integrity"
|
||||
},
|
||||
"recoveryStepFoldersTitle": {
|
||||
"message": "Verifying folder integrity"
|
||||
},
|
||||
"dataRecoveryTitle": {
|
||||
"message": "Data Recovery and Diagnostics"
|
||||
},
|
||||
"dataRecoveryDescription": {
|
||||
"message": "Use the data recovery tool to diagnose and repair issues with your account. After running diagnostics you have the option to save diagnostic logs for support and the option to repair any detected issues."
|
||||
},
|
||||
"runDiagnostics": {
|
||||
"message": "Run Diagnostics"
|
||||
},
|
||||
"repairIssues": {
|
||||
"message": "Repair Issues"
|
||||
},
|
||||
"saveDiagnosticLogs": {
|
||||
"message": "Save Diagnostic Logs"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
@@ -12287,5 +12377,53 @@
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
export class DisablePersonalVaultExportPolicy extends BasePolicyEditDefinition {
|
||||
name = "disablePersonalVaultExport";
|
||||
name = "disableExport";
|
||||
description = "disablePersonalVaultExportDescription";
|
||||
type = PolicyType.DisablePersonalVaultExport;
|
||||
component = DisablePersonalVaultExportPolicyComponent;
|
||||
|
||||
@@ -3,9 +3,7 @@ import { DeviceManagementComponentServiceAbstraction } from "./device-management
|
||||
/**
|
||||
* Default implementation of the device management component service
|
||||
*/
|
||||
export class DefaultDeviceManagementComponentService
|
||||
implements DeviceManagementComponentServiceAbstraction
|
||||
{
|
||||
export class DefaultDeviceManagementComponentService implements DeviceManagementComponentServiceAbstraction {
|
||||
/**
|
||||
* Show header information in web client
|
||||
*/
|
||||
|
||||
@@ -3,9 +3,7 @@ import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval
|
||||
/**
|
||||
* Default implementation of the LoginApprovalDialogComponentServiceAbstraction.
|
||||
*/
|
||||
export class DefaultLoginApprovalDialogComponentService
|
||||
implements LoginApprovalDialogComponentServiceAbstraction
|
||||
{
|
||||
export class DefaultLoginApprovalDialogComponentService implements LoginApprovalDialogComponentServiceAbstraction {
|
||||
/**
|
||||
* No-op implementation of the showLoginRequestedAlertIfWindowNotVisible method.
|
||||
* @returns
|
||||
|
||||
@@ -45,9 +45,7 @@ const VAULT_ROUTE = "/vault";
|
||||
* if it is required by showing a UI prompt. It is only one means of triggering migrations, in case the user stays unlocked for a while,
|
||||
* or regularly logs in without a master-password, when the migrations do require a master-password to run.
|
||||
*/
|
||||
export class DefaultEncryptedMigrationsSchedulerService
|
||||
implements EncryptedMigrationsSchedulerService
|
||||
{
|
||||
export class DefaultEncryptedMigrationsSchedulerService implements EncryptedMigrationsSchedulerService {
|
||||
isMigrating = false;
|
||||
url$: Observable<string>;
|
||||
|
||||
|
||||
@@ -184,7 +184,9 @@ import { DefaultChangeKdfApiService } from "@bitwarden/common/key-management/kdf
|
||||
import { ChangeKdfApiService } from "@bitwarden/common/key-management/kdf/change-kdf-api.service.abstraction";
|
||||
import { DefaultChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service";
|
||||
import { ChangeKdfService } from "@bitwarden/common/key-management/kdf/change-kdf.service.abstraction";
|
||||
import { KeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector-api.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { DefaultKeyConnectorApiService } from "@bitwarden/common/key-management/key-connector/services/default-key-connector-api.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
|
||||
import { KeyApiService } from "@bitwarden/common/key-management/keys/services/abstractions/key-api-service.abstraction";
|
||||
import { RotateableKeySetService } from "@bitwarden/common/key-management/keys/services/abstractions/rotateable-key-set.service";
|
||||
@@ -950,7 +952,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
FolderServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
@@ -970,7 +972,7 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
CipherServiceAbstraction,
|
||||
VaultExportApiService,
|
||||
PinServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
@@ -1355,16 +1357,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: PinServiceAbstraction,
|
||||
useClass: PinService,
|
||||
deps: [
|
||||
AccountServiceAbstraction,
|
||||
EncryptService,
|
||||
KdfConfigService,
|
||||
KeyGenerationService,
|
||||
LogService,
|
||||
KeyService,
|
||||
SdkService,
|
||||
PinStateServiceAbstraction,
|
||||
],
|
||||
deps: [EncryptService, LogService, KeyService, SdkService, PinStateServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: WebAuthnLoginPrfKeyServiceAbstraction,
|
||||
@@ -1835,6 +1828,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: IpcSessionRepository,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: KeyConnectorApiService,
|
||||
useClass: DefaultKeyConnectorApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PremiumInterestStateService,
|
||||
useClass: NoopPremiumInterestStateService,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user