mirror of
https://github.com/bitwarden/browser
synced 2026-02-09 13:10:17 +00:00
Merge branch 'main' into auth/pm-9115/implement-view-data-persistence-in-2FA-flows
This commit is contained in:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -84,7 +84,7 @@ libs/common/spec @bitwarden/team-platform-dev
|
||||
libs/common/src/state-migrations @bitwarden/team-platform-dev
|
||||
libs/platform @bitwarden/team-platform-dev
|
||||
# Node-specifc platform files
|
||||
libs/node @bitwarden/team-platform-dev
|
||||
libs/node @bitwarden/team-key-management-dev
|
||||
# Web utils used across app and connectors
|
||||
apps/web/src/utils/ @bitwarden/team-platform-dev
|
||||
# Web core and shared files
|
||||
|
||||
19
.github/workflows/chromatic.yml
vendored
19
.github/workflows/chromatic.yml
vendored
@@ -28,9 +28,22 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: get-changed-files-for-chromatic
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
storyFiles:
|
||||
- "apps/!(cli)/**"
|
||||
- "bitwarden_license/bit-web/src/app/**"
|
||||
- "libs/!(eslint)/**"
|
||||
- "package.json"
|
||||
- ".storybook/**"
|
||||
|
||||
- name: Get Node version
|
||||
id: retrieve-node-version
|
||||
if: steps.get-changed-files-for-chromatic.outputs.storyFiles == 'true'
|
||||
run: |
|
||||
NODE_NVMRC=$(cat .nvmrc)
|
||||
NODE_VERSION=${NODE_NVMRC/v/''}
|
||||
@@ -40,6 +53,7 @@ jobs:
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
if: steps.get-changed-files-for-chromatic.outputs.storyFiles == 'true'
|
||||
|
||||
- name: Cache NPM
|
||||
id: npm-cache
|
||||
@@ -47,12 +61,15 @@ jobs:
|
||||
with:
|
||||
path: "~/.npm"
|
||||
key: ${{ runner.os }}-npm-chromatic-${{ hashFiles('**/package-lock.json') }}
|
||||
if: steps.get-changed-files-for-chromatic.outputs.storyFiles == 'true'
|
||||
|
||||
- name: Install Node dependencies
|
||||
if: steps.get-changed-files-for-chromatic.outputs.storyFiles == 'true'
|
||||
run: npm ci
|
||||
|
||||
# Manually build the Storybook to resolve a bug related to TurboSnap
|
||||
- name: Build Storybook
|
||||
if: steps.get-changed-files-for-chromatic.outputs.storyFiles == 'true'
|
||||
run: npm run build-storybook:ci
|
||||
|
||||
- name: Publish to Chromatic
|
||||
@@ -64,3 +81,5 @@ jobs:
|
||||
exitOnceUploaded: true
|
||||
onlyChanged: true
|
||||
externals: "[\"libs/components/**/*.scss\", \"libs/components/**/*.css\", \"libs/components/tailwind.config*.js\"]"
|
||||
# Rather than use an `if` check on the whole publish step, we need to tell Chromatic to skip so that any Chromatic-spawned actions are properly skipped
|
||||
skip: ${{ steps.get-changed-files-for-chromatic.outputs.storyFiles == 'false' }}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DefaultLoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { SsoUrlService } from "@bitwarden/auth/common";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { firstValueFrom } from "rxjs";
|
||||
import { DefaultLoginComponentService, LoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { SsoUrlService } from "@bitwarden/auth/common";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@@ -840,7 +840,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
return;
|
||||
}
|
||||
|
||||
const clonedNode = formFieldElement.cloneNode() as FillableFormFieldElement;
|
||||
const clonedNode = formFieldElement.cloneNode(true) as FillableFormFieldElement;
|
||||
const identityLoginFields: AutofillFieldQualifierType[] = [
|
||||
AutofillFieldQualifier.identityUsername,
|
||||
AutofillFieldQualifier.identityEmail,
|
||||
|
||||
@@ -71,11 +71,13 @@ import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/bill
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||
import { FallbackBulkEncryptService } from "@bitwarden/common/key-management/crypto/services/fallback-bulk-encrypt.service";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
@@ -91,7 +93,6 @@ import {
|
||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Fido2ActiveRequestManager as Fido2ActiveRequestManagerAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
|
||||
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
|
||||
@@ -146,7 +147,6 @@ import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/no
|
||||
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||
import {
|
||||
ActiveUserStateProvider,
|
||||
DerivedStateProvider,
|
||||
|
||||
@@ -2,9 +2,9 @@ import { delay, filter, firstValueFrom, from, map, race, timer } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@@ -589,4 +589,113 @@ describe("BrowserApi", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* Safari sometimes returns >1 tabs unexpectedly even when
|
||||
* specificing a `windowId` or `currentWindow: true` query option.
|
||||
*
|
||||
* For example, when there are >=2 windows with an active pinned tab,
|
||||
* the pinned tab will always be included as the first entry in the array,
|
||||
* while the correct tab is included as the second entry.
|
||||
*
|
||||
* These tests can remain as verification when Safari fixes this bug.
|
||||
*/
|
||||
describe.each([{ isSafariApi: true }, { isSafariApi: false }])(
|
||||
"SafariTabsQuery %p",
|
||||
({ isSafariApi }) => {
|
||||
let originalIsSafariApi = BrowserApi.isSafariApi;
|
||||
const expectedWindowId = 10;
|
||||
const wrongWindowId = expectedWindowId + 1;
|
||||
const raceConditionWindowId = expectedWindowId + 2;
|
||||
const mismatchedWindowId = expectedWindowId + 3;
|
||||
|
||||
const resolvedTabsQueryResult = [
|
||||
mock<chrome.tabs.Tab>({
|
||||
title: "tab[0] is a pinned tab from another window",
|
||||
pinned: true,
|
||||
windowId: wrongWindowId,
|
||||
}),
|
||||
mock<chrome.tabs.Tab>({
|
||||
title: "tab[1] is the tab with the correct foreground window",
|
||||
windowId: expectedWindowId,
|
||||
}),
|
||||
];
|
||||
|
||||
function mockCurrentWindowId(id: number | null) {
|
||||
jest
|
||||
.spyOn(BrowserApi, "getCurrentWindow")
|
||||
.mockResolvedValue(mock<chrome.windows.Window>({ id }));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
originalIsSafariApi = BrowserApi.isSafariApi;
|
||||
BrowserApi.isSafariApi = isSafariApi;
|
||||
mockCurrentWindowId(expectedWindowId);
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue(resolvedTabsQueryResult);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
BrowserApi.isSafariApi = originalIsSafariApi;
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe.each([BrowserApi.getTabFromCurrentWindow, BrowserApi.getTabFromCurrentWindowId])(
|
||||
"%p",
|
||||
(getCurrTabFn) => {
|
||||
it("returns the first tab when the query result has one tab", async () => {
|
||||
const expectedSingleTab = resolvedTabsQueryResult[0];
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([expectedSingleTab]);
|
||||
const actualTab = await getCurrTabFn();
|
||||
expect(actualTab).toBe(expectedSingleTab);
|
||||
});
|
||||
|
||||
it("returns the first tab when the current window ID is mismatched", async () => {
|
||||
mockCurrentWindowId(mismatchedWindowId);
|
||||
const actualTab = await getCurrTabFn();
|
||||
expect(actualTab).toBe(resolvedTabsQueryResult[0]);
|
||||
});
|
||||
|
||||
it("returns the first tab when the current window ID is unavailable", async () => {
|
||||
mockCurrentWindowId(null);
|
||||
const actualTab = await getCurrTabFn();
|
||||
expect(actualTab).toBe(resolvedTabsQueryResult[0]);
|
||||
});
|
||||
|
||||
if (isSafariApi) {
|
||||
it("returns the tab with the current window ID", async () => {
|
||||
const actualTab = await getCurrTabFn();
|
||||
expect(actualTab.windowId).toBe(expectedWindowId);
|
||||
});
|
||||
|
||||
it(`returns the tab with the current window ID at the time of calling [Function ${getCurrTabFn.name}]`, async () => {
|
||||
jest.spyOn(BrowserApi, "tabsQuery").mockImplementation(() => {
|
||||
/*
|
||||
* Simulate rapid clicking/switching between windows, e.g.
|
||||
* 1. From Window A, call `getCurrTabFn()`
|
||||
* 2. getCurrTabFn() calls `await BrowserApi.tabsQuery()`
|
||||
* 3. Users switches to Window B before the `await` returns
|
||||
* 4. getCurrTabFn() calls `await BrowserApi.getCurrentWindow()`
|
||||
* ^ This now returns Window B and filters the results erroneously
|
||||
*/
|
||||
mockCurrentWindowId(raceConditionWindowId);
|
||||
|
||||
return Promise.resolve(resolvedTabsQueryResult);
|
||||
});
|
||||
|
||||
const actualTab = await getCurrTabFn();
|
||||
expect(actualTab.windowId).toBe(expectedWindowId);
|
||||
});
|
||||
} /* !isSafariApi */ else {
|
||||
it("falls back to tabsQueryFirst", async () => {
|
||||
const tabsQueryFirstSpy = jest.spyOn(BrowserApi, "tabsQueryFirst");
|
||||
const actualTab = await getCurrTabFn();
|
||||
|
||||
expect(tabsQueryFirstSpy).toHaveBeenCalled();
|
||||
expect(actualTab).toBe(resolvedTabsQueryResult[0]);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -125,7 +125,7 @@ export class BrowserApi {
|
||||
}
|
||||
|
||||
static async getTabFromCurrentWindowId(): Promise<chrome.tabs.Tab> | null {
|
||||
return await BrowserApi.tabsQueryFirst({
|
||||
return await BrowserApi.tabsQueryFirstCurrentWindowForSafari({
|
||||
active: true,
|
||||
windowId: chrome.windows.WINDOW_ID_CURRENT,
|
||||
});
|
||||
@@ -153,7 +153,7 @@ export class BrowserApi {
|
||||
}
|
||||
|
||||
static async getTabFromCurrentWindow(): Promise<chrome.tabs.Tab> | null {
|
||||
return await BrowserApi.tabsQueryFirst({
|
||||
return await BrowserApi.tabsQueryFirstCurrentWindowForSafari({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
@@ -197,6 +197,51 @@ export class BrowserApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop-in replacement for {@link BrowserApi.tabsQueryFirst}.
|
||||
*
|
||||
* Safari sometimes returns >1 tabs unexpectedly even when
|
||||
* specificing a `windowId` or `currentWindow: true` query option.
|
||||
*
|
||||
* For all of these calls,
|
||||
* ```
|
||||
* await chrome.tabs.query({active: true, currentWindow: true})
|
||||
* await chrome.tabs.query({active: true, windowId: chrome.windows.WINDOW_ID_CURRENT})
|
||||
* await chrome.tabs.query({active: true, windowId: 10})
|
||||
* ```
|
||||
*
|
||||
* Safari could return:
|
||||
* ```
|
||||
* [
|
||||
* {windowId: 2, pinned: true, title: "Incorrect tab in another window", …},
|
||||
* {windowId: 10, title: "Correct tab in foreground", …},
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* This function captures the current window ID manually before running the query,
|
||||
* then finds and returns the tab with the matching window ID.
|
||||
*
|
||||
* See the `SafariTabsQuery` tests in `browser-api.spec.ts`.
|
||||
*
|
||||
* This workaround can be removed when Safari fixes this bug.
|
||||
*/
|
||||
static async tabsQueryFirstCurrentWindowForSafari(
|
||||
options: chrome.tabs.QueryInfo,
|
||||
): Promise<chrome.tabs.Tab> | null {
|
||||
if (!BrowserApi.isSafariApi) {
|
||||
return await BrowserApi.tabsQueryFirst(options);
|
||||
}
|
||||
|
||||
const currentWindowId = (await BrowserApi.getCurrentWindow()).id;
|
||||
const tabs = await BrowserApi.tabsQuery(options);
|
||||
|
||||
if (tabs.length <= 1 || currentWindowId == null) {
|
||||
return tabs[0];
|
||||
}
|
||||
|
||||
return tabs.find((t) => t.windowId === currentWindowId) ?? tabs[0];
|
||||
}
|
||||
|
||||
static tabSendMessageData(
|
||||
tab: chrome.tabs.Tab,
|
||||
command: string,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -21,11 +21,3 @@
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.recovery-code-img {
|
||||
@include themify($themes) {
|
||||
content: url("../images/two-factor/rc" + themed("mfaLogoSuffix"));
|
||||
max-width: 100px;
|
||||
max-height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,9 @@ import {
|
||||
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
VaultTimeoutService,
|
||||
@@ -74,7 +76,6 @@ import {
|
||||
DefaultAnimationControlService,
|
||||
} from "@bitwarden/common/platform/abstractions/animation-control.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -101,7 +102,6 @@ import { ContainerService } from "@bitwarden/common/platform/services/container.
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||
import {
|
||||
DerivedStateProvider,
|
||||
GlobalStateProvider,
|
||||
|
||||
@@ -488,5 +488,79 @@ const mapAddEditCipherInfoToInitialValues = (
|
||||
initialValues.username = cipher.identity.username;
|
||||
}
|
||||
|
||||
if (cipher.type == CipherType.Identity) {
|
||||
const identity = cipher.identity;
|
||||
|
||||
if (identity != null) {
|
||||
if (identity.title != null) {
|
||||
initialValues.title = identity.title;
|
||||
}
|
||||
|
||||
if (identity.firstName != null) {
|
||||
initialValues.firstName = identity.firstName;
|
||||
}
|
||||
|
||||
if (identity.middleName != null) {
|
||||
initialValues.middleName = identity.middleName;
|
||||
}
|
||||
|
||||
if (identity.lastName != null) {
|
||||
initialValues.lastName = identity.lastName;
|
||||
}
|
||||
|
||||
if (identity.company != null) {
|
||||
initialValues.company = identity.company;
|
||||
}
|
||||
|
||||
if (identity.ssn != null) {
|
||||
initialValues.ssn = identity.ssn;
|
||||
}
|
||||
|
||||
if (identity.passportNumber != null) {
|
||||
initialValues.passportNumber = identity.passportNumber;
|
||||
}
|
||||
|
||||
if (identity.licenseNumber != null) {
|
||||
initialValues.licenseNumber = identity.licenseNumber;
|
||||
}
|
||||
|
||||
if (identity.email != null) {
|
||||
initialValues.email = identity.email;
|
||||
}
|
||||
|
||||
if (identity.phone != null) {
|
||||
initialValues.phone = identity.phone;
|
||||
}
|
||||
|
||||
if (identity.address1 != null) {
|
||||
initialValues.address1 = identity.address1;
|
||||
}
|
||||
|
||||
if (identity.address2 != null) {
|
||||
initialValues.address2 = identity.address2;
|
||||
}
|
||||
|
||||
if (identity.address3 != null) {
|
||||
initialValues.address3 = identity.address3;
|
||||
}
|
||||
|
||||
if (identity.city != null) {
|
||||
initialValues.city = identity.city;
|
||||
}
|
||||
|
||||
if (identity.state != null) {
|
||||
initialValues.state = identity.state;
|
||||
}
|
||||
|
||||
if (identity.postalCode != null) {
|
||||
initialValues.postalCode = identity.postalCode;
|
||||
}
|
||||
|
||||
if (identity.country != null) {
|
||||
initialValues.country = identity.country;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return initialValues;
|
||||
};
|
||||
|
||||
@@ -31,9 +31,9 @@ import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two
|
||||
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -7,9 +7,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
|
||||
@@ -5,9 +5,9 @@ import * as inquirer from "inquirer";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
4
apps/desktop/desktop_native/Cargo.lock
generated
4
apps/desktop/desktop_native/Cargo.lock
generated
@@ -2965,9 +2965,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.43.0"
|
||||
version = "1.43.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
|
||||
checksum = "492a604e2fd7f814268a378409e6c92b5525d747d10db9a229723f55a417958c"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
|
||||
@@ -51,7 +51,7 @@ ssh-encoding = "=0.2.0"
|
||||
ssh-key = {version = "=0.6.7", default-features = false }
|
||||
sysinfo = "0.33.1"
|
||||
thiserror = "=1.0.69"
|
||||
tokio = "=1.43.0"
|
||||
tokio = "=1.43.1"
|
||||
tokio-stream = "=0.1.15"
|
||||
tokio-util = "=0.7.13"
|
||||
typenum = "=1.17.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.4.0",
|
||||
"version": "2025.4.1",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
|
||||
export class RendererCryptoFunctionService
|
||||
extends WebCryptoFunctionService
|
||||
|
||||
@@ -52,6 +52,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
|
||||
@@ -60,7 +61,6 @@ import {
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
|
||||
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DefaultLoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { SsoUrlService } from "@bitwarden/auth/common";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
@@ -107,22 +107,17 @@ describe("DesktopLoginComponentService", () => {
|
||||
});
|
||||
|
||||
describe("redirectToSso", () => {
|
||||
// Array of all permutations of isAppImage, isSnapStore, and isDev
|
||||
// Array of all permutations of isAppImage and isDev
|
||||
const permutations = [
|
||||
[true, false, false], // Case 1: isAppImage true
|
||||
[false, true, false], // Case 2: isSnapStore true
|
||||
[false, false, true], // Case 3: isDev true
|
||||
[true, true, false], // Case 4: isAppImage and isSnapStore true
|
||||
[true, false, true], // Case 5: isAppImage and isDev true
|
||||
[false, true, true], // Case 6: isSnapStore and isDev true
|
||||
[true, true, true], // Case 7: all true
|
||||
[false, false, false], // Case 8: all false
|
||||
[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, isSnapStore, isDev]) => {
|
||||
it(`executes correct logic for isAppImage=${isAppImage}, isSnapStore=${isSnapStore}, isDev=${isDev}`, async () => {
|
||||
permutations.forEach(([isAppImage, isDev]) => {
|
||||
it(`executes correct logic for isAppImage=${isAppImage}, isDev=${isDev}`, async () => {
|
||||
(global as any).ipc.platform.isAppImage = isAppImage;
|
||||
(global as any).ipc.platform.isSnapStore = isSnapStore;
|
||||
(global as any).ipc.platform.isDev = isDev;
|
||||
|
||||
const email = "test@bitwarden.com";
|
||||
@@ -136,7 +131,7 @@ describe("DesktopLoginComponentService", () => {
|
||||
|
||||
await service.redirectToSsoLogin(email);
|
||||
|
||||
if (isAppImage || isSnapStore || isDev) {
|
||||
if (isAppImage || isDev) {
|
||||
expect(ipc.platform.localhostCallbackService.openSsoPrompt).toHaveBeenCalledWith(
|
||||
codeChallenge,
|
||||
state,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { firstValueFrom } from "rxjs";
|
||||
import { DefaultLoginComponentService, LoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { DESKTOP_SSO_CALLBACK, SsoUrlService } from "@bitwarden/auth/common";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -51,7 +51,7 @@ export class DesktopLoginComponentService
|
||||
): 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.isSnapStore || ipc.platform.isDev) {
|
||||
if (ipc.platform.isAppImage || ipc.platform.isDev) {
|
||||
await this.initiateSsoThroughLocalhostCallback(email, state, codeChallenge);
|
||||
} else {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.6 KiB |
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.4.0",
|
||||
"version": "2025.4.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.4.0",
|
||||
"version": "2025.4.1",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bitwarden/desktop-napi": "file:../desktop_native/napi"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/desktop",
|
||||
"productName": "Bitwarden",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.4.0",
|
||||
"version": "2025.4.1",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { crypto } from "@bitwarden/desktop-napi";
|
||||
import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@@ -21,11 +21,3 @@
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.recovery-code-img {
|
||||
@include themify($themes) {
|
||||
content: url("../images/two-factor/rc" + themed("mfaLogoSuffix"));
|
||||
max-width: 100px;
|
||||
max-height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { of } from "rxjs";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -4,8 +4,8 @@ import { combineLatest, concatMap, firstValueFrom, map } from "rxjs";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { NativeMessagingVersion } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -17,6 +17,7 @@ export class OrganizationUserAdminView {
|
||||
type: OrganizationUserType;
|
||||
status: OrganizationUserStatusType;
|
||||
externalId: string;
|
||||
ssoExternalId: string;
|
||||
permissions: PermissionsApi;
|
||||
resetPasswordEnrolled: boolean;
|
||||
hasMasterPassword: boolean;
|
||||
@@ -39,6 +40,7 @@ export class OrganizationUserAdminView {
|
||||
view.type = response.type;
|
||||
view.status = response.status;
|
||||
view.externalId = response.externalId;
|
||||
view.ssoExternalId = response.ssoExternalId;
|
||||
view.permissions = response.permissions;
|
||||
view.resetPasswordEnrolled = response.resetPasswordEnrolled;
|
||||
view.collections = response.collections.map((c) => ({
|
||||
|
||||
@@ -177,11 +177,17 @@
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
</ng-container>
|
||||
<bit-form-field>
|
||||
<bit-form-field *ngIf="isExternalIdVisible$ | async">
|
||||
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="externalId" />
|
||||
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field *ngIf="isSsoExternalIdVisible$ | async">
|
||||
<bit-label>{{ "ssoExternalId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="ssoExternalId" />
|
||||
<bit-hint>{{ "ssoExternalIdDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</bit-tab>
|
||||
<bit-tab *ngIf="organization.useGroups" [label]="'groups' | i18n">
|
||||
<div class="tw-mb-6">
|
||||
|
||||
@@ -125,6 +125,7 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
emails: [""],
|
||||
type: OrganizationUserType.User,
|
||||
externalId: this.formBuilder.control({ value: "", disabled: true }),
|
||||
ssoExternalId: this.formBuilder.control({ value: "", disabled: true }),
|
||||
accessSecretsManager: false,
|
||||
access: [[] as AccessItemValue[]],
|
||||
groups: [[] as AccessItemValue[]],
|
||||
@@ -155,6 +156,22 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
FeatureFlag.AccountDeprovisioning,
|
||||
);
|
||||
|
||||
protected isExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(
|
||||
map((isEnabled) => {
|
||||
return !isEnabled || !!this.formGroup.get("externalId")?.value;
|
||||
}),
|
||||
);
|
||||
|
||||
protected isSsoExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(
|
||||
map((isEnabled) => {
|
||||
return isEnabled && !!this.formGroup.get("ssoExternalId")?.value;
|
||||
}),
|
||||
);
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
get customUserTypeSelected(): boolean {
|
||||
@@ -402,6 +419,7 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
this.formGroup.patchValue({
|
||||
type: userDetails.type,
|
||||
externalId: userDetails.externalId,
|
||||
ssoExternalId: userDetails.ssoExternalId,
|
||||
access: accessSelections,
|
||||
accessSecretsManager: userDetails.accessSecretsManager,
|
||||
groups: groupAccessSelections,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { ItemModule } from "@bitwarden/components";
|
||||
|
||||
import { LooseComponentsModule, SharedModule } from "../../../shared";
|
||||
import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component";
|
||||
import { PoliciesModule } from "../../organizations/policies";
|
||||
@@ -15,6 +17,7 @@ import { TwoFactorSetupComponent } from "./two-factor-setup.component";
|
||||
PoliciesModule,
|
||||
OrganizationSettingsRoutingModule,
|
||||
AccountFingerprintComponent,
|
||||
ItemModule,
|
||||
],
|
||||
declarations: [AccountComponent, TwoFactorSetupComponent],
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BehaviorSubject } from "rxjs";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@@ -1,36 +1,50 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorRecoverResponse } from "@bitwarden/common/auth/models/response/two-factor-recover.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
ButtonModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-recovery",
|
||||
templateUrl: "two-factor-recovery.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, DialogModule, ButtonModule, TypographyModule, I18nPipe],
|
||||
})
|
||||
export class TwoFactorRecoveryComponent {
|
||||
type = -1;
|
||||
code: string;
|
||||
authed: boolean;
|
||||
code: string = "";
|
||||
authed: boolean = false;
|
||||
twoFactorProviderType = TwoFactorProviderType;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: any,
|
||||
@Inject(DIALOG_DATA) protected data: { response: { response: TwoFactorRecoverResponse } },
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
this.auth(data.response);
|
||||
}
|
||||
|
||||
auth(authResponse: any) {
|
||||
auth(authResponse: { response: TwoFactorRecoverResponse }) {
|
||||
this.authed = true;
|
||||
this.processResponse(authResponse.response);
|
||||
}
|
||||
|
||||
print() {
|
||||
const w = window.open();
|
||||
if (!w) {
|
||||
// return early if the window is not open
|
||||
return;
|
||||
}
|
||||
w.document.write(
|
||||
'<div style="font-size: 18px; text-align: center;">' +
|
||||
"<p>" +
|
||||
@@ -47,9 +61,9 @@ export class TwoFactorRecoveryComponent {
|
||||
w.print();
|
||||
}
|
||||
|
||||
private formatString(s: string) {
|
||||
private formatString(s: string): string {
|
||||
if (s == null) {
|
||||
return null;
|
||||
return "";
|
||||
}
|
||||
return s
|
||||
.replace(/(.{4})/g, "$1 ")
|
||||
@@ -61,7 +75,13 @@ export class TwoFactorRecoveryComponent {
|
||||
this.code = this.formatString(response.code);
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<any>) {
|
||||
static open(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<
|
||||
{ response: { response: TwoFactorRecoverResponse } },
|
||||
DialogRef<unknown, TwoFactorRecoveryComponent>
|
||||
>,
|
||||
) {
|
||||
return dialogService.open(TwoFactorRecoveryComponent, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
{{ "twoStepAuthenticatorInstructionSuffix" | i18n }}
|
||||
</p>
|
||||
|
||||
<p class="text-center">
|
||||
<p class="tw-text-center">
|
||||
<a
|
||||
href="https://apps.apple.com/ca/app/bitwarden-authenticator/id6497335175"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, FormControl, Validators } from "@angular/forms";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
@@ -18,12 +20,22 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
InputModule,
|
||||
LinkModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
|
||||
|
||||
@@ -44,6 +56,22 @@ declare global {
|
||||
@Component({
|
||||
selector: "app-two-factor-setup-authenticator",
|
||||
templateUrl: "two-factor-setup-authenticator.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
InputModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
CalloutModule,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
I18nPipe,
|
||||
AsyncActionsModule,
|
||||
JslibModule,
|
||||
],
|
||||
})
|
||||
export class TwoFactorSetupAuthenticatorComponent
|
||||
extends TwoFactorSetupMethodBaseComponent
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<bit-dialog [title]="'twoStepLogin' | i18n" [subtitle]="'Duo'">
|
||||
<ng-container bitDialogContent>
|
||||
<ng-container *ngIf="enabled">
|
||||
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
|
||||
<bit-callout type="success" [title]="'enabled' | i18n" icon="bwi bwi-check-circle">
|
||||
{{ "twoStepLoginProviderEnabled" | i18n }}
|
||||
</app-callout>
|
||||
</bit-callout>
|
||||
<img class="tw-float-right tw-ml-3 mfaType2" alt="Duo logo" />
|
||||
<strong>{{ "twoFactorDuoClientId" | i18n }}:</strong> {{ clientId }}
|
||||
<br />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
@@ -13,18 +12,41 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
InputModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-setup-duo",
|
||||
templateUrl: "two-factor-setup-duo.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
InputModule,
|
||||
TypographyModule,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
I18nPipe,
|
||||
ReactiveFormsModule,
|
||||
AsyncActionsModule,
|
||||
CalloutModule,
|
||||
],
|
||||
})
|
||||
export class TwoFactorSetupDuoComponent
|
||||
extends TwoFactorSetupMethodBaseComponent
|
||||
@@ -63,23 +85,23 @@ export class TwoFactorSetupDuoComponent
|
||||
);
|
||||
}
|
||||
|
||||
get clientId() {
|
||||
return this.formGroup.get("clientId").value;
|
||||
get clientId(): string {
|
||||
return this.formGroup.get("clientId")?.value || "";
|
||||
}
|
||||
get clientSecret() {
|
||||
return this.formGroup.get("clientSecret").value;
|
||||
get clientSecret(): string {
|
||||
return this.formGroup.get("clientSecret")?.value || "";
|
||||
}
|
||||
get host() {
|
||||
return this.formGroup.get("host").value;
|
||||
get host(): string {
|
||||
return this.formGroup.get("host")?.value || "";
|
||||
}
|
||||
set clientId(value: string) {
|
||||
this.formGroup.get("clientId").setValue(value);
|
||||
this.formGroup.get("clientId")?.setValue(value);
|
||||
}
|
||||
set clientSecret(value: string) {
|
||||
this.formGroup.get("clientSecret").setValue(value);
|
||||
this.formGroup.get("clientSecret")?.setValue(value);
|
||||
}
|
||||
set host(value: string) {
|
||||
this.formGroup.get("host").setValue(value);
|
||||
this.formGroup.get("host")?.setValue(value);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -147,7 +169,10 @@ export class TwoFactorSetupDuoComponent
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<TwoFactorDuoComponentConfig>,
|
||||
) => {
|
||||
return dialogService.open<boolean>(TwoFactorSetupDuoComponent, config);
|
||||
return dialogService.open<boolean, TwoFactorDuoComponentConfig>(
|
||||
TwoFactorSetupDuoComponent,
|
||||
config as DialogConfig<TwoFactorDuoComponentConfig, DialogRef<boolean>>,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -16,19 +15,41 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
InputModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-setup-email",
|
||||
templateUrl: "two-factor-setup-email.component.html",
|
||||
outputs: ["onUpdated"],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
I18nPipe,
|
||||
InputModule,
|
||||
ReactiveFormsModule,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class TwoFactorSetupEmailComponent
|
||||
extends TwoFactorSetupMethodBaseComponent
|
||||
@@ -36,8 +57,8 @@ export class TwoFactorSetupEmailComponent
|
||||
{
|
||||
@Output() onChangeStatus: EventEmitter<boolean> = new EventEmitter();
|
||||
type = TwoFactorProviderType.Email;
|
||||
sentEmail: string;
|
||||
emailPromise: Promise<unknown>;
|
||||
sentEmail: string = "";
|
||||
emailPromise: Promise<unknown> | undefined;
|
||||
override componentName = "app-two-factor-email";
|
||||
formGroup = this.formBuilder.group({
|
||||
token: ["", [Validators.required]],
|
||||
@@ -67,17 +88,17 @@ export class TwoFactorSetupEmailComponent
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
get token() {
|
||||
return this.formGroup.get("token").value;
|
||||
get token(): string {
|
||||
return this.formGroup.get("token")?.value || "";
|
||||
}
|
||||
set token(value: string) {
|
||||
this.formGroup.get("token").setValue(value);
|
||||
set token(value: string | null) {
|
||||
this.formGroup.get("token")?.setValue(value || "");
|
||||
}
|
||||
get email() {
|
||||
return this.formGroup.get("email").value;
|
||||
get email(): string {
|
||||
return this.formGroup.get("email")?.value || "";
|
||||
}
|
||||
set email(value: string) {
|
||||
this.formGroup.get("email").setValue(value);
|
||||
set email(value: string | null | undefined) {
|
||||
this.formGroup.get("email")?.setValue(value || "");
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -149,6 +170,9 @@ export class TwoFactorSetupEmailComponent
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AuthResponse<TwoFactorEmailResponse>>,
|
||||
) {
|
||||
return dialogService.open<boolean>(TwoFactorSetupEmailComponent, config);
|
||||
return dialogService.open<boolean, AuthResponse<TwoFactorEmailResponse>>(
|
||||
TwoFactorSetupEmailComponent,
|
||||
config as DialogConfig<AuthResponse<TwoFactorEmailResponse>, DialogRef<boolean>>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -17,18 +15,20 @@ import { DialogService, ToastService } from "@bitwarden/components";
|
||||
/**
|
||||
* Base class for two-factor setup components (ex: email, yubikey, webauthn, duo).
|
||||
*/
|
||||
@Directive()
|
||||
@Directive({
|
||||
standalone: true,
|
||||
})
|
||||
export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
@Output() onUpdated = new EventEmitter<boolean>();
|
||||
|
||||
type: TwoFactorProviderType;
|
||||
organizationId: string;
|
||||
type: TwoFactorProviderType | undefined;
|
||||
organizationId: string | null = null;
|
||||
twoFactorProviderType = TwoFactorProviderType;
|
||||
enabled = false;
|
||||
authed = false;
|
||||
|
||||
protected hashedSecret: string;
|
||||
protected verificationType: VerificationType;
|
||||
protected hashedSecret: string | undefined;
|
||||
protected verificationType: VerificationType | undefined;
|
||||
protected componentName = "";
|
||||
|
||||
constructor(
|
||||
@@ -74,6 +74,9 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
|
||||
try {
|
||||
const request = await this.buildRequestModel(TwoFactorProviderRequest);
|
||||
if (this.type === undefined) {
|
||||
throw new Error("Two-factor provider type is required");
|
||||
}
|
||||
request.type = this.type;
|
||||
if (this.organizationId != null) {
|
||||
promise = this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
|
||||
@@ -84,7 +87,7 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
this.enabled = false;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("twoStepDisabled"),
|
||||
});
|
||||
this.onUpdated.emit(false);
|
||||
@@ -105,6 +108,9 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
}
|
||||
|
||||
const request = await this.buildRequestModel(TwoFactorProviderRequest);
|
||||
if (this.type === undefined) {
|
||||
throw new Error("Two-factor provider type is required");
|
||||
}
|
||||
request.type = this.type;
|
||||
if (this.organizationId != null) {
|
||||
await this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
|
||||
@@ -114,7 +120,7 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
this.enabled = false;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("twoStepDisabled"),
|
||||
});
|
||||
this.onUpdated.emit(false);
|
||||
@@ -123,6 +129,9 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
protected async buildRequestModel<T extends SecretVerificationRequest>(
|
||||
requestClass: new () => T,
|
||||
) {
|
||||
if (this.hashedSecret === undefined || this.verificationType === undefined) {
|
||||
throw new Error("User verification data is missing");
|
||||
}
|
||||
return this.userVerificationService.buildRequest(
|
||||
{
|
||||
secret: this.hashedSecret,
|
||||
|
||||
@@ -5,23 +5,23 @@
|
||||
[subtitle]="'webAuthnTitle' | i18n"
|
||||
>
|
||||
<ng-container bitDialogContent>
|
||||
<app-callout
|
||||
<bit-callout
|
||||
type="success"
|
||||
title="{{ 'enabled' | i18n }}"
|
||||
icon="bwi bwi-check-circle"
|
||||
*ngIf="enabled"
|
||||
>
|
||||
{{ "twoStepLoginProviderEnabled" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="warning">
|
||||
</bit-callout>
|
||||
<bit-callout type="warning">
|
||||
<p bitTypography="body1">{{ "twoFactorWebAuthnWarning1" | i18n }}</p>
|
||||
</app-callout>
|
||||
</bit-callout>
|
||||
<img class="tw-float-right tw-ml-5 mfaType7" alt="FIDO2 WebAuthn logo" />
|
||||
<ul class="bwi-ul">
|
||||
<li *ngFor="let k of keys; let i = index" #removeKeyBtn [appApiAction]="k.removePromise">
|
||||
<i class="bwi bwi-li bwi-key"></i>
|
||||
<span *ngIf="!k.configured || !k.name" bitTypography="body1" class="tw-font-bold">
|
||||
{{ "webAuthnkeyX" | i18n: i + 1 }}
|
||||
{{ "webAuthnkeyX" | i18n: (i + 1).toString() }}
|
||||
</span>
|
||||
<span *ngIf="k.configured && k.name" bitTypography="body1" class="tw-font-bold">
|
||||
{{ k.name }}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, NgZone } from "@angular/core";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
@@ -18,12 +18,20 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
LinkModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
|
||||
|
||||
@@ -38,24 +46,36 @@ interface Key {
|
||||
@Component({
|
||||
selector: "app-two-factor-setup-webauthn",
|
||||
templateUrl: "two-factor-setup-webauthn.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
I18nPipe,
|
||||
JslibModule,
|
||||
LinkModule,
|
||||
ReactiveFormsModule,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseComponent {
|
||||
type = TwoFactorProviderType.WebAuthn;
|
||||
name: string;
|
||||
keys: Key[];
|
||||
keyIdAvailable: number = null;
|
||||
name: string = "";
|
||||
keys: Key[] = [];
|
||||
keyIdAvailable: number | null = null;
|
||||
keysConfiguredCount = 0;
|
||||
webAuthnError: boolean;
|
||||
webAuthnListening: boolean;
|
||||
webAuthnResponse: PublicKeyCredential;
|
||||
challengePromise: Promise<ChallengeResponse>;
|
||||
formPromise: Promise<TwoFactorWebAuthnResponse>;
|
||||
webAuthnError: boolean = false;
|
||||
webAuthnListening: boolean = false;
|
||||
webAuthnResponse: PublicKeyCredential | null = null;
|
||||
challengePromise: Promise<ChallengeResponse> | undefined;
|
||||
formPromise: Promise<TwoFactorWebAuthnResponse> | undefined;
|
||||
|
||||
override componentName = "app-two-factor-webauthn";
|
||||
|
||||
protected formGroup = new FormGroup({
|
||||
name: new FormControl({ value: "", disabled: !this.keyIdAvailable }),
|
||||
});
|
||||
protected formGroup: FormGroup;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorWebAuthnResponse>,
|
||||
@@ -78,6 +98,9 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
dialogService,
|
||||
toastService,
|
||||
);
|
||||
this.formGroup = new FormGroup({
|
||||
name: new FormControl({ value: "", disabled: false }),
|
||||
});
|
||||
this.auth(data);
|
||||
}
|
||||
|
||||
@@ -96,9 +119,14 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
|
||||
protected async enable() {
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest);
|
||||
|
||||
if (this.webAuthnResponse == undefined || this.keyIdAvailable == undefined) {
|
||||
throw new Error("WebAuthn response or key ID is missing");
|
||||
}
|
||||
|
||||
request.deviceResponse = this.webAuthnResponse;
|
||||
request.id = this.keyIdAvailable;
|
||||
request.name = this.formGroup.value.name;
|
||||
request.name = this.formGroup.value.name || "";
|
||||
|
||||
const response = await this.apiService.putTwoFactorWebAuthn(request);
|
||||
this.processResponse(response);
|
||||
@@ -164,10 +192,10 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
.create({
|
||||
publicKey: webAuthnChallenge,
|
||||
})
|
||||
.then((data: PublicKeyCredential) => {
|
||||
.then((data) => {
|
||||
this.ngZone.run(() => {
|
||||
this.webAuthnListening = false;
|
||||
this.webAuthnResponse = data;
|
||||
this.webAuthnResponse = data as PublicKeyCredential;
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -189,8 +217,11 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
this.resetWebAuthn();
|
||||
this.keys = [];
|
||||
this.keyIdAvailable = null;
|
||||
this.formGroup.get("name").enable();
|
||||
this.formGroup.get("name").setValue(null);
|
||||
const nameControl = this.formGroup.get("name");
|
||||
if (nameControl) {
|
||||
nameControl.enable();
|
||||
nameControl.setValue("");
|
||||
}
|
||||
this.keysConfiguredCount = 0;
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
if (response.keys != null) {
|
||||
@@ -207,7 +238,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.keys.push({ id: i, name: null, configured: false, removePromise: null });
|
||||
this.keys.push({ id: i, name: "", configured: false, removePromise: null });
|
||||
if (this.keyIdAvailable == null) {
|
||||
this.keyIdAvailable = i;
|
||||
}
|
||||
@@ -220,6 +251,9 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AuthResponse<TwoFactorWebAuthnResponse>>,
|
||||
) {
|
||||
return dialogService.open<boolean>(TwoFactorSetupWebAuthnComponent, config);
|
||||
return dialogService.open<boolean, AuthResponse<TwoFactorWebAuthnResponse>>(
|
||||
TwoFactorSetupWebAuthnComponent,
|
||||
config as DialogConfig<AuthResponse<TwoFactorWebAuthnResponse>, DialogRef<boolean>>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<form *ngIf="authed" [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="large" [title]="'twoStepLogin' | i18n" [subtitle]="'YubiKey'">
|
||||
<ng-container bitDialogContent>
|
||||
<app-callout
|
||||
<bit-callout
|
||||
*ngIf="enabled"
|
||||
type="success"
|
||||
title="{{ 'enabled' | i18n }}"
|
||||
icon="bwi bwi-check-circle"
|
||||
>
|
||||
{{ "twoStepLoginProviderEnabled" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="warning">
|
||||
</bit-callout>
|
||||
<bit-callout type="warning">
|
||||
<p bitTypography="body1">{{ "twoFactorYubikeyWarning" | i18n }}</p>
|
||||
<ul class="tw-mb-0" bitTypography="body1">
|
||||
<li>{{ "twoFactorYubikeySupportUsb" | i18n }}</li>
|
||||
<li>{{ "twoFactorYubikeySupportMobile" | i18n }}</li>
|
||||
</ul>
|
||||
</app-callout>
|
||||
</bit-callout>
|
||||
<img class="tw-float-right mfaType3" alt="YubiKey OTP security key logo" />
|
||||
<p bitTypography="body1">{{ "twoFactorYubikeyAdd" | i18n }}:</p>
|
||||
<ol bitTypography="body1">
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" formArrayName="formKeys">
|
||||
<div class="tw-col-span-6" *ngFor="let k of keys; let i = index">
|
||||
<div [formGroupName]="i">
|
||||
<bit-label>{{ "yubikeyX" | i18n: i + 1 }}</bit-label>
|
||||
<bit-label>{{ "yubikeyX" | i18n: (i + 1).toString() }}</bit-label>
|
||||
<bit-form-field *ngIf="!keys[i].existingKey">
|
||||
<input bitInput type="password" formControlName="key" appInputVerbatim />
|
||||
</bit-form-field>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormArray, FormBuilder, FormControl, FormGroup } from "@angular/forms";
|
||||
import {
|
||||
FormArray,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
} from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
@@ -12,7 +18,24 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
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 { DIALOG_DATA, DialogConfig, DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
CheckboxModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
InputModule,
|
||||
LinkModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
|
||||
|
||||
@@ -24,30 +47,49 @@ interface Key {
|
||||
@Component({
|
||||
selector: "app-two-factor-setup-yubikey",
|
||||
templateUrl: "two-factor-setup-yubikey.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
JslibModule,
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
CalloutModule,
|
||||
CheckboxModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
InputModule,
|
||||
AsyncActionsModule,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class TwoFactorSetupYubiKeyComponent
|
||||
extends TwoFactorSetupMethodBaseComponent
|
||||
implements OnInit
|
||||
{
|
||||
type = TwoFactorProviderType.Yubikey;
|
||||
keys: Key[];
|
||||
keys: Key[] = [];
|
||||
anyKeyHasNfc = false;
|
||||
|
||||
formPromise: Promise<TwoFactorYubiKeyResponse>;
|
||||
disablePromise: Promise<unknown>;
|
||||
formPromise: Promise<TwoFactorYubiKeyResponse> | undefined;
|
||||
disablePromise: Promise<unknown> | undefined;
|
||||
|
||||
override componentName = "app-two-factor-yubikey";
|
||||
formGroup: FormGroup<{
|
||||
formKeys: FormArray<FormControl<Key>>;
|
||||
anyKeyHasNfc: FormControl<boolean>;
|
||||
}>;
|
||||
formGroup:
|
||||
| FormGroup<{
|
||||
formKeys: FormArray<FormControl<Key | null>>;
|
||||
anyKeyHasNfc: FormControl<boolean | null>;
|
||||
}>
|
||||
| undefined;
|
||||
|
||||
get keysFormControl() {
|
||||
return this.formGroup.controls.formKeys.controls;
|
||||
return this.formGroup?.controls.formKeys.controls;
|
||||
}
|
||||
|
||||
get anyKeyHasNfcFormControl() {
|
||||
return this.formGroup.controls.anyKeyHasNfc;
|
||||
return this.formGroup?.controls.anyKeyHasNfc;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -82,6 +124,9 @@ export class TwoFactorSetupYubiKeyComponent
|
||||
}
|
||||
|
||||
refreshFormArrayData() {
|
||||
if (!this.formGroup) {
|
||||
return;
|
||||
}
|
||||
const formKeys = <FormArray>this.formGroup.get("formKeys");
|
||||
formKeys.clear();
|
||||
this.keys.forEach((val) => {
|
||||
@@ -99,6 +144,9 @@ export class TwoFactorSetupYubiKeyComponent
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (!this.formGroup) {
|
||||
return;
|
||||
}
|
||||
this.formGroup.markAllAsTouched();
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
@@ -117,14 +165,17 @@ export class TwoFactorSetupYubiKeyComponent
|
||||
};
|
||||
|
||||
protected async enable() {
|
||||
if (!this.formGroup) {
|
||||
return;
|
||||
}
|
||||
const keys = this.formGroup.controls.formKeys.value;
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorYubikeyOtpRequest);
|
||||
request.key1 = keys != null && keys.length > 0 ? keys[0].key : null;
|
||||
request.key2 = keys != null && keys.length > 1 ? keys[1].key : null;
|
||||
request.key3 = keys != null && keys.length > 2 ? keys[2].key : null;
|
||||
request.key4 = keys != null && keys.length > 3 ? keys[3].key : null;
|
||||
request.key5 = keys != null && keys.length > 4 ? keys[4].key : null;
|
||||
request.nfc = this.formGroup.value.anyKeyHasNfc;
|
||||
request.key1 = keys != null && keys.length > 0 ? (keys[0]?.key ?? "") : "";
|
||||
request.key2 = keys != null && keys.length > 1 ? (keys[1]?.key ?? "") : "";
|
||||
request.key3 = keys != null && keys.length > 2 ? (keys[2]?.key ?? "") : "";
|
||||
request.key4 = keys != null && keys.length > 3 ? (keys[3]?.key ?? "") : "";
|
||||
request.key5 = keys != null && keys.length > 4 ? (keys[4]?.key ?? "") : "";
|
||||
request.nfc = this.formGroup.value.anyKeyHasNfc ?? false;
|
||||
|
||||
this.processResponse(await this.apiService.putTwoFactorYubiKey(request));
|
||||
this.refreshFormArrayData();
|
||||
@@ -137,12 +188,16 @@ export class TwoFactorSetupYubiKeyComponent
|
||||
}
|
||||
|
||||
remove(pos: number) {
|
||||
this.keys[pos].key = null;
|
||||
this.keys[pos].existingKey = null;
|
||||
this.keys[pos].key = "";
|
||||
this.keys[pos].existingKey = "";
|
||||
|
||||
if (!this.keysFormControl || !this.keysFormControl[pos]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.keysFormControl[pos].setValue({
|
||||
existingKey: null,
|
||||
key: null,
|
||||
existingKey: "",
|
||||
key: "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -173,6 +228,9 @@ export class TwoFactorSetupYubiKeyComponent
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AuthResponse<TwoFactorYubiKeyResponse>>,
|
||||
) {
|
||||
return dialogService.open<boolean>(TwoFactorSetupYubiKeyComponent, config);
|
||||
return dialogService.open<boolean, AuthResponse<TwoFactorYubiKeyResponse>>(
|
||||
TwoFactorSetupYubiKeyComponent,
|
||||
config as DialogConfig<AuthResponse<TwoFactorYubiKeyResponse>, DialogRef<boolean>>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<app-header *ngIf="organizationId != null"></app-header>
|
||||
|
||||
<bit-container>
|
||||
<div class="tabbed-header" *ngIf="organizationId == null">
|
||||
<div class="tw-mt-6 tw-mb-2 tw-pb-2.5" *ngIf="organizationId == null">
|
||||
<h1 *ngIf="!organizationId || !isEnterpriseOrg">{{ "twoStepLogin" | i18n }}</h1>
|
||||
<h1 *ngIf="organizationId && isEnterpriseOrg">{{ "twoStepLoginEnforcement" | i18n }}</h1>
|
||||
</div>
|
||||
@@ -35,7 +35,7 @@
|
||||
{{ "providers" | i18n }}
|
||||
<small *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-fw text-muted"
|
||||
class="bwi bwi-spinner bwi-spin bwi-fw tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
@@ -45,32 +45,32 @@
|
||||
<bit-callout type="warning" *ngIf="showPolicyWarning">
|
||||
{{ "twoStepLoginPolicyUserWarning" | i18n }}
|
||||
</bit-callout>
|
||||
<ul class="list-group list-group-2fa">
|
||||
<li *ngFor="let p of providers" class="list-group-item d-flex align-items-center">
|
||||
<div class="logo-2fa d-flex justify-content-center">
|
||||
<auth-two-factor-icon [provider]="p.type" [name]="p.name" />
|
||||
<bit-item-group [attr.aria-label]="'providers' | i18n">
|
||||
<bit-item *ngFor="let p of providers" class="tw-py-4">
|
||||
<div slot="start" class="tw-min-w-[120px] tw-flex tw-justify-center">
|
||||
<auth-two-factor-icon class="tw-flex tw-items-center" [provider]="p.type" [name]="p.name" />
|
||||
</div>
|
||||
<div class="mx-4">
|
||||
<h3 class="mb-0">
|
||||
<div bit-item-content class="tw-px-4">
|
||||
<h3 class="tw-mb-0">
|
||||
<div
|
||||
class="font-weight-semibold tw-text-base"
|
||||
class="tw-font-semibold tw-text-base"
|
||||
[style]="p.enabled || p.premium ? 'display:inline-block' : ''"
|
||||
>
|
||||
{{ p.name }}
|
||||
</div>
|
||||
<ng-container *ngIf="p.enabled">
|
||||
<i
|
||||
class="bwi bwi-check text-success bwi-fw"
|
||||
class="bwi bwi-check tw-text-success-600 bwi-fw tw-ml-2"
|
||||
title="{{ 'enabled' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "enabled" | i18n }}</span>
|
||||
</ng-container>
|
||||
<app-premium-badge *ngIf="p.premium"></app-premium-badge>
|
||||
<app-premium-badge class="tw-ml-2" *ngIf="p.premium"></app-premium-badge>
|
||||
</h3>
|
||||
{{ p.description }}
|
||||
<div class="tw-mt-2 tw-text-wrap">{{ p.description }}</div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<bit-item-action slot="end">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
@@ -80,9 +80,9 @@
|
||||
>
|
||||
{{ "manage" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</bit-item-action>
|
||||
</bit-item>
|
||||
</bit-item-group>
|
||||
</bit-container>
|
||||
|
||||
<ng-template #duoTemplate></ng-template>
|
||||
|
||||
@@ -32,7 +32,10 @@ import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { DialogRef, DialogService } from "@bitwarden/components";
|
||||
import { DialogRef, DialogService, ItemModule } from "@bitwarden/components";
|
||||
|
||||
import { LooseComponentsModule } from "../../../shared/loose-components.module";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
|
||||
import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component";
|
||||
import { TwoFactorSetupAuthenticatorComponent } from "./two-factor-setup-authenticator.component";
|
||||
@@ -45,6 +48,8 @@ import { TwoFactorVerifyComponent } from "./two-factor-verify.component";
|
||||
@Component({
|
||||
selector: "app-two-factor-setup",
|
||||
templateUrl: "two-factor-setup.component.html",
|
||||
standalone: true,
|
||||
imports: [ItemModule, LooseComponentsModule, SharedModule],
|
||||
})
|
||||
export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("yubikeyTemplate", { read: ViewContainerRef, static: true })
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Inject, Output } from "@angular/core";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
@@ -13,7 +12,16 @@ import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-respo
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
type TwoFactorVerifyDialogData = {
|
||||
type: TwoFactorProviderType;
|
||||
@@ -23,13 +31,22 @@ type TwoFactorVerifyDialogData = {
|
||||
@Component({
|
||||
selector: "app-two-factor-verify",
|
||||
templateUrl: "two-factor-verify.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
I18nPipe,
|
||||
ReactiveFormsModule,
|
||||
UserVerificationFormInputComponent,
|
||||
],
|
||||
})
|
||||
export class TwoFactorVerifyComponent {
|
||||
type: TwoFactorProviderType;
|
||||
organizationId: string;
|
||||
@Output() onAuthed = new EventEmitter<AuthResponse<TwoFactorResponse>>();
|
||||
|
||||
formPromise: Promise<TwoFactorResponse>;
|
||||
formPromise: Promise<TwoFactorResponse> | undefined;
|
||||
|
||||
protected formGroup = new FormGroup({
|
||||
secret: new FormControl<Verification | null>(null),
|
||||
@@ -49,22 +66,25 @@ export class TwoFactorVerifyComponent {
|
||||
|
||||
submit = async () => {
|
||||
try {
|
||||
let hashedSecret: string;
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.formGroup.value.secret)
|
||||
.then((request) => {
|
||||
hashedSecret =
|
||||
this.formGroup.value.secret.type === VerificationType.MasterPassword
|
||||
? request.masterPasswordHash
|
||||
: request.otp;
|
||||
return this.apiCall(request);
|
||||
});
|
||||
let hashedSecret = "";
|
||||
if (!this.formGroup.value.secret) {
|
||||
throw new Error("Secret is required");
|
||||
}
|
||||
|
||||
const secret = this.formGroup.value.secret!;
|
||||
this.formPromise = this.userVerificationService.buildRequest(secret).then((request) => {
|
||||
hashedSecret =
|
||||
secret.type === VerificationType.MasterPassword
|
||||
? request.masterPasswordHash
|
||||
: request.otp;
|
||||
return this.apiCall(request);
|
||||
});
|
||||
|
||||
const response = await this.formPromise;
|
||||
this.dialogRef.close({
|
||||
response: response,
|
||||
secret: hashedSecret,
|
||||
verificationType: this.formGroup.value.secret.type,
|
||||
verificationType: secret.type,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorResponse && e.statusCode === 400) {
|
||||
@@ -88,6 +108,8 @@ export class TwoFactorVerifyComponent {
|
||||
return this.i18nService.t("authenticatorAppTitle");
|
||||
case TwoFactorProviderType.Yubikey:
|
||||
return "Yubikey";
|
||||
default:
|
||||
throw new Error(`Unknown two-factor type: ${this.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,10 +132,15 @@ export class TwoFactorVerifyComponent {
|
||||
return this.apiService.getTwoFactorAuthenticator(request);
|
||||
case TwoFactorProviderType.Yubikey:
|
||||
return this.apiService.getTwoFactorYubiKey(request);
|
||||
default:
|
||||
throw new Error(`Unknown two-factor type: ${this.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<TwoFactorVerifyDialogData>) {
|
||||
return dialogService.open<AuthResponse<any>>(TwoFactorVerifyComponent, config);
|
||||
return dialogService.open<AuthResponse<any>, TwoFactorVerifyDialogData>(
|
||||
TwoFactorVerifyComponent,
|
||||
config as DialogConfig<TwoFactorVerifyDialogData, DialogRef<AuthResponse<any>>>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,12 @@ export class BillingNotificationService {
|
||||
message: message,
|
||||
});
|
||||
}
|
||||
|
||||
showError(message: string, title: string = "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title,
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
@@ -61,7 +62,6 @@ import {
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Urls,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -153,6 +154,11 @@ export class ProductSwitcherService {
|
||||
// TODO: This should be migrated to an Observable provided by the provider service and moved to the combineLatest above. See AC-2092.
|
||||
const providers = await this.providerService.getAll();
|
||||
|
||||
const providerPortalName =
|
||||
providers[0]?.providerType === ProviderType.BusinessUnit
|
||||
? "Business Unit Portal"
|
||||
: "Provider Portal";
|
||||
|
||||
const orgsMarketingRoute = this.platformUtilsService.isSelfHost()
|
||||
? {
|
||||
route: "https://bitwarden.com/products/business/",
|
||||
@@ -201,7 +207,7 @@ export class ProductSwitcherService {
|
||||
isActive: this.router.url.includes("/organizations/"),
|
||||
},
|
||||
provider: {
|
||||
name: "Provider Portal",
|
||||
name: providerPortalName,
|
||||
icon: "bwi-provider",
|
||||
appRoute: ["/providers", providers[0]?.id],
|
||||
isActive: this.router.url.includes("/providers/"),
|
||||
|
||||
@@ -33,14 +33,6 @@ import { ApiKeyComponent } from "../auth/settings/security/api-key.component";
|
||||
import { ChangeKdfModule } from "../auth/settings/security/change-kdf/change-kdf.module";
|
||||
import { SecurityKeysComponent } from "../auth/settings/security/security-keys.component";
|
||||
import { SecurityComponent } from "../auth/settings/security/security.component";
|
||||
import { TwoFactorRecoveryComponent } from "../auth/settings/two-factor/two-factor-recovery.component";
|
||||
import { TwoFactorSetupAuthenticatorComponent } from "../auth/settings/two-factor/two-factor-setup-authenticator.component";
|
||||
import { TwoFactorSetupDuoComponent } from "../auth/settings/two-factor/two-factor-setup-duo.component";
|
||||
import { TwoFactorSetupEmailComponent } from "../auth/settings/two-factor/two-factor-setup-email.component";
|
||||
import { TwoFactorSetupWebAuthnComponent } from "../auth/settings/two-factor/two-factor-setup-webauthn.component";
|
||||
import { TwoFactorSetupYubiKeyComponent } from "../auth/settings/two-factor/two-factor-setup-yubikey.component";
|
||||
import { TwoFactorSetupComponent } from "../auth/settings/two-factor/two-factor-setup.component";
|
||||
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor/two-factor-verify.component";
|
||||
import { UserVerificationModule } from "../auth/shared/components/user-verification";
|
||||
import { UpdatePasswordComponent } from "../auth/update-password.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
@@ -145,14 +137,6 @@ import { SharedModule } from "./shared.module";
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
TwoFactorSetupAuthenticatorComponent,
|
||||
TwoFactorSetupDuoComponent,
|
||||
TwoFactorSetupEmailComponent,
|
||||
TwoFactorRecoveryComponent,
|
||||
TwoFactorSetupComponent,
|
||||
TwoFactorVerifyComponent,
|
||||
TwoFactorSetupWebAuthnComponent,
|
||||
TwoFactorSetupYubiKeyComponent,
|
||||
UpdatePasswordComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
@@ -204,13 +188,6 @@ import { SharedModule } from "./shared.module";
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
TwoFactorSetupAuthenticatorComponent,
|
||||
TwoFactorSetupDuoComponent,
|
||||
TwoFactorSetupEmailComponent,
|
||||
TwoFactorSetupComponent,
|
||||
TwoFactorVerifyComponent,
|
||||
TwoFactorSetupWebAuthnComponent,
|
||||
TwoFactorSetupYubiKeyComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
UpdatePasswordComponent,
|
||||
UserLayoutComponent,
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<th bitCell bitSortable="organizationId" *ngIf="!isAdminConsoleActive">
|
||||
{{ "owner" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-text-right" bitSortable="score" default>
|
||||
<th bitCell class="tw-text-right" bitSortable="scoreKey" default>
|
||||
{{ "weakness" | i18n }}
|
||||
</th>
|
||||
</ng-container>
|
||||
|
||||
@@ -18,8 +18,8 @@ import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/se
|
||||
|
||||
import { CipherReportComponent } from "./cipher-report.component";
|
||||
|
||||
type ReportScore = { label: string; badgeVariant: BadgeVariant };
|
||||
type ReportResult = CipherView & { score: number; reportValue: ReportScore };
|
||||
type ReportScore = { label: string; badgeVariant: BadgeVariant; sortOrder: number };
|
||||
type ReportResult = CipherView & { score: number; reportValue: ReportScore; scoreKey: number };
|
||||
|
||||
@Component({
|
||||
selector: "app-weak-passwords-report",
|
||||
@@ -110,7 +110,12 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
||||
|
||||
if (result.score != null && result.score <= 2) {
|
||||
const scoreValue = this.scoreKey(result.score);
|
||||
const row = { ...ciph, score: result.score, reportValue: scoreValue } as ReportResult;
|
||||
const row = {
|
||||
...ciph,
|
||||
score: result.score,
|
||||
reportValue: scoreValue,
|
||||
scoreKey: scoreValue.sortOrder,
|
||||
} as ReportResult;
|
||||
this.weakPasswordCiphers.push(row);
|
||||
}
|
||||
});
|
||||
@@ -129,13 +134,13 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
||||
private scoreKey(score: number): ReportScore {
|
||||
switch (score) {
|
||||
case 4:
|
||||
return { label: "strong", badgeVariant: "success" };
|
||||
return { label: "strong", badgeVariant: "success", sortOrder: 1 };
|
||||
case 3:
|
||||
return { label: "good", badgeVariant: "primary" };
|
||||
return { label: "good", badgeVariant: "primary", sortOrder: 2 };
|
||||
case 2:
|
||||
return { label: "weak", badgeVariant: "warning" };
|
||||
return { label: "weak", badgeVariant: "warning", sortOrder: 3 };
|
||||
default:
|
||||
return { label: "veryWeak", badgeVariant: "danger" };
|
||||
return { label: "veryWeak", badgeVariant: "danger", sortOrder: 4 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
@@ -33,7 +33,7 @@ window.addEventListener("load", async () => {
|
||||
|
||||
displayHandoffMessage(client);
|
||||
} else if (client === "browser") {
|
||||
window.postMessage({ command: "duoResult", code: code, state: state }, "*");
|
||||
window.postMessage({ command: "duoResult", code, state }, window.location.origin);
|
||||
displayHandoffMessage(client);
|
||||
} else if (client === "mobile" || client === "desktop") {
|
||||
if (client === "desktop") {
|
||||
|
||||
@@ -32,7 +32,7 @@ function initiateWebAppSso(code: string, state: string) {
|
||||
}
|
||||
|
||||
function initiateBrowserSso(code: string, state: string, lastpass: boolean) {
|
||||
window.postMessage({ command: "authResult", code: code, state: state, lastpass: lastpass }, "*");
|
||||
window.postMessage({ command: "authResult", code, state, lastpass }, window.location.origin);
|
||||
const handOffMessage = ("; " + document.cookie)
|
||||
.split("; ssoHandOffMessage=")
|
||||
.pop()
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -149,27 +149,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"atRiskMembersWithCount": {
|
||||
"message": "At-risk members ($COUNT$)",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"atRiskMembersDescription": {
|
||||
"message": "These members are logging into applications with weak, exposed, or reused passwords."
|
||||
},
|
||||
"atRiskMembersDescriptionWithApp": {
|
||||
"message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.",
|
||||
"placeholders": {
|
||||
"appname": {
|
||||
"content": "$1",
|
||||
"example": "Salesforce"
|
||||
}
|
||||
}
|
||||
},
|
||||
"totalMembers": {
|
||||
"message": "Total members"
|
||||
},
|
||||
@@ -3345,6 +3324,12 @@
|
||||
"externalIdDesc": {
|
||||
"message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector and API."
|
||||
},
|
||||
"ssoExternalId": {
|
||||
"message": "SSO External ID"
|
||||
},
|
||||
"ssoExternalIdDesc": {
|
||||
"message": "SSO External ID is an unencrypted reference between Bitwarden and your configured SSO provider."
|
||||
},
|
||||
"nestCollectionUnder": {
|
||||
"message": "Nest collection under"
|
||||
},
|
||||
@@ -9308,9 +9293,6 @@
|
||||
"sdksDesc": {
|
||||
"message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications."
|
||||
},
|
||||
"singleSignOn": {
|
||||
"message": "Single sign-on"
|
||||
},
|
||||
"ssoDescStart": {
|
||||
"message": "Configure",
|
||||
"description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider."
|
||||
@@ -10610,5 +10592,14 @@
|
||||
},
|
||||
"cannotCreateCollection": {
|
||||
"message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections."
|
||||
},
|
||||
"businessUnit": {
|
||||
"message": "Business Unit"
|
||||
},
|
||||
"businessUnits": {
|
||||
"message": "Business Units"
|
||||
},
|
||||
"newBusinessUnit": {
|
||||
"message": "New business unit"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +30,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list-group-2fa {
|
||||
.logo-2fa {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@each $mfaType in $mfaTypes {
|
||||
.mfaType#{$mfaType} {
|
||||
content: url("../images/two-factor/" + $mfaType + ".png");
|
||||
@@ -66,14 +60,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.recovery-code-img {
|
||||
@include themify($themes) {
|
||||
content: url("../images/two-factor/rc" + themed("mfaLogoSuffix"));
|
||||
max-width: 100px;
|
||||
max-height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
@include themify($themes) {
|
||||
background-color: themed("pwStrengthBackground");
|
||||
|
||||
@@ -10,9 +10,9 @@ import { OrganizationDomainResponse } from "@bitwarden/common/admin-console/abst
|
||||
import { OrganizationDomainRequest } from "@bitwarden/common/admin-console/services/organization-domain/requests/organization-domain.request";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<app-layout>
|
||||
<app-side-nav variant="secondary" *ngIf="provider$ | async as provider">
|
||||
<bit-nav-logo [openIcon]="logo" route="." [label]="'providerPortal' | i18n"></bit-nav-logo>
|
||||
<bit-nav-logo
|
||||
[openIcon]="logo$ | async"
|
||||
route="."
|
||||
[label]="'providerPortal' | i18n"
|
||||
></bit-nav-logo>
|
||||
|
||||
<bit-nav-item
|
||||
icon="bwi-provider"
|
||||
[text]="'clients' | i18n"
|
||||
[text]="clientsTranslationKey$ | async | i18n"
|
||||
[route]="(isBillable | async) ? 'manage-client-organizations' : 'clients'"
|
||||
></bit-nav-item>
|
||||
<bit-nav-group
|
||||
|
||||
@@ -8,9 +8,10 @@ import { takeUntil } from "rxjs/operators";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { ProviderStatusType, ProviderType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { IconModule } from "@bitwarden/components";
|
||||
import { Icon, IconModule } from "@bitwarden/components";
|
||||
import { BusinessUnitPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/business-unit-portal-logo.icon";
|
||||
import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo";
|
||||
import { WebLayoutModule } from "@bitwarden/web-vault/app/layouts/web-layout.module";
|
||||
|
||||
@@ -26,9 +27,13 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
protected provider$: Observable<Provider>;
|
||||
|
||||
protected logo$: Observable<Icon>;
|
||||
|
||||
protected isBillable: Observable<boolean>;
|
||||
protected canAccessBilling$: Observable<boolean>;
|
||||
|
||||
protected clientsTranslationKey$: Observable<string>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private providerService: ProviderService,
|
||||
@@ -42,16 +47,28 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy {
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
this.logo$ = this.provider$.pipe(
|
||||
map((provider) =>
|
||||
provider.providerType === ProviderType.BusinessUnit
|
||||
? BusinessUnitPortalLogo
|
||||
: ProviderPortalLogo,
|
||||
),
|
||||
);
|
||||
|
||||
this.isBillable = this.provider$.pipe(
|
||||
map((provider) => provider?.providerStatus === ProviderStatusType.Billable),
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
this.canAccessBilling$ = combineLatest([this.isBillable, this.provider$]).pipe(
|
||||
map(
|
||||
([hasConsolidatedBilling, provider]) => hasConsolidatedBilling && provider.isProviderAdmin,
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
this.clientsTranslationKey$ = this.provider$.pipe(
|
||||
map((provider) =>
|
||||
provider.providerType === ProviderType.BusinessUnit ? "businessUnits" : "clients",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
hasConsolidatedBilling,
|
||||
ProviderBillingHistoryComponent,
|
||||
} from "../../billing/providers";
|
||||
import { SetupBusinessUnitComponent } from "../../billing/providers/setup/setup-business-unit.component";
|
||||
|
||||
import { ClientsComponent } from "./clients/clients.component";
|
||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||
@@ -49,6 +50,11 @@ const routes: Routes = [
|
||||
component: SetupProviderComponent,
|
||||
data: { titleId: "setupProvider" },
|
||||
},
|
||||
{
|
||||
path: "setup-business-unit",
|
||||
component: SetupBusinessUnitComponent,
|
||||
data: { titleId: "setupProvider" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ProviderSubscriptionStatusComponent,
|
||||
} from "../../billing/providers";
|
||||
import { AddExistingOrganizationDialogComponent } from "../../billing/providers/clients/add-existing-organization-dialog.component";
|
||||
import { SetupBusinessUnitComponent } from "../../billing/providers/setup/setup-business-unit.component";
|
||||
|
||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||
@@ -75,6 +76,7 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr
|
||||
ProviderSubscriptionStatusComponent,
|
||||
ProvidersComponent,
|
||||
VerifyRecoverDeleteProviderComponent,
|
||||
SetupBusinessUnitComponent,
|
||||
],
|
||||
providers: [WebProviderService],
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<app-header>
|
||||
<app-header [title]="pageTitle">
|
||||
<bit-search [placeholder]="'search' | i18n" [formControl]="searchControl"></bit-search>
|
||||
<ng-container *ngIf="addExistingOrgsFromProviderPortal$ | async; else addExistingOrgsDisabled">
|
||||
<button
|
||||
@@ -14,7 +14,7 @@
|
||||
<bit-menu #clientMenu>
|
||||
<button type="button" bitMenuItem (click)="createClient()">
|
||||
<i aria-hidden="true" class="bwi bwi-business"></i>
|
||||
{{ "newClient" | i18n }}
|
||||
{{ newClientButtonLabel }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addExistingOrganization()">
|
||||
<i aria-hidden="true" class="bwi bwi-filter"></i>
|
||||
@@ -48,7 +48,7 @@
|
||||
<ng-container *ngIf="!loading">
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="53" class="tw-overflow-hidden">
|
||||
<ng-container header>
|
||||
<th colspan="2" bitCell bitSortable="organizationName" default>{{ "client" | i18n }}</th>
|
||||
<th colspan="2" bitCell bitSortable="organizationName" default>{{ clientColumnHeader }}</th>
|
||||
<th bitCell bitSortable="seats">{{ "assigned" | i18n }}</th>
|
||||
<th bitCell bitSortable="occupiedSeats">{{ "used" | i18n }}</th>
|
||||
<th bitCell bitSortable="remainingSeats">{{ "remaining" | i18n }}</th>
|
||||
|
||||
@@ -6,7 +6,11 @@ import { firstValueFrom, from, lastValueFrom, map } from "rxjs";
|
||||
import { debounceTime, first, switchMap } from "rxjs/operators";
|
||||
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import {
|
||||
ProviderStatusType,
|
||||
ProviderType,
|
||||
ProviderUserType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
@@ -73,6 +77,10 @@ export class ManageClientsComponent {
|
||||
FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal,
|
||||
);
|
||||
|
||||
pageTitle = this.i18nService.t("clients");
|
||||
clientColumnHeader = this.i18nService.t("client");
|
||||
newClientButtonLabel = this.i18nService.t("newClient");
|
||||
|
||||
constructor(
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private providerService: ProviderService,
|
||||
@@ -124,6 +132,11 @@ export class ManageClientsComponent {
|
||||
async load() {
|
||||
try {
|
||||
this.provider = await firstValueFrom(this.providerService.get$(this.providerId));
|
||||
if (this.provider?.providerType === ProviderType.BusinessUnit) {
|
||||
this.pageTitle = this.i18nService.t("businessUnits");
|
||||
this.clientColumnHeader = this.i18nService.t("businessUnit");
|
||||
this.newClientButtonLabel = this.i18nService.t("newBusinessUnit");
|
||||
}
|
||||
this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin;
|
||||
this.dataSource.data = (
|
||||
await this.billingApiService.getProviderClientOrganizations(this.providerId)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<div class="tw-mt-12 tw-flex tw-justify-center" *ngIf="loading">
|
||||
<div>
|
||||
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="bitwardenLogo"></bit-icon>
|
||||
<p 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>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-flex tw-flex-row tw-justify-center tw-mt-12" *ngIf="!loading && !authed">
|
||||
<div class="tw-w-[400px] tw-mt-5">
|
||||
<h2 class="tw-flex tw-justify-center tw-mb-4">{{ "setupProvider" | i18n }}</h2>
|
||||
<bit-card>
|
||||
<p>{{ "setupProviderLoginDesc" | i18n }}</p>
|
||||
<hr />
|
||||
<button bitButton type="button" [block]="true" (click)="login()" buttonType="primary">
|
||||
{{ "logIn" | i18n }}
|
||||
</button>
|
||||
</bit-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { filter, map, switchMap } from "rxjs/operators";
|
||||
|
||||
import { BitwardenLogo } from "@bitwarden/auth/angular";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { ProviderKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
|
||||
import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./setup-business-unit.component.html",
|
||||
})
|
||||
export class SetupBusinessUnitComponent extends BaseAcceptComponent {
|
||||
protected bitwardenLogo = BitwardenLogo;
|
||||
|
||||
failedMessage = "emergencyInviteAcceptFailed";
|
||||
failedShortMessage = "emergencyInviteAcceptFailedShort";
|
||||
requiredParameters = ["organizationId", "email", "token"];
|
||||
|
||||
constructor(
|
||||
activatedRoute: ActivatedRoute,
|
||||
authService: AuthService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
private encryptService: EncryptService,
|
||||
i18nService: I18nService,
|
||||
private keyService: KeyService,
|
||||
private organizationBillingApiService: OrganizationBillingApiServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
router: Router,
|
||||
private stateProvider: StateProvider,
|
||||
private syncService: SyncService,
|
||||
) {
|
||||
super(router, platformUtilsService, i18nService, activatedRoute, authService);
|
||||
}
|
||||
|
||||
async authedHandler(queryParams: Params) {
|
||||
await this.process(queryParams);
|
||||
}
|
||||
|
||||
async unauthedHandler(_: Params) {}
|
||||
|
||||
async login() {
|
||||
await this.router.navigate(["/login"], { queryParams: { email: this.email } });
|
||||
}
|
||||
|
||||
process = async (queryParams: Params): Promise<boolean> => {
|
||||
const fail = async () => {
|
||||
this.billingNotificationService.showError(this.i18nService.t(this.failedMessage));
|
||||
return await this.router.navigate(["/"]);
|
||||
};
|
||||
|
||||
const organizationId = queryParams.organizationId as string;
|
||||
const token = queryParams.token as string;
|
||||
|
||||
if (!organizationId || !token) {
|
||||
return await fail();
|
||||
}
|
||||
|
||||
const activeUserId$ = this.stateProvider.activeUserId$.pipe(
|
||||
filter((userId): userId is NonNullable<typeof userId> => userId != null),
|
||||
);
|
||||
|
||||
const organizationKey$ = activeUserId$.pipe(
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
filter(
|
||||
(organizationKeysById): organizationKeysById is NonNullable<typeof organizationKeysById> =>
|
||||
organizationKeysById != null && organizationId in organizationKeysById,
|
||||
),
|
||||
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
|
||||
);
|
||||
|
||||
const [{ encryptedString: encryptedProviderKey }, providerKey] =
|
||||
await this.keyService.makeOrgKey<ProviderKey>();
|
||||
|
||||
const organizationKey = await firstValueFrom(organizationKey$);
|
||||
|
||||
const { encryptedString: encryptedOrganizationKey } = await this.encryptService.encrypt(
|
||||
organizationKey.key,
|
||||
providerKey,
|
||||
);
|
||||
|
||||
if (!encryptedProviderKey || !encryptedOrganizationKey) {
|
||||
return await fail();
|
||||
}
|
||||
|
||||
const userId = await firstValueFrom(activeUserId$);
|
||||
|
||||
const request = {
|
||||
userId,
|
||||
token,
|
||||
providerKey: encryptedProviderKey,
|
||||
organizationKey: encryptedOrganizationKey,
|
||||
};
|
||||
|
||||
try {
|
||||
const providerId = await this.organizationBillingApiService.setupBusinessUnit(
|
||||
organizationId,
|
||||
request,
|
||||
);
|
||||
await this.syncService.fullSync(true);
|
||||
this.billingNotificationService.showSuccess(this.i18nService.t("providerSetup"));
|
||||
return await this.router.navigate(["/providers", providerId]);
|
||||
} catch (error) {
|
||||
this.billingNotificationService.handleError(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -39,8 +39,8 @@ export class ProviderSubscriptionStatusComponent {
|
||||
switch (this.subscription.providerType) {
|
||||
case ProviderType.Msp:
|
||||
return "managedServiceProvider";
|
||||
case ProviderType.MultiOrganizationEnterprise:
|
||||
return "multiOrganizationEnterprise";
|
||||
case ProviderType.BusinessUnit:
|
||||
return "businessUnit";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,18 @@ export class ProviderSubscriptionStatusComponent {
|
||||
},
|
||||
};
|
||||
}
|
||||
case "trialing": {
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: this.i18nService.t("trial"),
|
||||
},
|
||||
date: {
|
||||
label: nextChargeDateLabel,
|
||||
value: this.subscription.currentPeriodEndDate,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "past_due": {
|
||||
const pastDueText = this.i18nService.t("pastDue");
|
||||
const suspensionDate = this.datePipe.transform(
|
||||
|
||||
@@ -95,10 +95,13 @@ export class CriticalApplicationsComponent implements OnInit {
|
||||
}
|
||||
|
||||
goToAllAppsTab = async () => {
|
||||
await this.router.navigate([`organizations/${this.organizationId}/risk-insights`], {
|
||||
queryParams: { tabIndex: RiskInsightsTabType.AllApps },
|
||||
queryParamsHandling: "merge",
|
||||
});
|
||||
await this.router.navigate(
|
||||
[`organizations/${this.organizationId}/access-intelligence/risk-insights`],
|
||||
{
|
||||
queryParams: { tabIndex: RiskInsightsTabType.AllApps },
|
||||
queryParamsHandling: "merge",
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
unmarkAsCriticalApp = async (hostname: string) => {
|
||||
|
||||
@@ -64,10 +64,12 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons
|
||||
|
||||
export class OrganizationUserDetailsResponse extends OrganizationUserResponse {
|
||||
managedByOrganization: boolean;
|
||||
ssoExternalId: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.managedByOrganization = this.getResponseProperty("ManagedByOrganization") ?? false;
|
||||
this.ssoExternalId = this.getResponseProperty("SsoExternalId");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@@ -18,9 +18,9 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@@ -144,9 +144,11 @@ import { OrganizationBillingApiService } from "@bitwarden/common/billing/service
|
||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||
import { TaxService } from "@bitwarden/common/billing/services/tax.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
@@ -166,7 +168,6 @@ import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platf
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
RegionConfig,
|
||||
@@ -219,7 +220,6 @@ import { StateService } from "@bitwarden/common/platform/services/state.service"
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||
import {
|
||||
ActiveUserStateProvider,
|
||||
DerivedStateProvider,
|
||||
@@ -1470,7 +1470,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: LoginSuccessHandlerService,
|
||||
useClass: DefaultLoginSuccessHandlerService,
|
||||
deps: [SyncService, UserAsymmetricKeysRegenerationService],
|
||||
deps: [SyncService, UserAsymmetricKeysRegenerationService, LoginEmailService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TaskService,
|
||||
|
||||
@@ -25,11 +25,11 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth
|
||||
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
||||
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -809,11 +809,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async handleSuccessfulLoginNavigation(userId: UserId) {
|
||||
if (this.flow === Flow.StandardAuthRequest) {
|
||||
// Only need to set remembered email on standard login with auth req flow
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
}
|
||||
|
||||
await this.loginSuccessHandlerService.run(userId);
|
||||
await this.router.navigate(["vault"]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { of } from "rxjs";
|
||||
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Environment,
|
||||
@@ -14,7 +14,7 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
|
||||
|
||||
import { DefaultLoginComponentService } from "./default-login-component.service";
|
||||
|
||||
jest.mock("@bitwarden/common/platform/abstractions/crypto-function.service");
|
||||
jest.mock("@bitwarden/common/key-management/crypto/abstractions/crypto-function.service");
|
||||
jest.mock("@bitwarden/common/platform/abstractions/environment.service");
|
||||
jest.mock("@bitwarden/common/platform/abstractions/platform-utils.service");
|
||||
jest.mock("@bitwarden/common/auth/abstractions/sso-login.service.abstraction");
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { LoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -27,7 +27,12 @@
|
||||
|
||||
<!-- Remember Email input -->
|
||||
<bit-form-control>
|
||||
<input type="checkbox" formControlName="rememberEmail" bitCheckbox />
|
||||
<input
|
||||
type="checkbox"
|
||||
formControlName="rememberEmail"
|
||||
(input)="onRememberEmailInput($event)"
|
||||
bitCheckbox
|
||||
/>
|
||||
<bit-label>{{ "rememberEmail" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
@@ -39,18 +44,18 @@
|
||||
|
||||
<div class="tw-text-center">{{ "or" | i18n }}</div>
|
||||
|
||||
<!-- Link to Login with Passkey page -->
|
||||
<!-- Button to Login with Passkey -->
|
||||
<ng-container *ngIf="isLoginWithPasskeySupported()">
|
||||
<a
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
block
|
||||
linkType="primary"
|
||||
routerLink="/login-with-passkey"
|
||||
(mousedown)="$event.preventDefault()"
|
||||
buttonType="secondary"
|
||||
(click)="handleLoginWithPasskeyClick()"
|
||||
>
|
||||
<i class="bwi bwi-passkey tw-mr-1"></i>
|
||||
{{ "logInWithPasskey" | i18n }}
|
||||
</a>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<!-- Button to Login with SSO -->
|
||||
|
||||
@@ -148,6 +148,62 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private async defaultOnInit(): Promise<void> {
|
||||
let paramEmailIsSet = false;
|
||||
|
||||
const params = await firstValueFrom(this.activatedRoute.queryParams);
|
||||
|
||||
if (params) {
|
||||
const qParamsEmail = params.email;
|
||||
|
||||
// If there is an email in the query params, set that email as the form field value
|
||||
if (qParamsEmail != null && qParamsEmail.indexOf("@") > -1) {
|
||||
this.formGroup.controls.email.setValue(qParamsEmail);
|
||||
paramEmailIsSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no params or no email in the query params, loadEmailSettings from state
|
||||
if (!paramEmailIsSet) {
|
||||
await this.loadRememberedEmail();
|
||||
}
|
||||
|
||||
// Check to see if the device is known so that we can show the Login with Device option
|
||||
if (this.emailFormControl.value) {
|
||||
await this.getKnownDevice(this.emailFormControl.value);
|
||||
}
|
||||
|
||||
// Backup check to handle unknown case where activatedRoute is not available
|
||||
// This shouldn't happen under normal circumstances
|
||||
if (!this.activatedRoute) {
|
||||
await this.loadRememberedEmail();
|
||||
}
|
||||
}
|
||||
|
||||
private async desktopOnInit(): Promise<void> {
|
||||
// TODO: refactor to not use deprecated broadcaster service.
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
case "windowIsFocused":
|
||||
if (this.deferFocus === null) {
|
||||
this.deferFocus = !message.windowIsFocused;
|
||||
if (!this.deferFocus) {
|
||||
this.focusInput();
|
||||
}
|
||||
} else if (this.deferFocus && message.windowIsFocused) {
|
||||
this.focusInput();
|
||||
this.deferFocus = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.messagingService.send("getWindowIsFocused");
|
||||
}
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
if (this.clientType === ClientType.Desktop) {
|
||||
if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) {
|
||||
@@ -172,7 +228,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
try {
|
||||
const authResult = await this.loginStrategyService.logIn(credentials);
|
||||
|
||||
await this.saveEmailSettings();
|
||||
await this.handleAuthResult(authResult);
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
@@ -250,7 +305,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
|
||||
// User logged in successfully so execute side effects
|
||||
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
// Determine where to send the user next
|
||||
if (authResult.forcePasswordReset != ForceSetPasswordReason.None) {
|
||||
@@ -288,7 +342,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
await this.router.navigate(["vault"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the master password meets the enforced policy requirements
|
||||
* and if the user is required to change their password.
|
||||
@@ -344,11 +397,10 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.saveEmailSettings();
|
||||
await this.router.navigate(["/login-with-device"]);
|
||||
}
|
||||
|
||||
protected async validateEmail(): Promise<boolean> {
|
||||
protected async emailIsValid(): Promise<boolean> {
|
||||
this.formGroup.controls.email.markAsTouched();
|
||||
this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true });
|
||||
return this.formGroup.controls.email.valid;
|
||||
@@ -399,37 +451,14 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the email value from the input field.
|
||||
* @param event The event object from the input field.
|
||||
*/
|
||||
onEmailInput(event: Event) {
|
||||
const emailInput = event.target as HTMLInputElement;
|
||||
this.formGroup.controls.email.setValue(emailInput.value);
|
||||
this.loginEmailService.setLoginEmail(emailInput.value);
|
||||
}
|
||||
|
||||
isLoginWithPasskeySupported() {
|
||||
return this.loginComponentService.isLoginWithPasskeySupported();
|
||||
}
|
||||
|
||||
protected async goToHint(): Promise<void> {
|
||||
await this.saveEmailSettings();
|
||||
await this.router.navigateByUrl("/hint");
|
||||
}
|
||||
|
||||
protected async saveEmailSettings(): Promise<void> {
|
||||
const email = this.formGroup.value.email;
|
||||
if (!email) {
|
||||
this.logService.error("Email is required to save email settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loginEmailService.setLoginEmail(email);
|
||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail ?? false);
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue button clicked (or enter key pressed).
|
||||
* Adds the login url to the browser's history so that the back button can be used to go back to the email entry state.
|
||||
@@ -445,13 +474,44 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
* Continue to the master password entry state (only if email is validated)
|
||||
*/
|
||||
protected async continue(): Promise<void> {
|
||||
const isEmailValid = await this.validateEmail();
|
||||
const isEmailValid = await this.emailIsValid();
|
||||
|
||||
if (isEmailValid) {
|
||||
await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Login with Passkey button click.
|
||||
* We need a handler here in order to persist the remember email selection to state before routing.
|
||||
* @param event - The event object.
|
||||
*/
|
||||
async handleLoginWithPasskeyClick() {
|
||||
await this.router.navigate(["/login-with-passkey"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the SSO button click.
|
||||
* @param event - The event object.
|
||||
*/
|
||||
async handleSsoClick() {
|
||||
// Make sure the email is valid
|
||||
const isEmailValid = await this.emailIsValid();
|
||||
if (!isEmailValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the email is not empty, for type safety
|
||||
const email = this.formGroup.value.email;
|
||||
if (!email) {
|
||||
this.logService.error("Email is required for SSO");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the user to SSO, either through routing or through redirecting to the web app
|
||||
await this.loginComponentService.redirectToSsoLogin(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to check if the device is known.
|
||||
* Known means that the user has logged in with this device before.
|
||||
@@ -473,23 +533,17 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private async loadEmailSettings(): Promise<void> {
|
||||
// Try to load the email from memory first
|
||||
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
const rememberEmail = this.loginEmailService.getRememberEmail();
|
||||
|
||||
if (email) {
|
||||
this.formGroup.controls.email.setValue(email);
|
||||
this.formGroup.controls.rememberEmail.setValue(rememberEmail);
|
||||
/**
|
||||
* Check to see if the user has remembered an email on the current device.
|
||||
* If so, set the email in the form field and set rememberEmail to true. If not, set rememberEmail to false.
|
||||
*/
|
||||
private async loadRememberedEmail(): Promise<void> {
|
||||
const storedEmail = await firstValueFrom(this.loginEmailService.rememberedEmail$);
|
||||
if (storedEmail) {
|
||||
this.formGroup.controls.email.setValue(storedEmail);
|
||||
this.formGroup.controls.rememberEmail.setValue(true);
|
||||
} else {
|
||||
// If there is no email in memory, check for a storedEmail on disk
|
||||
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
|
||||
|
||||
if (storedEmail) {
|
||||
this.formGroup.controls.email.setValue(storedEmail);
|
||||
// If there is a storedEmail, rememberEmail defaults to true
|
||||
this.formGroup.controls.rememberEmail.setValue(true);
|
||||
}
|
||||
this.formGroup.controls.rememberEmail.setValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,62 +557,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
?.focus();
|
||||
}
|
||||
|
||||
private async defaultOnInit(): Promise<void> {
|
||||
let paramEmailIsSet = false;
|
||||
|
||||
const params = await firstValueFrom(this.activatedRoute.queryParams);
|
||||
|
||||
if (params) {
|
||||
const qParamsEmail = params.email;
|
||||
|
||||
// If there is an email in the query params, set that email as the form field value
|
||||
if (qParamsEmail != null && qParamsEmail.indexOf("@") > -1) {
|
||||
this.formGroup.controls.email.setValue(qParamsEmail);
|
||||
paramEmailIsSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no params or no email in the query params, loadEmailSettings from state
|
||||
if (!paramEmailIsSet) {
|
||||
await this.loadEmailSettings();
|
||||
}
|
||||
|
||||
// Check to see if the device is known so that we can show the Login with Device option
|
||||
if (this.emailFormControl.value) {
|
||||
await this.getKnownDevice(this.emailFormControl.value);
|
||||
}
|
||||
|
||||
// Backup check to handle unknown case where activatedRoute is not available
|
||||
// This shouldn't happen under normal circumstances
|
||||
if (!this.activatedRoute) {
|
||||
await this.loadEmailSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private async desktopOnInit(): Promise<void> {
|
||||
// TODO: refactor to not use deprecated broadcaster service.
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
case "windowIsFocused":
|
||||
if (this.deferFocus === null) {
|
||||
this.deferFocus = !message.windowIsFocused;
|
||||
if (!this.deferFocus) {
|
||||
this.focusInput();
|
||||
}
|
||||
} else if (this.deferFocus && message.windowIsFocused) {
|
||||
this.focusInput();
|
||||
this.deferFocus = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.messagingService.send("getWindowIsFocused");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine if the back button should be shown.
|
||||
* @returns true if the back button should be shown.
|
||||
@@ -597,27 +595,46 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the SSO button click.
|
||||
* Persist the entered email address and the user's choice to remember it to state.
|
||||
*/
|
||||
async handleSsoClick() {
|
||||
const email = this.formGroup.value.email;
|
||||
|
||||
// Make sure the email is valid
|
||||
const isEmailValid = await this.validateEmail();
|
||||
if (!isEmailValid) {
|
||||
return;
|
||||
private async persistEmailIfValid(): Promise<void> {
|
||||
if (await this.emailIsValid()) {
|
||||
const email = this.formGroup.value.email;
|
||||
const rememberEmail = this.formGroup.value.rememberEmail ?? false;
|
||||
if (!email) {
|
||||
return;
|
||||
}
|
||||
await this.loginEmailService.setLoginEmail(email);
|
||||
await this.loginEmailService.setRememberedEmailChoice(email, rememberEmail);
|
||||
} else {
|
||||
await this.loginEmailService.clearLoginEmail();
|
||||
await this.loginEmailService.clearRememberedEmail();
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the email is not empty, for type safety
|
||||
if (!email) {
|
||||
this.logService.error("Email is required for SSO");
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Set the email value from the input field.
|
||||
* We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until
|
||||
* the user submits. This is because currently our validation errors are shown below the input fields, and
|
||||
* displaying them causes the screen to "jump".
|
||||
* @param event The event object from the input field.
|
||||
*/
|
||||
async onEmailInput(event: Event) {
|
||||
const emailInput = event.target as HTMLInputElement;
|
||||
this.formGroup.controls.email.setValue(emailInput.value);
|
||||
await this.persistEmailIfValid();
|
||||
}
|
||||
|
||||
// Save the email configuration for the login component
|
||||
await this.saveEmailSettings();
|
||||
|
||||
// Send the user to SSO, either through routing or through redirecting to the web app
|
||||
await this.loginComponentService.redirectToSsoLogin(email);
|
||||
/**
|
||||
* Set the Remember Email value from the input field.
|
||||
* We only update the form controls onSubmit instead of onBlur because we don't want to show validation errors until
|
||||
* the user submits. This is because currently our validation errors are shown below the input fields, and
|
||||
* displaying them causes the screen to "jump".
|
||||
* @param event The event object from the input field.
|
||||
*/
|
||||
async onRememberEmailInput(event: Event) {
|
||||
const rememberEmailInput = event.target as HTMLInputElement;
|
||||
this.formGroup.controls.rememberEmail.setValue(rememberEmailInput.checked);
|
||||
await this.persistEmailIfValid();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { LoginSuccessHandlerService } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { LoginEmailServiceAbstraction } from "../../common/abstractions/login-email.service";
|
||||
import { LoginStrategyServiceAbstraction } from "../../common/abstractions/login-strategy.service";
|
||||
|
||||
/**
|
||||
@@ -60,8 +59,7 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private syncService: SyncService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -143,9 +141,7 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
this.loginSuccessHandlerService.run(authResult.userId);
|
||||
|
||||
// If verification succeeds, navigate to vault
|
||||
await this.router.navigate(["/vault"]);
|
||||
|
||||
@@ -25,11 +25,11 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@@ -264,19 +264,6 @@ describe("TwoFactorAuthComponent", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("calls loginEmailService.clearValues() when login is successful", async () => {
|
||||
// Arrange
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
// spy on loginEmailService.clearValues
|
||||
const clearValuesSpy = jest.spyOn(mockLoginEmailService, "clearValues");
|
||||
|
||||
// Act
|
||||
await component.submit(token, remember);
|
||||
|
||||
// Assert
|
||||
expect(clearValuesSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("Set Master Password scenarios", () => {
|
||||
beforeEach(() => {
|
||||
const authResult = new AuthResult();
|
||||
|
||||
@@ -475,7 +475,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
|
||||
// User is fully logged in so handle any post login logic before executing navigation
|
||||
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||
this.loginEmailService.clearValues();
|
||||
|
||||
// Save off the OrgSsoIdentifier for use in the TDE flows
|
||||
// - TDE login decryption options component
|
||||
|
||||
@@ -1,43 +1,34 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export abstract class LoginEmailServiceAbstraction {
|
||||
/**
|
||||
* An observable that monitors the loginEmail in memory.
|
||||
* An observable that monitors the loginEmail.
|
||||
* The loginEmail is the email that is being used in the current login process.
|
||||
*/
|
||||
loginEmail$: Observable<string | null>;
|
||||
abstract loginEmail$: Observable<string | null>;
|
||||
/**
|
||||
* An observable that monitors the storedEmail on disk.
|
||||
* An observable that monitors the remembered email.
|
||||
* This will return null if an account is being added.
|
||||
*/
|
||||
storedEmail$: Observable<string | null>;
|
||||
abstract rememberedEmail$: Observable<string | null>;
|
||||
/**
|
||||
* Sets the loginEmail in memory.
|
||||
* The loginEmail is the email that is being used in the current login process.
|
||||
* Consumed through `loginEmail$` observable.
|
||||
*/
|
||||
setLoginEmail: (email: string) => Promise<void>;
|
||||
abstract setLoginEmail: (email: string) => Promise<void>;
|
||||
/**
|
||||
* Gets from memory whether or not the email should be stored on disk when `saveEmailSettings` is called.
|
||||
* @returns A boolean stating whether or not the email should be stored on disk.
|
||||
* Persist the user's choice of whether to remember their email on subsequent login attempts.
|
||||
* Consumed through `rememberedEmail$` observable.
|
||||
*/
|
||||
getRememberEmail: () => boolean;
|
||||
abstract setRememberedEmailChoice: (email: string, remember: boolean) => Promise<void>;
|
||||
/**
|
||||
* Sets in memory whether or not the email should be stored on disk when `saveEmailSettings` is called.
|
||||
* Clears the in-progress login email, to be used after a successful login.
|
||||
*/
|
||||
setRememberEmail: (value: boolean) => void;
|
||||
abstract clearLoginEmail: () => Promise<void>;
|
||||
|
||||
/**
|
||||
* Sets the email and rememberEmail properties in memory to null.
|
||||
* Clears the remembered email.
|
||||
*/
|
||||
clearValues: () => void;
|
||||
/**
|
||||
* Saves or clears the email on disk
|
||||
* - If an account is being added, only changes the stored email when rememberEmail is true.
|
||||
* - If rememberEmail is true, sets the email on disk to the current email.
|
||||
* - If rememberEmail is false, sets the email on disk to null.
|
||||
* Always clears the email and rememberEmail properties from memory.
|
||||
* @returns A promise that resolves once the email settings are saved.
|
||||
*/
|
||||
saveEmailSettings: () => Promise<void>;
|
||||
abstract clearRememberedEmail: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { LoginEmailService, STORED_EMAIL } from "./login-email.service";
|
||||
|
||||
describe("LoginEmailService", () => {
|
||||
let sut: LoginEmailService;
|
||||
let service: LoginEmailService;
|
||||
|
||||
let accountService: FakeAccountService;
|
||||
let authService: MockProxy<AuthService>;
|
||||
@@ -34,119 +34,93 @@ describe("LoginEmailService", () => {
|
||||
mockAuthStatuses$ = new BehaviorSubject<Record<UserId, AuthenticationStatus>>({});
|
||||
authService.authStatuses$ = mockAuthStatuses$;
|
||||
|
||||
sut = new LoginEmailService(accountService, authService, stateProvider);
|
||||
service = new LoginEmailService(accountService, authService, stateProvider);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("storedEmail$", () => {
|
||||
it("returns the stored email when not adding an account", async () => {
|
||||
await sut.setLoginEmail("userEmail@bitwarden.com");
|
||||
sut.setRememberEmail(true);
|
||||
await sut.saveEmailSettings();
|
||||
describe("rememberedEmail$", () => {
|
||||
it("returns the remembered email when not adding an account", async () => {
|
||||
const testEmail = "test@bitwarden.com";
|
||||
|
||||
const result = await firstValueFrom(sut.storedEmail$);
|
||||
await service.setRememberedEmailChoice(testEmail, true);
|
||||
|
||||
expect(result).toEqual("userEmail@bitwarden.com");
|
||||
const result = await firstValueFrom(service.rememberedEmail$);
|
||||
|
||||
expect(result).toEqual(testEmail);
|
||||
});
|
||||
|
||||
it("returns the stored email when not adding an account and the user has just logged in", async () => {
|
||||
await sut.setLoginEmail("userEmail@bitwarden.com");
|
||||
sut.setRememberEmail(true);
|
||||
await sut.saveEmailSettings();
|
||||
it("returns the remembered email when not adding an account and the user has just logged in", async () => {
|
||||
const testEmail = "test@bitwarden.com";
|
||||
|
||||
await service.setRememberedEmailChoice(testEmail, true);
|
||||
|
||||
mockAuthStatuses$.next({ [userId]: AuthenticationStatus.Unlocked });
|
||||
// account service already initialized with userId as active user
|
||||
|
||||
const result = await firstValueFrom(sut.storedEmail$);
|
||||
const result = await firstValueFrom(service.rememberedEmail$);
|
||||
|
||||
expect(result).toEqual("userEmail@bitwarden.com");
|
||||
expect(result).toEqual(testEmail);
|
||||
});
|
||||
|
||||
it("returns null when adding an account", async () => {
|
||||
await sut.setLoginEmail("userEmail@bitwarden.com");
|
||||
sut.setRememberEmail(true);
|
||||
await sut.saveEmailSettings();
|
||||
const testEmail = "test@bitwarden.com";
|
||||
|
||||
await service.setRememberedEmailChoice(testEmail, true);
|
||||
|
||||
mockAuthStatuses$.next({
|
||||
[userId]: AuthenticationStatus.Unlocked,
|
||||
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(sut.storedEmail$);
|
||||
const result = await firstValueFrom(service.rememberedEmail$);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveEmailSettings", () => {
|
||||
it("saves the email when not adding an account", async () => {
|
||||
await sut.setLoginEmail("userEmail@bitwarden.com");
|
||||
sut.setRememberEmail(true);
|
||||
await sut.saveEmailSettings();
|
||||
describe("setRememberedEmailChoice", () => {
|
||||
it("sets the remembered email when remember is true", async () => {
|
||||
const testEmail = "test@bitwarden.com";
|
||||
|
||||
await service.setRememberedEmailChoice(testEmail, true);
|
||||
|
||||
const result = await firstValueFrom(storedEmailState.state$);
|
||||
|
||||
expect(result).toEqual("userEmail@bitwarden.com");
|
||||
expect(result).toEqual(testEmail);
|
||||
});
|
||||
|
||||
it("clears the email when not adding an account and rememberEmail is false", async () => {
|
||||
it("clears the remembered email when remember is false", async () => {
|
||||
storedEmailState.stateSubject.next("initialEmail@bitwarden.com");
|
||||
|
||||
await sut.setLoginEmail("userEmail@bitwarden.com");
|
||||
sut.setRememberEmail(false);
|
||||
await sut.saveEmailSettings();
|
||||
const testEmail = "test@bitwarden.com";
|
||||
|
||||
await service.setRememberedEmailChoice(testEmail, false);
|
||||
|
||||
const result = await firstValueFrom(storedEmailState.state$);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("saves the email when adding an account", async () => {
|
||||
mockAuthStatuses$.next({
|
||||
[userId]: AuthenticationStatus.Unlocked,
|
||||
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
|
||||
});
|
||||
describe("setLoginEmail", () => {
|
||||
it("sets the login email", async () => {
|
||||
const testEmail = "test@bitwarden.com";
|
||||
await service.setLoginEmail(testEmail);
|
||||
|
||||
await sut.setLoginEmail("userEmail@bitwarden.com");
|
||||
sut.setRememberEmail(true);
|
||||
await sut.saveEmailSettings();
|
||||
|
||||
const result = await firstValueFrom(storedEmailState.state$);
|
||||
|
||||
expect(result).toEqual("userEmail@bitwarden.com");
|
||||
expect(await firstValueFrom(service.loginEmail$)).toEqual(testEmail);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not clear the email when adding an account and rememberEmail is false", async () => {
|
||||
storedEmailState.stateSubject.next("initialEmail@bitwarden.com");
|
||||
describe("clearLoginEmail", () => {
|
||||
it("clears the login email", async () => {
|
||||
const testEmail = "test@bitwarden.com";
|
||||
await service.setLoginEmail(testEmail);
|
||||
await service.clearLoginEmail();
|
||||
|
||||
mockAuthStatuses$.next({
|
||||
[userId]: AuthenticationStatus.Unlocked,
|
||||
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
|
||||
});
|
||||
|
||||
await sut.setLoginEmail("userEmail@bitwarden.com");
|
||||
sut.setRememberEmail(false);
|
||||
await sut.saveEmailSettings();
|
||||
|
||||
const result = await firstValueFrom(storedEmailState.state$);
|
||||
|
||||
// result should not be null
|
||||
expect(result).toEqual("initialEmail@bitwarden.com");
|
||||
});
|
||||
|
||||
it("does not clear the email and rememberEmail after saving", async () => {
|
||||
// Browser uses these values to maintain the email between login and 2fa components so
|
||||
// we do not want to clear them too early.
|
||||
await sut.setLoginEmail("userEmail@bitwarden.com");
|
||||
sut.setRememberEmail(true);
|
||||
await sut.saveEmailSettings();
|
||||
|
||||
const result = await firstValueFrom(sut.loginEmail$);
|
||||
|
||||
expect(result).toBe("userEmail@bitwarden.com");
|
||||
expect(await firstValueFrom(service.loginEmail$)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable, firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -26,8 +24,6 @@ export const STORED_EMAIL = new KeyDefinition<string>(LOGIN_EMAIL_DISK, "storedE
|
||||
});
|
||||
|
||||
export class LoginEmailService implements LoginEmailServiceAbstraction {
|
||||
private rememberEmail: boolean;
|
||||
|
||||
// True if an account is currently being added through account switching
|
||||
private readonly addingAccount$: Observable<boolean>;
|
||||
|
||||
@@ -35,7 +31,7 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
|
||||
loginEmail$: Observable<string | null>;
|
||||
|
||||
private readonly storedEmailState: GlobalState<string>;
|
||||
storedEmail$: Observable<string | null>;
|
||||
rememberedEmail$: Observable<string | null>;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
@@ -60,7 +56,7 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
|
||||
|
||||
this.loginEmail$ = this.loginEmailState.state$;
|
||||
|
||||
this.storedEmail$ = this.storedEmailState.state$.pipe(
|
||||
this.rememberedEmail$ = this.storedEmailState.state$.pipe(
|
||||
switchMap(async (storedEmail) => {
|
||||
// When adding an account, we don't show the stored email
|
||||
if (await firstValueFrom(this.addingAccount$)) {
|
||||
@@ -71,44 +67,32 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
/** Sets the login email in memory.
|
||||
* The login email is the email that is being used in the current login process.
|
||||
*/
|
||||
async setLoginEmail(email: string) {
|
||||
await this.loginEmailState.update((_) => email);
|
||||
}
|
||||
|
||||
getRememberEmail() {
|
||||
return this.rememberEmail;
|
||||
/**
|
||||
* Clears the in-progress login email from state.
|
||||
* Note: Only clear on successful login or you are sure they are not needed.
|
||||
* The extension client uses these values to maintain the email between login and 2fa components so
|
||||
* we do not want to clear them too early.
|
||||
*/
|
||||
async clearLoginEmail() {
|
||||
await this.loginEmailState.update((_) => null);
|
||||
}
|
||||
|
||||
setRememberEmail(value: boolean) {
|
||||
this.rememberEmail = value ?? false;
|
||||
async setRememberedEmailChoice(email: string, remember: boolean): Promise<void> {
|
||||
if (remember) {
|
||||
await this.storedEmailState.update((_) => email);
|
||||
} else {
|
||||
await this.storedEmailState.update((_) => null);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: only clear values on successful login or you are sure they are not needed.
|
||||
// Browser uses these values to maintain the email between login and 2fa components so
|
||||
// we do not want to clear them too early.
|
||||
async clearValues() {
|
||||
await this.setLoginEmail(null);
|
||||
this.rememberEmail = false;
|
||||
}
|
||||
|
||||
async saveEmailSettings() {
|
||||
const addingAccount = await firstValueFrom(this.addingAccount$);
|
||||
const email = await firstValueFrom(this.loginEmail$);
|
||||
|
||||
await this.storedEmailState.update((storedEmail) => {
|
||||
// If we're adding an account, only overwrite the stored email when rememberEmail is true
|
||||
if (addingAccount) {
|
||||
if (this.rememberEmail) {
|
||||
return email;
|
||||
}
|
||||
return storedEmail;
|
||||
}
|
||||
|
||||
// Saving with rememberEmail set to false will clear the stored email
|
||||
if (this.rememberEmail) {
|
||||
return email;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
async clearRememberedEmail(): Promise<void> {
|
||||
await this.storedEmailState.update((_) => null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,17 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserAsymmetricKeysRegenerationService } from "@bitwarden/key-management";
|
||||
|
||||
import { LoginSuccessHandlerService } from "../../abstractions/login-success-handler.service";
|
||||
import { LoginEmailService } from "../login-email/login-email.service";
|
||||
|
||||
export class DefaultLoginSuccessHandlerService implements LoginSuccessHandlerService {
|
||||
constructor(
|
||||
private syncService: SyncService,
|
||||
private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService,
|
||||
private loginEmailService: LoginEmailService,
|
||||
) {}
|
||||
async run(userId: UserId): Promise<void> {
|
||||
await this.syncService.fullSync(true);
|
||||
await this.userAsymmetricKeysRegenerationService.regenerateIfNeeded(userId);
|
||||
await this.loginEmailService.clearLoginEmail();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user