Merge remote-tracking branch 'origin/main' into run-migrations-less-often
This commit is contained in:
commit
9a83e7e856
|
@ -7,5 +7,6 @@ checkmarx:
|
|||
scan:
|
||||
configs:
|
||||
sast:
|
||||
presetName: "BW ASA Premium"
|
||||
# Exclude spec files, and test specific files
|
||||
filter: "!*.spec.ts,!**/spec/**,!apps/desktop/native-messaging-test-runner/**"
|
||||
|
|
|
@ -61,6 +61,7 @@ apps/web/src/app/billing @bitwarden/team-billing-dev
|
|||
libs/angular/src/billing @bitwarden/team-billing-dev
|
||||
libs/common/src/billing @bitwarden/team-billing-dev
|
||||
libs/billing @bitwarden/team-billing-dev
|
||||
bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev
|
||||
|
||||
## Platform team files ##
|
||||
apps/browser/src/platform @bitwarden/team-platform-dev
|
||||
|
|
|
@ -40,7 +40,10 @@ jobs:
|
|||
base_uri: https://ast.checkmarx.net/
|
||||
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
|
||||
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
|
||||
additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }}
|
||||
additional_params: |
|
||||
--report-format sarif \
|
||||
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
} from "../../../platform/background/service-factories/state-service.factory";
|
||||
|
||||
import { AccountServiceInitOptions, accountServiceFactory } from "./account-service.factory";
|
||||
import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory";
|
||||
|
||||
type AuthServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
|
@ -32,7 +33,8 @@ export type AuthServiceInitOptions = AuthServiceFactoryOptions &
|
|||
MessagingServiceInitOptions &
|
||||
CryptoServiceInitOptions &
|
||||
ApiServiceInitOptions &
|
||||
StateServiceInitOptions;
|
||||
StateServiceInitOptions &
|
||||
TokenServiceInitOptions;
|
||||
|
||||
export function authServiceFactory(
|
||||
cache: { authService?: AbstractAuthService } & CachedServices,
|
||||
|
@ -49,6 +51,7 @@ export function authServiceFactory(
|
|||
await cryptoServiceFactory(cache, opts),
|
||||
await apiServiceFactory(cache, opts),
|
||||
await stateServiceFactory(cache, opts),
|
||||
await tokenServiceFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,9 +39,13 @@ import {
|
|||
platformUtilsServiceFactory,
|
||||
} from "../../../platform/background/service-factories/platform-utils-service.factory";
|
||||
import {
|
||||
StateServiceInitOptions,
|
||||
stateServiceFactory,
|
||||
} from "../../../platform/background/service-factories/state-service.factory";
|
||||
StateProviderInitOptions,
|
||||
stateProviderFactory,
|
||||
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||
import {
|
||||
SecureStorageServiceInitOptions,
|
||||
secureStorageServiceFactory,
|
||||
} from "../../../platform/background/service-factories/storage-service.factory";
|
||||
|
||||
import {
|
||||
UserDecryptionOptionsServiceInitOptions,
|
||||
|
@ -55,11 +59,12 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor
|
|||
CryptoFunctionServiceInitOptions &
|
||||
CryptoServiceInitOptions &
|
||||
EncryptServiceInitOptions &
|
||||
StateServiceInitOptions &
|
||||
AppIdServiceInitOptions &
|
||||
DevicesApiServiceInitOptions &
|
||||
I18nServiceInitOptions &
|
||||
PlatformUtilsServiceInitOptions &
|
||||
StateProviderInitOptions &
|
||||
SecureStorageServiceInitOptions &
|
||||
UserDecryptionOptionsServiceInitOptions;
|
||||
|
||||
export function deviceTrustCryptoServiceFactory(
|
||||
|
@ -76,11 +81,12 @@ export function deviceTrustCryptoServiceFactory(
|
|||
await cryptoFunctionServiceFactory(cache, opts),
|
||||
await cryptoServiceFactory(cache, opts),
|
||||
await encryptServiceFactory(cache, opts),
|
||||
await stateServiceFactory(cache, opts),
|
||||
await appIdServiceFactory(cache, opts),
|
||||
await devicesApiServiceFactory(cache, opts),
|
||||
await i18nServiceFactory(cache, opts),
|
||||
await platformUtilsServiceFactory(cache, opts),
|
||||
await stateProviderFactory(cache, opts),
|
||||
await secureStorageServiceFactory(cache, opts),
|
||||
await userDecryptionOptionsServiceFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
|||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
|
@ -62,6 +63,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
pinCryptoService: PinCryptoServiceAbstraction,
|
||||
private routerService: BrowserRouterService,
|
||||
biometricStateService: BiometricStateService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
|
@ -84,6 +86,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
userVerificationService,
|
||||
pinCryptoService,
|
||||
biometricStateService,
|
||||
accountService,
|
||||
);
|
||||
this.successRoute = "/tabs/current";
|
||||
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
LoginEmailServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
|
@ -49,6 +50,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
|
|||
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
authRequestService: AuthRequestServiceAbstraction,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
private location: Location,
|
||||
) {
|
||||
super(
|
||||
|
@ -70,6 +72,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
|
|||
deviceTrustCryptoService,
|
||||
authRequestService,
|
||||
loginStrategyService,
|
||||
accountService,
|
||||
);
|
||||
super.onSuccessfulLogin = async () => {
|
||||
await syncService.fullSync(true);
|
||||
|
|
|
@ -604,9 +604,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
|||
* @param sender - The sender of the port message
|
||||
*/
|
||||
private getNewVaultItemDetails({ sender }: chrome.runtime.Port) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" });
|
||||
void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -643,8 +641,8 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
|||
collectionIds: cipherView.collectionIds,
|
||||
});
|
||||
|
||||
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
|
||||
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
|
||||
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,10 +16,6 @@ import {
|
|||
logServiceFactory,
|
||||
LogServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/log-service.factory";
|
||||
import {
|
||||
stateServiceFactory,
|
||||
StateServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/state-service.factory";
|
||||
import {
|
||||
cipherServiceFactory,
|
||||
CipherServiceInitOptions,
|
||||
|
@ -44,7 +40,6 @@ type AutoFillServiceOptions = FactoryOptions;
|
|||
|
||||
export type AutoFillServiceInitOptions = AutoFillServiceOptions &
|
||||
CipherServiceInitOptions &
|
||||
StateServiceInitOptions &
|
||||
AutofillSettingsServiceInitOptions &
|
||||
TotpServiceInitOptions &
|
||||
EventCollectionServiceInitOptions &
|
||||
|
@ -63,7 +58,6 @@ export function autofillServiceFactory(
|
|||
async () =>
|
||||
new AutofillService(
|
||||
await cipherServiceFactory(cache, opts),
|
||||
await stateServiceFactory(cache, opts),
|
||||
await autofillSettingsServiceFactory(cache, opts),
|
||||
await totpServiceFactory(cache, opts),
|
||||
await eventCollectionServiceFactory(cache, opts),
|
||||
|
|
|
@ -99,9 +99,7 @@ class AutofillInit implements AutofillInitInterface {
|
|||
return pageDetails;
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
chrome.runtime.sendMessage({
|
||||
void chrome.runtime.sendMessage({
|
||||
command: "collectPageDetailsResponse",
|
||||
tab: message.tab,
|
||||
details: pageDetails,
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
exports[`AutofillOverlayList initAutofillOverlayList the list of ciphers for an authenticated user creates the view for a list of ciphers 1`] = `
|
||||
<div
|
||||
aria-modal="true"
|
||||
class="overlay-list-container theme_light"
|
||||
role="dialog"
|
||||
>
|
||||
<ul
|
||||
class="overlay-actions-list"
|
||||
|
@ -436,9 +434,7 @@ exports[`AutofillOverlayList initAutofillOverlayList the list of ciphers for an
|
|||
|
||||
exports[`AutofillOverlayList initAutofillOverlayList the locked overlay for an unauthenticated user creates the views for the locked overlay 1`] = `
|
||||
<div
|
||||
aria-modal="true"
|
||||
class="overlay-list-container theme_light"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="locked-overlay overlay-list-message"
|
||||
|
@ -490,9 +486,7 @@ exports[`AutofillOverlayList initAutofillOverlayList the locked overlay for an u
|
|||
|
||||
exports[`AutofillOverlayList initAutofillOverlayList the overlay with an empty list of ciphers creates the views for the no results overlay 1`] = `
|
||||
<div
|
||||
aria-modal="true"
|
||||
class="overlay-list-container theme_light"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="no-items overlay-list-message"
|
||||
|
|
|
@ -312,6 +312,24 @@ describe("AutofillOverlayList", () => {
|
|||
});
|
||||
|
||||
describe("directing user focus into the overlay list", () => {
|
||||
it("sets ARIA attributes that define the list as a `dialog` to screen reader users", () => {
|
||||
postWindowMessage(
|
||||
createInitAutofillOverlayListMessageMock({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
cipherList: [],
|
||||
}),
|
||||
);
|
||||
const overlayContainerSetAttributeSpy = jest.spyOn(
|
||||
autofillOverlayList["overlayListContainer"],
|
||||
"setAttribute",
|
||||
);
|
||||
|
||||
postWindowMessage({ command: "focusOverlayList" });
|
||||
|
||||
expect(overlayContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog");
|
||||
expect(overlayContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true");
|
||||
});
|
||||
|
||||
it("focuses the unlock button element if the user is not authenticated", () => {
|
||||
postWindowMessage(
|
||||
createInitAutofillOverlayListMessageMock({
|
||||
|
|
|
@ -59,8 +59,6 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
|||
|
||||
this.overlayListContainer = globalThis.document.createElement("div");
|
||||
this.overlayListContainer.classList.add("overlay-list-container", themeClass);
|
||||
this.overlayListContainer.setAttribute("role", "dialog");
|
||||
this.overlayListContainer.setAttribute("aria-modal", "true");
|
||||
this.resizeObserver.observe(this.overlayListContainer);
|
||||
|
||||
this.shadowDom.append(linkElement, this.overlayListContainer);
|
||||
|
@ -487,6 +485,9 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
|
|||
* the first cipher button.
|
||||
*/
|
||||
private focusOverlayList() {
|
||||
this.overlayListContainer.setAttribute("role", "dialog");
|
||||
this.overlayListContainer.setAttribute("aria-modal", "true");
|
||||
|
||||
const unlockButtonElement = this.overlayListContainer.querySelector(
|
||||
"#unlock-button",
|
||||
) as HTMLElement;
|
||||
|
|
|
@ -32,7 +32,6 @@ import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
|||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { BrowserStateService } from "../../platform/services/browser-state.service";
|
||||
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
|
@ -63,7 +62,6 @@ const mockEquivalentDomains = [
|
|||
describe("AutofillService", () => {
|
||||
let autofillService: AutofillService;
|
||||
const cipherService = mock<CipherService>();
|
||||
const stateService = mock<BrowserStateService>();
|
||||
const autofillSettingsService = mock<AutofillSettingsService>();
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
@ -78,7 +76,6 @@ describe("AutofillService", () => {
|
|||
beforeEach(() => {
|
||||
autofillService = new AutofillService(
|
||||
cipherService,
|
||||
stateService,
|
||||
autofillSettingsService,
|
||||
totpService,
|
||||
eventCollectionService,
|
||||
|
|
|
@ -20,7 +20,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
||||
import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window";
|
||||
import { AutofillPort } from "../enums/autofill-port.enums";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
|
@ -49,7 +48,6 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private stateService: BrowserStateService,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
private totpService: TotpService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
|
|
|
@ -807,6 +807,36 @@ describe("CollectAutofillContentService", () => {
|
|||
});
|
||||
|
||||
describe("buildAutofillFieldItem", () => {
|
||||
it("returns a `null` value if the field is a child of a `button[type='submit']`", async () => {
|
||||
const usernameField = {
|
||||
labelText: "Username",
|
||||
id: "username-id",
|
||||
type: "text",
|
||||
};
|
||||
document.body.innerHTML = `
|
||||
<form>
|
||||
<div>
|
||||
<div>
|
||||
<label for="${usernameField.id}">${usernameField.labelText}</label>
|
||||
<button type="submit">
|
||||
<input id="${usernameField.id}" type="${usernameField.type}" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
const usernameInput = document.getElementById(
|
||||
usernameField.id,
|
||||
) as ElementWithOpId<FillableFormFieldElement>;
|
||||
|
||||
const autofillFieldItem = await collectAutofillContentService["buildAutofillFieldItem"](
|
||||
usernameInput,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(autofillFieldItem).toBeNull();
|
||||
});
|
||||
|
||||
it("returns an existing autofill field item if it exists", async () => {
|
||||
const index = 0;
|
||||
const usernameField = {
|
||||
|
@ -847,27 +877,6 @@ describe("CollectAutofillContentService", () => {
|
|||
/>
|
||||
</form>
|
||||
`;
|
||||
document.body.innerHTML = `
|
||||
<form>
|
||||
<label for="${usernameField.id}">${usernameField.labelText}</label>
|
||||
<input
|
||||
id="${usernameField.id}"
|
||||
class="${usernameField.classes}"
|
||||
name="${usernameField.name}"
|
||||
type="${usernameField.type}"
|
||||
maxlength="${usernameField.maxLength}"
|
||||
tabindex="${usernameField.tabIndex}"
|
||||
title="${usernameField.title}"
|
||||
autocomplete="${usernameField.autocomplete}"
|
||||
data-label="${usernameField.dataLabel}"
|
||||
aria-label="${usernameField.ariaLabel}"
|
||||
placeholder="${usernameField.placeholder}"
|
||||
rel="${usernameField.rel}"
|
||||
value="${usernameField.value}"
|
||||
data-stripe="${usernameField.dataStripe}"
|
||||
/>
|
||||
</form>
|
||||
`;
|
||||
const existingFieldData: AutofillField = {
|
||||
elementNumber: index,
|
||||
htmlClass: usernameField.classes,
|
||||
|
|
|
@ -92,9 +92,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
|||
const { formElements, formFieldElements } = this.queryAutofillFormAndFieldElements();
|
||||
const autofillFormsData: Record<string, AutofillForm> =
|
||||
this.buildAutofillFormsData(formElements);
|
||||
const autofillFieldsData: AutofillField[] = await this.buildAutofillFieldsData(
|
||||
formFieldElements as FormFieldElement[],
|
||||
);
|
||||
const autofillFieldsData: AutofillField[] = (
|
||||
await this.buildAutofillFieldsData(formFieldElements as FormFieldElement[])
|
||||
).filter((field) => !!field);
|
||||
this.sortAutofillFieldElementsMap();
|
||||
|
||||
if (!autofillFieldsData.length) {
|
||||
|
@ -333,15 +333,18 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
|
|||
* Builds an AutofillField object from the given form element. Will only return
|
||||
* shared field values if the element is a span element. Will not return any label
|
||||
* values if the element is a hidden input element.
|
||||
* @param {ElementWithOpId<FormFieldElement>} element
|
||||
* @param {number} index
|
||||
* @returns {Promise<AutofillField>}
|
||||
* @private
|
||||
*
|
||||
* @param element - The form field element to build the AutofillField object from
|
||||
* @param index - The index of the form field element
|
||||
*/
|
||||
private buildAutofillFieldItem = async (
|
||||
element: ElementWithOpId<FormFieldElement>,
|
||||
index: number,
|
||||
): Promise<AutofillField> => {
|
||||
): Promise<AutofillField | null> => {
|
||||
if (element.closest("button[type='submit']")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
element.opid = `__${index}`;
|
||||
|
||||
const existingAutofillField = this.autofillFieldElements.get(element);
|
||||
|
|
|
@ -98,7 +98,7 @@ describe("InsertAutofillContentService", () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
windowLocationSpy.mockRestore();
|
||||
confirmSpy.mockRestore();
|
||||
document.body.innerHTML = "";
|
||||
|
|
|
@ -146,6 +146,7 @@ import {
|
|||
} from "@bitwarden/common/tools/password-strength";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
||||
import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
|
||||
import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
@ -207,6 +208,7 @@ import { BrowserStateService as StateServiceAbstraction } from "../platform/serv
|
|||
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
|
||||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
||||
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
|
||||
import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service";
|
||||
import BrowserMessagingService from "../platform/services/browser-messaging.service";
|
||||
import { BrowserStateService } from "../platform/services/browser-state.service";
|
||||
|
@ -230,7 +232,7 @@ import RuntimeBackground from "./runtime.background";
|
|||
|
||||
export default class MainBackground {
|
||||
messagingService: MessagingServiceAbstraction;
|
||||
storageService: AbstractStorageService;
|
||||
storageService: AbstractStorageService & ObservableStorageService;
|
||||
secureStorageService: AbstractStorageService;
|
||||
memoryStorageService: AbstractMemoryStorageService;
|
||||
memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService;
|
||||
|
@ -275,6 +277,7 @@ export default class MainBackground {
|
|||
eventUploadService: EventUploadServiceAbstraction;
|
||||
policyService: InternalPolicyServiceAbstraction;
|
||||
sendService: InternalSendServiceAbstraction;
|
||||
sendStateProvider: SendStateProvider;
|
||||
fileUploadService: FileUploadServiceAbstraction;
|
||||
cipherFileUploadService: CipherFileUploadServiceAbstraction;
|
||||
organizationService: InternalOrganizationServiceAbstraction;
|
||||
|
@ -365,22 +368,28 @@ export default class MainBackground {
|
|||
this.cryptoFunctionService = new WebCryptoFunctionService(self);
|
||||
this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService);
|
||||
this.storageService = new BrowserLocalStorageService();
|
||||
|
||||
const mv3MemoryStorageCreator = (partitionName: string) => {
|
||||
// TODO: Consider using multithreaded encrypt service in popup only context
|
||||
return new LocalBackedSessionStorageService(
|
||||
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
|
||||
this.keyGenerationService,
|
||||
new BrowserLocalStorageService(),
|
||||
new BrowserMemoryStorageService(),
|
||||
partitionName,
|
||||
);
|
||||
};
|
||||
|
||||
this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used
|
||||
this.memoryStorageService = BrowserApi.isManifestVersion(3)
|
||||
? new LocalBackedSessionStorageService(
|
||||
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
|
||||
this.keyGenerationService,
|
||||
)
|
||||
? mv3MemoryStorageCreator("stateService")
|
||||
: new MemoryStorageService();
|
||||
this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
|
||||
? new LocalBackedSessionStorageService(
|
||||
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
|
||||
this.keyGenerationService,
|
||||
)
|
||||
? mv3MemoryStorageCreator("stateProviders")
|
||||
: new BackgroundMemoryStorageService();
|
||||
|
||||
const storageServiceProvider = new StorageServiceProvider(
|
||||
this.storageService as BrowserLocalStorageService,
|
||||
this.storageService,
|
||||
this.memoryStorageForStateProviders,
|
||||
);
|
||||
|
||||
|
@ -556,11 +565,12 @@ export default class MainBackground {
|
|||
this.cryptoFunctionService,
|
||||
this.cryptoService,
|
||||
this.encryptService,
|
||||
this.stateService,
|
||||
this.appIdService,
|
||||
this.devicesApiService,
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.stateProvider,
|
||||
this.secureStorageService,
|
||||
this.userDecryptionOptionsService,
|
||||
);
|
||||
|
||||
|
@ -579,6 +589,7 @@ export default class MainBackground {
|
|||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.stateService,
|
||||
this.tokenService,
|
||||
);
|
||||
|
||||
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
|
||||
|
@ -698,11 +709,14 @@ export default class MainBackground {
|
|||
logoutCallback,
|
||||
);
|
||||
this.containerService = new ContainerService(this.cryptoService, this.encryptService);
|
||||
|
||||
this.sendStateProvider = new SendStateProvider(this.stateProvider);
|
||||
this.sendService = new SendService(
|
||||
this.cryptoService,
|
||||
this.i18nService,
|
||||
this.keyGenerationService,
|
||||
this.stateService,
|
||||
this.sendStateProvider,
|
||||
this.encryptService,
|
||||
);
|
||||
this.sendApiService = new SendApiService(
|
||||
this.apiService,
|
||||
|
@ -753,7 +767,6 @@ export default class MainBackground {
|
|||
|
||||
this.autofillService = new AutofillService(
|
||||
this.cipherService,
|
||||
this.stateService,
|
||||
this.autofillSettingsService,
|
||||
this.totpService,
|
||||
this.eventCollectionService,
|
||||
|
|
|
@ -5,6 +5,10 @@ import {
|
|||
CryptoServiceInitOptions,
|
||||
cryptoServiceFactory,
|
||||
} from "../../platform/background/service-factories/crypto-service.factory";
|
||||
import {
|
||||
EncryptServiceInitOptions,
|
||||
encryptServiceFactory,
|
||||
} from "../../platform/background/service-factories/encrypt-service.factory";
|
||||
import {
|
||||
FactoryOptions,
|
||||
CachedServices,
|
||||
|
@ -18,10 +22,11 @@ import {
|
|||
KeyGenerationServiceInitOptions,
|
||||
keyGenerationServiceFactory,
|
||||
} from "../../platform/background/service-factories/key-generation-service.factory";
|
||||
|
||||
import {
|
||||
stateServiceFactory,
|
||||
StateServiceInitOptions,
|
||||
} from "../../platform/background/service-factories/state-service.factory";
|
||||
SendStateProviderInitOptions,
|
||||
sendStateProviderFactory,
|
||||
} from "./send-state-provider.factory";
|
||||
|
||||
type SendServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
|
@ -29,7 +34,8 @@ export type SendServiceInitOptions = SendServiceFactoryOptions &
|
|||
CryptoServiceInitOptions &
|
||||
I18nServiceInitOptions &
|
||||
KeyGenerationServiceInitOptions &
|
||||
StateServiceInitOptions;
|
||||
SendStateProviderInitOptions &
|
||||
EncryptServiceInitOptions;
|
||||
|
||||
export function sendServiceFactory(
|
||||
cache: { sendService?: InternalSendService } & CachedServices,
|
||||
|
@ -44,7 +50,8 @@ export function sendServiceFactory(
|
|||
await cryptoServiceFactory(cache, opts),
|
||||
await i18nServiceFactory(cache, opts),
|
||||
await keyGenerationServiceFactory(cache, opts),
|
||||
await stateServiceFactory(cache, opts),
|
||||
await sendStateProviderFactory(cache, opts),
|
||||
await encryptServiceFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
||||
|
||||
import {
|
||||
CachedServices,
|
||||
FactoryOptions,
|
||||
factory,
|
||||
} from "../../platform/background/service-factories/factory-options";
|
||||
import {
|
||||
StateProviderInitOptions,
|
||||
stateProviderFactory,
|
||||
} from "../../platform/background/service-factories/state-provider.factory";
|
||||
|
||||
type SendStateProviderFactoryOptions = FactoryOptions;
|
||||
|
||||
export type SendStateProviderInitOptions = SendStateProviderFactoryOptions &
|
||||
StateProviderInitOptions;
|
||||
|
||||
export function sendStateProviderFactory(
|
||||
cache: { sendStateProvider?: SendStateProvider } & CachedServices,
|
||||
opts: SendStateProviderInitOptions,
|
||||
): Promise<SendStateProvider> {
|
||||
return factory(
|
||||
cache,
|
||||
"sendStateProvider",
|
||||
opts,
|
||||
async () => new SendStateProvider(await stateProviderFactory(cache, opts)),
|
||||
);
|
||||
}
|
|
@ -1,42 +1,35 @@
|
|||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
|
||||
import MainBackground from "../background/main.background";
|
||||
|
||||
import { onAlarmListener } from "./alarms/on-alarm-listener";
|
||||
import { registerAlarms } from "./alarms/register-alarms";
|
||||
import { BrowserApi } from "./browser/browser-api";
|
||||
import {
|
||||
contextMenusClickedListener,
|
||||
onCommandListener,
|
||||
onInstallListener,
|
||||
runtimeMessageListener,
|
||||
windowsOnFocusChangedListener,
|
||||
tabsOnActivatedListener,
|
||||
tabsOnReplacedListener,
|
||||
tabsOnUpdatedListener,
|
||||
} from "./listeners";
|
||||
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
chrome.commands.onCommand.addListener(onCommandListener);
|
||||
chrome.runtime.onInstalled.addListener(onInstallListener);
|
||||
chrome.alarms.onAlarm.addListener(onAlarmListener);
|
||||
registerAlarms();
|
||||
chrome.windows.onFocusChanged.addListener(windowsOnFocusChangedListener);
|
||||
chrome.tabs.onActivated.addListener(tabsOnActivatedListener);
|
||||
chrome.tabs.onReplaced.addListener(tabsOnReplacedListener);
|
||||
chrome.tabs.onUpdated.addListener(tabsOnUpdatedListener);
|
||||
chrome.contextMenus.onClicked.addListener(contextMenusClickedListener);
|
||||
BrowserApi.messageListener(
|
||||
"runtime.background",
|
||||
(message: { command: string }, sender, sendResponse) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
runtimeMessageListener(message, sender);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const bitwardenMain = ((self as any).bitwardenMain = new MainBackground());
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
bitwardenMain.bootstrap().then(() => {
|
||||
const logService = new ConsoleLogService(false);
|
||||
const bitwardenMain = ((self as any).bitwardenMain = new MainBackground());
|
||||
bitwardenMain
|
||||
.bootstrap()
|
||||
.then(() => {
|
||||
// Finished bootstrapping
|
||||
});
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
startHeartbeat().catch((error) => logService.error(error));
|
||||
}
|
||||
})
|
||||
.catch((error) => logService.error(error));
|
||||
|
||||
/**
|
||||
* Tracks when a service worker was last alive and extends the service worker
|
||||
* lifetime by writing the current time to extension storage every 20 seconds.
|
||||
*/
|
||||
async function runHeartbeat() {
|
||||
await chrome.storage.local.set({ "last-heartbeat": new Date().getTime() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the heartbeat interval which keeps the service worker alive.
|
||||
*/
|
||||
async function startHeartbeat() {
|
||||
// Run the heartbeat once at service worker startup, then again every 20 seconds.
|
||||
runHeartbeat()
|
||||
.then(() => setInterval(runHeartbeat, 20 * 1000))
|
||||
.catch((error) => logService.error(error));
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { MemoryStorageService } from "@bitwarden/common/platform/services/memory
|
|||
|
||||
import { BrowserApi } from "../../browser/browser-api";
|
||||
import BrowserLocalStorageService from "../../services/browser-local-storage.service";
|
||||
import BrowserMemoryStorageService from "../../services/browser-memory-storage.service";
|
||||
import { LocalBackedSessionStorageService } from "../../services/local-backed-session-storage.service";
|
||||
import { BackgroundMemoryStorageService } from "../../storage/background-memory-storage.service";
|
||||
|
||||
|
@ -17,13 +18,14 @@ import {
|
|||
keyGenerationServiceFactory,
|
||||
} from "./key-generation-service.factory";
|
||||
|
||||
type StorageServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type DiskStorageServiceInitOptions = StorageServiceFactoryOptions;
|
||||
export type SecureStorageServiceInitOptions = StorageServiceFactoryOptions;
|
||||
export type MemoryStorageServiceInitOptions = StorageServiceFactoryOptions &
|
||||
export type DiskStorageServiceInitOptions = FactoryOptions;
|
||||
export type SecureStorageServiceInitOptions = FactoryOptions;
|
||||
export type SessionStorageServiceInitOptions = FactoryOptions;
|
||||
export type MemoryStorageServiceInitOptions = FactoryOptions &
|
||||
EncryptServiceInitOptions &
|
||||
KeyGenerationServiceInitOptions;
|
||||
KeyGenerationServiceInitOptions &
|
||||
DiskStorageServiceInitOptions &
|
||||
SessionStorageServiceInitOptions;
|
||||
|
||||
export function diskStorageServiceFactory(
|
||||
cache: { diskStorageService?: AbstractStorageService } & CachedServices,
|
||||
|
@ -47,6 +49,13 @@ export function secureStorageServiceFactory(
|
|||
return factory(cache, "secureStorageService", opts, () => new BrowserLocalStorageService());
|
||||
}
|
||||
|
||||
export function sessionStorageServiceFactory(
|
||||
cache: { sessionStorageService?: AbstractStorageService } & CachedServices,
|
||||
opts: SessionStorageServiceInitOptions,
|
||||
): Promise<AbstractStorageService> {
|
||||
return factory(cache, "sessionStorageService", opts, () => new BrowserMemoryStorageService());
|
||||
}
|
||||
|
||||
export function memoryStorageServiceFactory(
|
||||
cache: { memoryStorageService?: AbstractMemoryStorageService } & CachedServices,
|
||||
opts: MemoryStorageServiceInitOptions,
|
||||
|
@ -56,6 +65,9 @@ export function memoryStorageServiceFactory(
|
|||
return new LocalBackedSessionStorageService(
|
||||
await encryptServiceFactory(cache, opts),
|
||||
await keyGenerationServiceFactory(cache, opts),
|
||||
await diskStorageServiceFactory(cache, opts),
|
||||
await sessionStorageServiceFactory(cache, opts),
|
||||
"serviceFactories",
|
||||
);
|
||||
}
|
||||
return new MemoryStorageService();
|
||||
|
|
|
@ -3,22 +3,9 @@ import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage
|
|||
|
||||
import { Account } from "../../../models/account";
|
||||
import { BrowserComponentState } from "../../../models/browserComponentState";
|
||||
import { BrowserGroupingsComponentState } from "../../../models/browserGroupingsComponentState";
|
||||
import { BrowserSendComponentState } from "../../../models/browserSendComponentState";
|
||||
|
||||
export abstract class BrowserStateService extends BaseStateServiceAbstraction<Account> {
|
||||
getBrowserGroupingComponentState: (
|
||||
options?: StorageOptions,
|
||||
) => Promise<BrowserGroupingsComponentState>;
|
||||
setBrowserGroupingComponentState: (
|
||||
value: BrowserGroupingsComponentState,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getBrowserVaultItemsComponentState: (options?: StorageOptions) => Promise<BrowserComponentState>;
|
||||
setBrowserVaultItemsComponentState: (
|
||||
value: BrowserComponentState,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getBrowserSendComponentState: (options?: StorageOptions) => Promise<BrowserSendComponentState>;
|
||||
setBrowserSendComponentState: (
|
||||
value: BrowserSendComponentState,
|
||||
|
|
|
@ -18,7 +18,6 @@ import { UserId } from "@bitwarden/common/types/guid";
|
|||
|
||||
import { Account } from "../../models/account";
|
||||
import { BrowserComponentState } from "../../models/browserComponentState";
|
||||
import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState";
|
||||
import { BrowserSendComponentState } from "../../models/browserSendComponentState";
|
||||
|
||||
import { BrowserStateService } from "./browser-state.service";
|
||||
|
@ -86,27 +85,6 @@ describe("Browser State Service", () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe("getBrowserGroupingComponentState", () => {
|
||||
it("should return a BrowserGroupingsComponentState", async () => {
|
||||
state.accounts[userId].groupings = new BrowserGroupingsComponentState();
|
||||
|
||||
const actual = await sut.getBrowserGroupingComponentState();
|
||||
expect(actual).toBeInstanceOf(BrowserGroupingsComponentState);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBrowserVaultItemsComponentState", () => {
|
||||
it("should return a BrowserComponentState", async () => {
|
||||
const componentState = new BrowserComponentState();
|
||||
componentState.scrollY = 0;
|
||||
componentState.searchText = "test";
|
||||
state.accounts[userId].ciphers = componentState;
|
||||
|
||||
const actual = await sut.getBrowserVaultItemsComponentState();
|
||||
expect(actual).toStrictEqual(componentState);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBrowserSendComponentState", () => {
|
||||
it("should return a BrowserSendComponentState", async () => {
|
||||
const sendState = new BrowserSendComponentState();
|
||||
|
|
|
@ -16,7 +16,6 @@ import { StateService as BaseStateService } from "@bitwarden/common/platform/ser
|
|||
|
||||
import { Account } from "../../models/account";
|
||||
import { BrowserComponentState } from "../../models/browserComponentState";
|
||||
import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState";
|
||||
import { BrowserSendComponentState } from "../../models/browserSendComponentState";
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
|
||||
|
@ -116,50 +115,6 @@ export class BrowserStateService
|
|||
);
|
||||
}
|
||||
|
||||
async getBrowserGroupingComponentState(
|
||||
options?: StorageOptions,
|
||||
): Promise<BrowserGroupingsComponentState> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
)?.groupings;
|
||||
}
|
||||
|
||||
async setBrowserGroupingComponentState(
|
||||
value: BrowserGroupingsComponentState,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
||||
);
|
||||
account.groupings = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getBrowserVaultItemsComponentState(
|
||||
options?: StorageOptions,
|
||||
): Promise<BrowserComponentState> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
)?.ciphers;
|
||||
}
|
||||
|
||||
async setBrowserVaultItemsComponentState(
|
||||
value: BrowserComponentState,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
||||
);
|
||||
account.ciphers = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getBrowserSendComponentState(options?: StorageOptions): Promise<BrowserSendComponentState> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
|
|
|
@ -2,45 +2,70 @@ import { mock, MockProxy } from "jest-mock-extended";
|
|||
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import {
|
||||
AbstractMemoryStorageService,
|
||||
AbstractStorageService,
|
||||
StorageUpdate,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import BrowserLocalStorageService from "./browser-local-storage.service";
|
||||
import BrowserMemoryStorageService from "./browser-memory-storage.service";
|
||||
import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service";
|
||||
|
||||
describe("Browser Session Storage Service", () => {
|
||||
describe("LocalBackedSessionStorage", () => {
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let keyGenerationService: MockProxy<KeyGenerationService>;
|
||||
let localStorageService: MockProxy<AbstractStorageService>;
|
||||
let sessionStorageService: MockProxy<AbstractMemoryStorageService>;
|
||||
|
||||
let cache: Map<string, any>;
|
||||
const testObj = { a: 1, b: 2 };
|
||||
|
||||
let localStorage: BrowserLocalStorageService;
|
||||
let sessionStorage: BrowserMemoryStorageService;
|
||||
|
||||
const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000"));
|
||||
let getSessionKeySpy: jest.SpyInstance;
|
||||
let sendUpdateSpy: jest.SpyInstance<void, [storageUpdate: StorageUpdate]>;
|
||||
const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input));
|
||||
|
||||
let sut: LocalBackedSessionStorageService;
|
||||
|
||||
const mockExistingSessionKey = (key: SymmetricCryptoKey) => {
|
||||
sessionStorageService.get.mockImplementation((storageKey) => {
|
||||
if (storageKey === "localEncryptionKey_test") {
|
||||
return Promise.resolve(key?.toJSON());
|
||||
}
|
||||
|
||||
return Promise.reject("No implementation for " + storageKey);
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
encryptService = mock<EncryptService>();
|
||||
keyGenerationService = mock<KeyGenerationService>();
|
||||
localStorageService = mock<AbstractStorageService>();
|
||||
sessionStorageService = mock<AbstractMemoryStorageService>();
|
||||
|
||||
sut = new LocalBackedSessionStorageService(encryptService, keyGenerationService);
|
||||
sut = new LocalBackedSessionStorageService(
|
||||
encryptService,
|
||||
keyGenerationService,
|
||||
localStorageService,
|
||||
sessionStorageService,
|
||||
"test",
|
||||
);
|
||||
|
||||
cache = sut["cache"];
|
||||
localStorage = sut["localStorage"];
|
||||
sessionStorage = sut["sessionStorage"];
|
||||
|
||||
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
|
||||
derivedKey: key,
|
||||
salt: "bitwarden-ephemeral",
|
||||
material: null, // Not used
|
||||
});
|
||||
|
||||
getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey");
|
||||
getSessionKeySpy.mockResolvedValue(key);
|
||||
});
|
||||
|
||||
it("should exist", () => {
|
||||
expect(sut).toBeInstanceOf(LocalBackedSessionStorageService);
|
||||
sendUpdateSpy = jest.spyOn(sut, "sendUpdate");
|
||||
sendUpdateSpy.mockReturnValue();
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
|
@ -54,7 +79,7 @@ describe("Browser Session Storage Service", () => {
|
|||
const session = { test: testObj };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(sut, "getSessionEncKey").mockResolvedValue(key);
|
||||
mockExistingSessionKey(key);
|
||||
});
|
||||
|
||||
describe("no session retrieved", () => {
|
||||
|
@ -62,6 +87,7 @@ describe("Browser Session Storage Service", () => {
|
|||
let spy: jest.SpyInstance;
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(sut, "getLocalSession").mockResolvedValue(null);
|
||||
localStorageService.get.mockResolvedValue(null);
|
||||
result = await sut.get("test");
|
||||
});
|
||||
|
||||
|
@ -123,31 +149,31 @@ describe("Browser Session Storage Service", () => {
|
|||
|
||||
describe("remove", () => {
|
||||
it("should save null", async () => {
|
||||
const spy = jest.spyOn(sut, "save");
|
||||
spy.mockResolvedValue(null);
|
||||
await sut.remove("test");
|
||||
expect(spy).toHaveBeenCalledWith("test", null);
|
||||
expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("save", () => {
|
||||
describe("caching", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(localStorage, "get").mockResolvedValue(null);
|
||||
jest.spyOn(sessionStorage, "get").mockResolvedValue(null);
|
||||
jest.spyOn(localStorage, "save").mockResolvedValue();
|
||||
jest.spyOn(sessionStorage, "save").mockResolvedValue();
|
||||
localStorageService.get.mockResolvedValue(null);
|
||||
sessionStorageService.get.mockResolvedValue(null);
|
||||
|
||||
localStorageService.save.mockResolvedValue();
|
||||
sessionStorageService.save.mockResolvedValue();
|
||||
|
||||
encryptService.encrypt.mockResolvedValue(mockEnc("{}"));
|
||||
});
|
||||
|
||||
it("should remove key from cache if value is null", async () => {
|
||||
cache.set("test", {});
|
||||
const deleteSpy = jest.spyOn(cache, "delete");
|
||||
const cacheSetSpy = jest.spyOn(cache, "set");
|
||||
expect(cache.has("test")).toBe(true);
|
||||
await sut.save("test", null);
|
||||
expect(cache.has("test")).toBe(false);
|
||||
expect(deleteSpy).toHaveBeenCalledWith("test");
|
||||
// Don't remove from cache, just replace with null
|
||||
expect(cache.get("test")).toBe(null);
|
||||
expect(cacheSetSpy).toHaveBeenCalledWith("test", null);
|
||||
});
|
||||
|
||||
it("should set cache if value is non-null", async () => {
|
||||
|
@ -197,7 +223,7 @@ describe("Browser Session Storage Service", () => {
|
|||
});
|
||||
|
||||
it("should return the stored symmetric crypto key", async () => {
|
||||
jest.spyOn(sessionStorage, "get").mockResolvedValue({ ...key });
|
||||
sessionStorageService.get.mockResolvedValue({ ...key });
|
||||
const result = await sut.getSessionEncKey();
|
||||
|
||||
expect(result).toStrictEqual(key);
|
||||
|
@ -205,7 +231,6 @@ describe("Browser Session Storage Service", () => {
|
|||
|
||||
describe("new key creation", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(sessionStorage, "get").mockResolvedValue(null);
|
||||
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
|
||||
salt: "salt",
|
||||
material: null,
|
||||
|
@ -218,25 +243,24 @@ describe("Browser Session Storage Service", () => {
|
|||
const result = await sut.getSessionEncKey();
|
||||
|
||||
expect(result).toStrictEqual(key);
|
||||
expect(keyGenerationService.createKeyWithPurpose).toBeCalledTimes(1);
|
||||
expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should store a symmetric crypto key if it makes one", async () => {
|
||||
const spy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
|
||||
await sut.getSessionEncKey();
|
||||
|
||||
expect(spy).toBeCalledWith(key);
|
||||
expect(spy).toHaveBeenCalledWith(key);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLocalSession", () => {
|
||||
it("should return null if session is null", async () => {
|
||||
const spy = jest.spyOn(localStorage, "get").mockResolvedValue(null);
|
||||
const result = await sut.getLocalSession(key);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(spy).toBeCalledWith("session");
|
||||
expect(localStorageService.get).toHaveBeenCalledWith("session_test");
|
||||
});
|
||||
|
||||
describe("non-null sessions", () => {
|
||||
|
@ -245,7 +269,7 @@ describe("Browser Session Storage Service", () => {
|
|||
const decryptedSession = JSON.stringify(session);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(localStorage, "get").mockResolvedValue(encSession.encryptedString);
|
||||
localStorageService.get.mockResolvedValue(encSession.encryptedString);
|
||||
});
|
||||
|
||||
it("should decrypt returned sessions", async () => {
|
||||
|
@ -267,13 +291,12 @@ describe("Browser Session Storage Service", () => {
|
|||
it("should remove state if decryption fails", async () => {
|
||||
encryptService.decryptToUtf8.mockResolvedValue(null);
|
||||
const setSessionEncKeySpy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
|
||||
const removeLocalSessionSpy = jest.spyOn(localStorage, "remove").mockResolvedValue();
|
||||
|
||||
const result = await sut.getLocalSession(key);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(setSessionEncKeySpy).toHaveBeenCalledWith(null);
|
||||
expect(removeLocalSessionSpy).toHaveBeenCalledWith("session");
|
||||
expect(localStorageService.remove).toHaveBeenCalledWith("session_test");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -284,7 +307,7 @@ describe("Browser Session Storage Service", () => {
|
|||
|
||||
it("should encrypt a stringified session", async () => {
|
||||
encryptService.encrypt.mockImplementation(mockEnc);
|
||||
jest.spyOn(localStorage, "save").mockResolvedValue();
|
||||
localStorageService.save.mockResolvedValue();
|
||||
await sut.setLocalSession(testSession, key);
|
||||
|
||||
expect(encryptService.encrypt).toHaveBeenNthCalledWith(1, testJSON, key);
|
||||
|
@ -292,32 +315,31 @@ describe("Browser Session Storage Service", () => {
|
|||
|
||||
it("should remove local session if null", async () => {
|
||||
encryptService.encrypt.mockResolvedValue(null);
|
||||
const spy = jest.spyOn(localStorage, "remove").mockResolvedValue();
|
||||
await sut.setLocalSession(null, key);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith("session");
|
||||
expect(localStorageService.remove).toHaveBeenCalledWith("session_test");
|
||||
});
|
||||
|
||||
it("should save encrypted string", async () => {
|
||||
encryptService.encrypt.mockImplementation(mockEnc);
|
||||
const spy = jest.spyOn(localStorage, "save").mockResolvedValue();
|
||||
await sut.setLocalSession(testSession, key);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith("session", (await mockEnc(testJSON)).encryptedString);
|
||||
expect(localStorageService.save).toHaveBeenCalledWith(
|
||||
"session_test",
|
||||
(await mockEnc(testJSON)).encryptedString,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setSessionKey", () => {
|
||||
it("should remove if null", async () => {
|
||||
const spy = jest.spyOn(sessionStorage, "remove").mockResolvedValue();
|
||||
await sut.setSessionEncKey(null);
|
||||
expect(spy).toHaveBeenCalledWith("localEncryptionKey");
|
||||
expect(sessionStorageService.remove).toHaveBeenCalledWith("localEncryptionKey_test");
|
||||
});
|
||||
|
||||
it("should save key when not null", async () => {
|
||||
const spy = jest.spyOn(sessionStorage, "save").mockResolvedValue();
|
||||
await sut.setSessionEncKey(key);
|
||||
expect(spy).toHaveBeenCalledWith("localEncryptionKey", key);
|
||||
expect(sessionStorageService.save).toHaveBeenCalledWith("localEncryptionKey_test", key);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,40 +1,60 @@
|
|||
import { Subject } from "rxjs";
|
||||
import { Observable, Subject, filter, map, merge, share, tap } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import {
|
||||
AbstractMemoryStorageService,
|
||||
AbstractStorageService,
|
||||
ObservableStorageService,
|
||||
StorageUpdate,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { fromChromeEvent } from "../browser/from-chrome-event";
|
||||
import { devFlag } from "../decorators/dev-flag.decorator";
|
||||
import { devFlagEnabled } from "../flags";
|
||||
|
||||
import BrowserLocalStorageService from "./browser-local-storage.service";
|
||||
import BrowserMemoryStorageService from "./browser-memory-storage.service";
|
||||
|
||||
const keys = {
|
||||
encKey: "localEncryptionKey",
|
||||
sessionKey: "session",
|
||||
};
|
||||
|
||||
export class LocalBackedSessionStorageService extends AbstractMemoryStorageService {
|
||||
export class LocalBackedSessionStorageService
|
||||
extends AbstractMemoryStorageService
|
||||
implements ObservableStorageService
|
||||
{
|
||||
private cache = new Map<string, unknown>();
|
||||
private localStorage = new BrowserLocalStorageService();
|
||||
private sessionStorage = new BrowserMemoryStorageService();
|
||||
private updatesSubject = new Subject<StorageUpdate>();
|
||||
updates$;
|
||||
|
||||
private commandName = `localBackedSessionStorage_${this.name}`;
|
||||
private encKey = `localEncryptionKey_${this.name}`;
|
||||
private sessionKey = `session_${this.name}`;
|
||||
|
||||
updates$: Observable<StorageUpdate>;
|
||||
|
||||
constructor(
|
||||
private encryptService: EncryptService,
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private localStorage: AbstractStorageService,
|
||||
private sessionStorage: AbstractStorageService,
|
||||
private name: string,
|
||||
) {
|
||||
super();
|
||||
this.updates$ = this.updatesSubject.asObservable();
|
||||
|
||||
const remoteObservable = fromChromeEvent(chrome.runtime.onMessage).pipe(
|
||||
filter(([msg]) => msg.command === this.commandName),
|
||||
map(([msg]) => msg.update as StorageUpdate),
|
||||
tap((update) => {
|
||||
if (update.updateType === "remove") {
|
||||
this.cache.set(update.key, null);
|
||||
} else {
|
||||
this.cache.delete(update.key);
|
||||
}
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
remoteObservable.subscribe();
|
||||
|
||||
this.updates$ = merge(this.updatesSubject.asObservable(), remoteObservable);
|
||||
}
|
||||
|
||||
get valuesRequireDeserialization(): boolean {
|
||||
|
@ -70,23 +90,37 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
|||
|
||||
async save<T>(key: string, obj: T): Promise<void> {
|
||||
if (obj == null) {
|
||||
this.cache.delete(key);
|
||||
} else {
|
||||
this.cache.set(key, obj);
|
||||
return await this.remove(key);
|
||||
}
|
||||
|
||||
this.cache.set(key, obj);
|
||||
await this.updateLocalSessionValue(key, obj);
|
||||
this.sendUpdate({ key, updateType: "save" });
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<void> {
|
||||
this.cache.set(key, null);
|
||||
await this.updateLocalSessionValue(key, null);
|
||||
this.sendUpdate({ key, updateType: "remove" });
|
||||
}
|
||||
|
||||
sendUpdate(storageUpdate: StorageUpdate) {
|
||||
this.updatesSubject.next(storageUpdate);
|
||||
void chrome.runtime.sendMessage({
|
||||
command: this.commandName,
|
||||
update: storageUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
private async updateLocalSessionValue<T>(key: string, obj: T) {
|
||||
const sessionEncKey = await this.getSessionEncKey();
|
||||
const localSession = (await this.getLocalSession(sessionEncKey)) ?? {};
|
||||
localSession[key] = obj;
|
||||
await this.setLocalSession(localSession, sessionEncKey);
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<void> {
|
||||
await this.save(key, null);
|
||||
}
|
||||
|
||||
async getLocalSession(encKey: SymmetricCryptoKey): Promise<Record<string, unknown>> {
|
||||
const local = await this.localStorage.get<string>(keys.sessionKey);
|
||||
const local = await this.localStorage.get<string>(this.sessionKey);
|
||||
|
||||
if (local == null) {
|
||||
return null;
|
||||
|
@ -100,7 +134,7 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
|||
if (sessionJson == null) {
|
||||
// Error with decryption -- session is lost, delete state and key and start over
|
||||
await this.setSessionEncKey(null);
|
||||
await this.localStorage.remove(keys.sessionKey);
|
||||
await this.localStorage.remove(this.sessionKey);
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(sessionJson);
|
||||
|
@ -119,9 +153,9 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
|||
// Make sure we're storing the jsonified version of the session
|
||||
const jsonSession = JSON.parse(JSON.stringify(session));
|
||||
if (session == null) {
|
||||
await this.localStorage.remove(keys.sessionKey);
|
||||
await this.localStorage.remove(this.sessionKey);
|
||||
} else {
|
||||
await this.localStorage.save(keys.sessionKey, jsonSession);
|
||||
await this.localStorage.save(this.sessionKey, jsonSession);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,13 +164,13 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
|||
const encSession = await this.encryptService.encrypt(jsonSession, key);
|
||||
|
||||
if (encSession == null) {
|
||||
return await this.localStorage.remove(keys.sessionKey);
|
||||
return await this.localStorage.remove(this.sessionKey);
|
||||
}
|
||||
await this.localStorage.save(keys.sessionKey, encSession.encryptedString);
|
||||
await this.localStorage.save(this.sessionKey, encSession.encryptedString);
|
||||
}
|
||||
|
||||
async getSessionEncKey(): Promise<SymmetricCryptoKey> {
|
||||
let storedKey = await this.sessionStorage.get<SymmetricCryptoKey>(keys.encKey);
|
||||
let storedKey = await this.sessionStorage.get<SymmetricCryptoKey>(this.encKey);
|
||||
if (storedKey == null || Object.keys(storedKey).length == 0) {
|
||||
const generatedKey = await this.keyGenerationService.createKeyWithPurpose(
|
||||
128,
|
||||
|
@ -153,9 +187,9 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi
|
|||
|
||||
async setSessionEncKey(input: SymmetricCryptoKey): Promise<void> {
|
||||
if (input == null) {
|
||||
await this.sessionStorage.remove(keys.encKey);
|
||||
await this.sessionStorage.remove(this.encKey);
|
||||
} else {
|
||||
await this.sessionStorage.save(keys.encKey, input);
|
||||
await this.sessionStorage.save(this.encKey, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,7 +172,7 @@ describe("Browser Utils Service", () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("sends a copy to clipboard message to the desktop application if a user is using the safari browser", async () => {
|
||||
|
@ -264,7 +264,7 @@ describe("Browser Utils Service", () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("sends a ready from clipboard message to the desktop application if a user is using the safari browser", async () => {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { BrowserApi } from "../platform/browser/browser-api";
|
|||
import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service";
|
||||
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
||||
import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service";
|
||||
import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service";
|
||||
|
||||
import { routerTransition } from "./app-routing.animations";
|
||||
import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component";
|
||||
|
@ -37,6 +38,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
private i18nService: I18nService,
|
||||
private router: Router,
|
||||
private stateService: BrowserStateService,
|
||||
private vaultBrowserStateService: VaultBrowserStateService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private ngZone: NgZone,
|
||||
private platformUtilsService: ForegroundPlatformUtilsService,
|
||||
|
@ -227,8 +229,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
await Promise.all([
|
||||
this.stateService.setBrowserGroupingComponentState(null),
|
||||
this.stateService.setBrowserVaultItemsComponentState(null),
|
||||
this.vaultBrowserStateService.setBrowserGroupingsComponentState(null),
|
||||
this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null),
|
||||
this.stateService.setBrowserSendComponentState(null),
|
||||
this.stateService.setBrowserSendTypeComponentState(null),
|
||||
]);
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
AuthRequestServiceAbstraction,
|
||||
LoginStrategyServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
|
@ -47,6 +48,7 @@ import {
|
|||
UserNotificationSettingsService,
|
||||
UserNotificationSettingsServiceAbstraction,
|
||||
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
|
@ -85,7 +87,8 @@ import { DialogService } from "@bitwarden/components";
|
|||
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
|
||||
|
||||
import { UnauthGuardService } from "../../auth/popup/services";
|
||||
import { AutofillService } from "../../autofill/services/abstractions/autofill.service";
|
||||
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
||||
import AutofillService from "../../autofill/services/autofill.service";
|
||||
import MainBackground from "../../background/main.background";
|
||||
import { Account } from "../../models/account";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
|
@ -102,6 +105,7 @@ import { ForegroundPlatformUtilsService } from "../../platform/services/platform
|
|||
import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider";
|
||||
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
|
||||
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
|
||||
import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service";
|
||||
import { VaultFilterService } from "../../vault/services/vault-filter.service";
|
||||
|
||||
import { DebounceNavigationService } from "./debounce-navigation.service";
|
||||
|
@ -311,10 +315,22 @@ const safeProviders: SafeProvider[] = [
|
|||
useClass: BrowserLocalStorageService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AutofillServiceAbstraction,
|
||||
useExisting: AutofillService,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AutofillService,
|
||||
useFactory: getBgService<AutofillService>("autofillService"),
|
||||
deps: [],
|
||||
deps: [
|
||||
CipherService,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
TotpService,
|
||||
EventCollectionServiceAbstraction,
|
||||
LogService,
|
||||
DomainSettingsService,
|
||||
UserVerificationService,
|
||||
BillingAccountProfileStateService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: VaultExportServiceAbstraction,
|
||||
|
@ -377,6 +393,13 @@ const safeProviders: SafeProvider[] = [
|
|||
provide: OBSERVABLE_DISK_STORAGE,
|
||||
useExisting: AbstractStorageService,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: VaultBrowserStateService,
|
||||
useFactory: (stateProvider: StateProvider) => {
|
||||
return new VaultBrowserStateService(stateProvider);
|
||||
},
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: StateServiceAbstraction,
|
||||
useFactory: (
|
||||
|
|
|
@ -20,7 +20,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
|||
import { BrowserGroupingsComponentState } from "../../../../models/browserGroupingsComponentState";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { BrowserStateService } from "../../../../platform/services/abstractions/browser-state.service";
|
||||
import { VaultBrowserStateService } from "../../../services/vault-browser-state.service";
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
const ComponentId = "VaultComponent";
|
||||
|
@ -84,8 +84,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||
private platformUtilsService: PlatformUtilsService,
|
||||
private searchService: SearchService,
|
||||
private location: Location,
|
||||
private browserStateService: BrowserStateService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private vaultBrowserStateService: VaultBrowserStateService,
|
||||
) {
|
||||
this.noFolderListSize = 100;
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||
this.showLeftHeader = !(
|
||||
BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
|
||||
);
|
||||
await this.browserStateService.setBrowserVaultItemsComponentState(null);
|
||||
await this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null);
|
||||
|
||||
this.broadcasterService.subscribe(ComponentId, (message: any) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
|
@ -120,7 +120,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||
const restoredScopeState = await this.restoreState();
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
this.state = await this.browserStateService.getBrowserGroupingComponentState();
|
||||
this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState();
|
||||
if (this.state?.searchText) {
|
||||
this.searchText = this.state.searchText;
|
||||
} else if (params.searchText) {
|
||||
|
@ -413,11 +413,11 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||
collections: this.collections,
|
||||
deletedCount: this.deletedCount,
|
||||
});
|
||||
await this.browserStateService.setBrowserGroupingComponentState(this.state);
|
||||
await this.vaultBrowserStateService.setBrowserGroupingsComponentState(this.state);
|
||||
}
|
||||
|
||||
private async restoreState(): Promise<boolean> {
|
||||
this.state = await this.browserStateService.getBrowserGroupingComponentState();
|
||||
this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState();
|
||||
if (this.state == null) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
|||
import { BrowserComponentState } from "../../../../models/browserComponentState";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { BrowserStateService } from "../../../../platform/services/abstractions/browser-state.service";
|
||||
import { VaultBrowserStateService } from "../../../services/vault-browser-state.service";
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
const ComponentId = "VaultItemsComponent";
|
||||
|
@ -59,7 +59,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
|
|||
private ngZone: NgZone,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private stateService: BrowserStateService,
|
||||
private stateService: VaultBrowserStateService,
|
||||
private i18nService: I18nService,
|
||||
private collectionService: CollectionService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import {
|
||||
FakeAccountService,
|
||||
mockAccountServiceWith,
|
||||
} from "@bitwarden/common/../spec/fake-account-service";
|
||||
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { BrowserComponentState } from "../../models/browserComponentState";
|
||||
import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState";
|
||||
|
||||
import {
|
||||
VAULT_BROWSER_COMPONENT,
|
||||
VAULT_BROWSER_GROUPINGS_COMPONENT,
|
||||
VaultBrowserStateService,
|
||||
} from "./vault-browser-state.service";
|
||||
|
||||
describe("Vault Browser State Service", () => {
|
||||
let stateProvider: FakeStateProvider;
|
||||
|
||||
let accountService: FakeAccountService;
|
||||
let stateService: VaultBrowserStateService;
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
stateService = new VaultBrowserStateService(stateProvider);
|
||||
});
|
||||
|
||||
describe("getBrowserGroupingsComponentState", () => {
|
||||
it("should return a BrowserGroupingsComponentState", async () => {
|
||||
await stateService.setBrowserGroupingsComponentState(new BrowserGroupingsComponentState());
|
||||
|
||||
const actual = await stateService.getBrowserGroupingsComponentState();
|
||||
|
||||
expect(actual).toBeInstanceOf(BrowserGroupingsComponentState);
|
||||
});
|
||||
|
||||
it("should deserialize BrowserGroupingsComponentState", () => {
|
||||
const sut = VAULT_BROWSER_GROUPINGS_COMPONENT;
|
||||
|
||||
const expectedState = {
|
||||
deletedCount: 0,
|
||||
collectionCounts: new Map<string, number>(),
|
||||
folderCounts: new Map<string, number>(),
|
||||
typeCounts: new Map<CipherType, number>(),
|
||||
};
|
||||
|
||||
const result = sut.deserializer(
|
||||
JSON.parse(JSON.stringify(expectedState)) as Jsonify<BrowserGroupingsComponentState>,
|
||||
);
|
||||
|
||||
expect(result).toEqual(expectedState);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBrowserVaultItemsComponentState", () => {
|
||||
it("should deserialize BrowserComponentState", () => {
|
||||
const sut = VAULT_BROWSER_COMPONENT;
|
||||
|
||||
const expectedState = {
|
||||
scrollY: 0,
|
||||
searchText: "test",
|
||||
};
|
||||
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(expectedState)));
|
||||
|
||||
expect(result).toEqual(expectedState);
|
||||
});
|
||||
|
||||
it("should return a BrowserComponentState", async () => {
|
||||
const componentState = new BrowserComponentState();
|
||||
componentState.scrollY = 0;
|
||||
componentState.searchText = "test";
|
||||
|
||||
await stateService.setBrowserVaultItemsComponentState(componentState);
|
||||
|
||||
const actual = await stateService.getBrowserVaultItemsComponentState();
|
||||
expect(actual).toStrictEqual(componentState);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
import { Observable, firstValueFrom } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
ActiveUserState,
|
||||
KeyDefinition,
|
||||
StateProvider,
|
||||
VAULT_BROWSER_MEMORY,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
|
||||
import { BrowserComponentState } from "../../models/browserComponentState";
|
||||
import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState";
|
||||
|
||||
export const VAULT_BROWSER_GROUPINGS_COMPONENT = new KeyDefinition<BrowserGroupingsComponentState>(
|
||||
VAULT_BROWSER_MEMORY,
|
||||
"vault_browser_groupings_component",
|
||||
{
|
||||
deserializer: (obj: Jsonify<BrowserGroupingsComponentState>) =>
|
||||
BrowserGroupingsComponentState.fromJSON(obj),
|
||||
},
|
||||
);
|
||||
|
||||
export const VAULT_BROWSER_COMPONENT = new KeyDefinition<BrowserComponentState>(
|
||||
VAULT_BROWSER_MEMORY,
|
||||
"vault_browser_component",
|
||||
{
|
||||
deserializer: (obj: Jsonify<BrowserComponentState>) => BrowserComponentState.fromJSON(obj),
|
||||
},
|
||||
);
|
||||
|
||||
export class VaultBrowserStateService {
|
||||
vaultBrowserGroupingsComponentState$: Observable<BrowserGroupingsComponentState>;
|
||||
vaultBrowserComponentState$: Observable<BrowserComponentState>;
|
||||
|
||||
private activeUserVaultBrowserGroupingsComponentState: ActiveUserState<BrowserGroupingsComponentState>;
|
||||
private activeUserVaultBrowserComponentState: ActiveUserState<BrowserComponentState>;
|
||||
|
||||
constructor(protected stateProvider: StateProvider) {
|
||||
this.activeUserVaultBrowserGroupingsComponentState = this.stateProvider.getActive(
|
||||
VAULT_BROWSER_GROUPINGS_COMPONENT,
|
||||
);
|
||||
this.activeUserVaultBrowserComponentState =
|
||||
this.stateProvider.getActive(VAULT_BROWSER_COMPONENT);
|
||||
|
||||
this.vaultBrowserGroupingsComponentState$ =
|
||||
this.activeUserVaultBrowserGroupingsComponentState.state$;
|
||||
this.vaultBrowserComponentState$ = this.activeUserVaultBrowserComponentState.state$;
|
||||
}
|
||||
|
||||
async getBrowserGroupingsComponentState(): Promise<BrowserGroupingsComponentState> {
|
||||
return await firstValueFrom(this.vaultBrowserGroupingsComponentState$);
|
||||
}
|
||||
|
||||
async setBrowserGroupingsComponentState(value: BrowserGroupingsComponentState): Promise<void> {
|
||||
await this.activeUserVaultBrowserGroupingsComponentState.update(() => value);
|
||||
}
|
||||
|
||||
async getBrowserVaultItemsComponentState(): Promise<BrowserComponentState> {
|
||||
return await firstValueFrom(this.vaultBrowserComponentState$);
|
||||
}
|
||||
|
||||
async setBrowserVaultItemsComponentState(value: BrowserComponentState): Promise<void> {
|
||||
await this.activeUserVaultBrowserComponentState.update(() => value);
|
||||
}
|
||||
}
|
|
@ -106,6 +106,7 @@ import {
|
|||
PasswordStrengthServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/password-strength";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service";
|
||||
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
|
@ -194,6 +195,7 @@ export class Main {
|
|||
sendProgram: SendProgram;
|
||||
logService: ConsoleLogService;
|
||||
sendService: SendService;
|
||||
sendStateProvider: SendStateProvider;
|
||||
fileUploadService: FileUploadService;
|
||||
cipherFileUploadService: CipherFileUploadService;
|
||||
keyConnectorService: KeyConnectorService;
|
||||
|
@ -388,11 +390,14 @@ export class Main {
|
|||
|
||||
this.fileUploadService = new FileUploadService(this.logService);
|
||||
|
||||
this.sendStateProvider = new SendStateProvider(this.stateProvider);
|
||||
|
||||
this.sendService = new SendService(
|
||||
this.cryptoService,
|
||||
this.i18nService,
|
||||
this.keyGenerationService,
|
||||
this.stateService,
|
||||
this.sendStateProvider,
|
||||
this.encryptService,
|
||||
);
|
||||
|
||||
this.cipherFileUploadService = new CipherFileUploadService(
|
||||
|
@ -455,11 +460,12 @@ export class Main {
|
|||
this.cryptoFunctionService,
|
||||
this.cryptoService,
|
||||
this.encryptService,
|
||||
this.stateService,
|
||||
this.appIdService,
|
||||
this.devicesApiService,
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.stateProvider,
|
||||
this.secureStorageService,
|
||||
this.userDecryptionOptionsService,
|
||||
);
|
||||
|
||||
|
@ -503,6 +509,7 @@ export class Main {
|
|||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.stateService,
|
||||
this.tokenService,
|
||||
);
|
||||
|
||||
this.configApiService = new ConfigApiService(this.apiService, this.tokenService);
|
||||
|
@ -712,12 +719,6 @@ export class Main {
|
|||
this.containerService.attachToGlobal(global);
|
||||
await this.i18nService.init();
|
||||
this.twoFactorService.init();
|
||||
|
||||
const installedVersion = await this.stateService.getInstalledVersion();
|
||||
const currentVersion = await this.platformUtilsService.getApplicationVersion();
|
||||
if (installedVersion == null || installedVersion !== currentVersion) {
|
||||
await this.stateService.setInstalledVersion(currentVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export class SendRemovePasswordCommand {
|
|||
try {
|
||||
await this.sendApiService.removePassword(id);
|
||||
|
||||
const updatedSend = await this.sendService.get(id);
|
||||
const updatedSend = await firstValueFrom(this.sendService.get$(id));
|
||||
const decSend = await updatedSend.decrypt();
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -15,7 +15,7 @@ manual_test = []
|
|||
[dependencies]
|
||||
aes = "=0.8.4"
|
||||
anyhow = "=1.0.80"
|
||||
arboard = { version = "=3.3.0", default-features = false, features = ["wayland-data-control"] }
|
||||
arboard = { version = "=3.3.2", default-features = false, features = ["wayland-data-control"] }
|
||||
base64 = "=0.22.0"
|
||||
cbc = { version = "=0.1.2", features = ["alloc"] }
|
||||
napi = { version = "=2.16.0", features = ["async"] }
|
||||
|
|
|
@ -19,18 +19,16 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node16": "1.0.4",
|
||||
"@types/node": "18.19.19",
|
||||
"@types/node": "18.19.29",
|
||||
"@types/node-ipc": "9.2.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
},
|
||||
"../../../libs/common": {
|
||||
"name": "@bitwarden/common",
|
||||
"version": "0.0.0",
|
||||
"license": "GPL-3.0"
|
||||
},
|
||||
"../../../libs/node": {
|
||||
"name": "@bitwarden/node",
|
||||
"version": "0.0.0",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
|
@ -57,9 +55,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
|
@ -79,9 +77,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA=="
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.10.tgz",
|
||||
"integrity": "sha512-PiaIWIoPvO6qm6t114ropMCagj6YAF24j9OkCA2mJDXFnlionEwhsBCJ8yek4aib575BI3OkART/90WsgHgLWw=="
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
|
@ -99,9 +97,9 @@
|
|||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.19.tgz",
|
||||
"integrity": "sha512-qqV6hSy9zACEhQUy5CEGeuXAZN0fNjqLWRIvOXOwdFYhFoKBiY08VKR5kgchr90+TitLVhpUEb54hk4bYaArUw==",
|
||||
"version": "18.19.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.29.tgz",
|
||||
"integrity": "sha512-5pAX7ggTmWZdhUrhRWLPf+5oM7F80bcKVCBbr0zwEkTNzTJL2CWQjznpFgHYy6GrzkYi2Yjy7DHKoynFxqPV8g==",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
|
@ -116,9 +114,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.8.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
|
||||
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
@ -127,9 +125,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
|
||||
"integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
|
@ -217,9 +215,9 @@
|
|||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node16": "1.0.4",
|
||||
"@types/node": "18.19.19",
|
||||
"@types/node": "18.19.29",
|
||||
"@types/node-ipc": "9.2.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
|
|
|
@ -53,18 +53,6 @@ export class InitService {
|
|||
const htmlEl = this.win.document.documentElement;
|
||||
htmlEl.classList.add("os_" + this.platformUtilsService.getDeviceString());
|
||||
this.themingService.applyThemeChangesTo(this.document);
|
||||
let installAction = null;
|
||||
const installedVersion = await this.stateService.getInstalledVersion();
|
||||
const currentVersion = await this.platformUtilsService.getApplicationVersion();
|
||||
if (installedVersion == null) {
|
||||
installAction = "install";
|
||||
} else if (installedVersion !== currentVersion) {
|
||||
installAction = "update";
|
||||
}
|
||||
|
||||
if (installAction != null) {
|
||||
await this.stateService.setInstalledVersion(currentVersion);
|
||||
}
|
||||
|
||||
const containerService = new ContainerService(this.cryptoService, this.encryptService);
|
||||
containerService.attachToGlobal(this.win);
|
||||
|
|
|
@ -12,6 +12,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
|||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
|
@ -23,7 +24,10 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { LockComponent } from "./lock.component";
|
||||
|
@ -49,6 +53,9 @@ describe("LockComponent", () => {
|
|||
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
|
||||
let activatedRouteMock: MockProxy<ActivatedRoute>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
beforeEach(async () => {
|
||||
stateServiceMock = mock<StateService>();
|
||||
stateServiceMock.activeAccount$ = of(null);
|
||||
|
@ -147,6 +154,10 @@ describe("LockComponent", () => {
|
|||
provide: BiometricStateService,
|
||||
useValue: biometricStateService,
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: accountService,
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
|
|
@ -9,6 +9,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
|||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
|
@ -59,6 +60,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
userVerificationService: UserVerificationService,
|
||||
pinCryptoService: PinCryptoServiceAbstraction,
|
||||
biometricStateService: BiometricStateService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
|
@ -81,6 +83,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
userVerificationService,
|
||||
pinCryptoService,
|
||||
biometricStateService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
LoginEmailServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
|
@ -57,6 +58,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
|
|||
deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
authRequestService: AuthRequestServiceAbstraction,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
private location: Location,
|
||||
) {
|
||||
super(
|
||||
|
@ -78,6 +80,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {
|
|||
deviceTrustCryptoService,
|
||||
authRequestService,
|
||||
loginStrategyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
super.onSuccessfulLogin = () => {
|
||||
|
|
|
@ -3,7 +3,6 @@ import { FormBuilder } from "@angular/forms";
|
|||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
|
@ -37,8 +36,6 @@ const BroadcasterSubscriptionId = "LoginComponent";
|
|||
export class LoginComponent extends BaseLoginComponent implements OnDestroy {
|
||||
@ViewChild("environment", { read: ViewContainerRef, static: true })
|
||||
environmentModal: ViewContainerRef;
|
||||
@ViewChild("environmentSelector", { read: ViewContainerRef, static: true })
|
||||
environmentSelector: EnvironmentSelectorComponent;
|
||||
|
||||
protected componentDestroyed$: Subject<void> = new Subject();
|
||||
webVaultHostname = "";
|
||||
|
|
|
@ -9,19 +9,607 @@
|
|||
"version": "2024.3.2",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bitwarden/desktop-native": "file:../desktop_native"
|
||||
"@bitwarden/desktop-native": "file:../desktop_native",
|
||||
"argon2": "0.31.0"
|
||||
}
|
||||
},
|
||||
"../desktop_native": {
|
||||
"version": "0.1.0",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "2.14.8"
|
||||
"@napi-rs/cli": "2.16.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@bitwarden/desktop-native": {
|
||||
"resolved": "../desktop_native",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"make-dir": "^3.1.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"nopt": "^5.0.0",
|
||||
"npmlog": "^5.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"tar": "^6.1.11"
|
||||
},
|
||||
"bin": {
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@phc/format": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
|
||||
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
||||
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
|
||||
},
|
||||
"node_modules/are-we-there-yet": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
|
||||
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
|
||||
"dependencies": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/argon2": {
|
||||
"version": "0.31.0",
|
||||
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.31.0.tgz",
|
||||
"integrity": "sha512-r56NWwlE3tjD/FIqL1T+V4Ka+Mb5yMF35w1YWHpwpEjeONXBUbxmjhWkWqY63mse8lpcZ+ZZIGpKL+s+qXhyfg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"@phc/format": "^1.0.0",
|
||||
"node-addon-api": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
|
||||
"bin": {
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"node_modules/console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/gauge": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
|
||||
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
|
||||
"dependencies": {
|
||||
"aproba": "^1.0.3 || ^2.0.0",
|
||||
"color-support": "^1.1.2",
|
||||
"console-control-strings": "^1.0.0",
|
||||
"has-unicode": "^2.0.1",
|
||||
"object-assign": "^4.1.1",
|
||||
"signal-exit": "^3.0.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wide-align": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"dependencies": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz",
|
||||
"integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==",
|
||||
"engines": {
|
||||
"node": "^16 || ^18 || >= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
|
||||
"dependencies": {
|
||||
"abbrev": "1"
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/npmlog": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
|
||||
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
|
||||
"dependencies": {
|
||||
"are-we-there-yet": "^2.0.0",
|
||||
"console-control-strings": "^1.1.0",
|
||||
"gauge": "^3.0.0",
|
||||
"set-blocking": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
|
||||
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^5.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wide-align": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
|
||||
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
|
||||
"dependencies": {
|
||||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,12 @@
|
|||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { DeviceKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { Account } from "../../models/account";
|
||||
|
||||
export class ElectronStateService extends BaseStateService<GlobalState, Account> {
|
||||
private partialKeys = {
|
||||
deviceKey: "_deviceKey",
|
||||
};
|
||||
|
||||
async addAccount(account: Account) {
|
||||
// Apply desktop overides to default account values
|
||||
account = new Account(account);
|
||||
await super.addAccount(account);
|
||||
}
|
||||
|
||||
override async getDeviceKey(options?: StorageOptions): Promise<DeviceKey | null> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const b64DeviceKey = await this.secureStorageService.get<string>(
|
||||
`${options.userId}${this.partialKeys.deviceKey}`,
|
||||
options,
|
||||
);
|
||||
|
||||
if (b64DeviceKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SymmetricCryptoKey(Utils.fromB64ToArray(b64DeviceKey)) as DeviceKey;
|
||||
}
|
||||
|
||||
override async setDeviceKey(value: DeviceKey, options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.saveSecureStorageKey(this.partialKeys.deviceKey, value.keyB64, options);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,11 @@ module.exports = {
|
|||
...sharedConfig,
|
||||
preset: "jest-preset-angular",
|
||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||
prefix: "<rootDir>/",
|
||||
}),
|
||||
moduleNameMapper: pathsToModuleNameMapper(
|
||||
// lets us use @bitwarden/common/spec in web tests
|
||||
{ "@bitwarden/common/spec": ["../../libs/common/spec"], ...(compilerOptions?.paths ?? {}) },
|
||||
{
|
||||
prefix: "<rootDir>/",
|
||||
},
|
||||
),
|
||||
};
|
||||
|
|
|
@ -26,6 +26,10 @@ export class OrganizationUserView {
|
|||
twoFactorEnabled: boolean;
|
||||
usesKeyConnector: boolean;
|
||||
hasMasterPassword: boolean;
|
||||
/**
|
||||
* True if this organizaztion user has been granted access to Secrets Manager, false otherwise.
|
||||
*/
|
||||
accessSecretsManager: boolean;
|
||||
|
||||
collections: CollectionAccessSelectionView[] = [];
|
||||
groups: string[] = [];
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<a routerLink="." class="tw-m-5 tw-mt-7 tw-block" [appA11yTitle]="'adminConsole' | i18n">
|
||||
<bit-icon [icon]="logo"></bit-icon>
|
||||
</a>
|
||||
<org-switcher [filter]="orgFilter"></org-switcher>
|
||||
<org-switcher [filter]="orgFilter" [hideNewButton]="hideNewOrgButton$ | async"></org-switcher>
|
||||
|
||||
<bit-nav-item
|
||||
icon="bwi-collection"
|
||||
|
@ -105,6 +105,8 @@
|
|||
*ngIf="organization.canManageScim"
|
||||
></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
|
||||
<app-toggle-width></app-toggle-width>
|
||||
</nav>
|
||||
|
||||
<ng-container *ngIf="organization$ | async as organization">
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
@ -23,6 +25,7 @@ import { BannerModule, IconModule, LayoutComponent, NavigationModule } from "@bi
|
|||
|
||||
import { PaymentMethodWarningsModule } from "../../../billing/shared";
|
||||
import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component";
|
||||
import { ToggleWidthComponent } from "../../../layouts/toggle-width.component";
|
||||
import { AdminConsoleLogo } from "../../icons/admin-console-logo";
|
||||
|
||||
@Component({
|
||||
|
@ -39,6 +42,7 @@ import { AdminConsoleLogo } from "../../icons/admin-console-logo";
|
|||
OrgSwitcherComponent,
|
||||
BannerModule,
|
||||
PaymentMethodWarningsModule,
|
||||
ToggleWidthComponent,
|
||||
],
|
||||
})
|
||||
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
|
@ -48,6 +52,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
|||
|
||||
organization$: Observable<Organization>;
|
||||
showPaymentAndHistory$: Observable<boolean>;
|
||||
hideNewOrgButton$: Observable<boolean>;
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
|
@ -61,6 +66,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
|||
private organizationService: OrganizationService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private configService: ConfigService,
|
||||
private policyService: PolicyService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -85,6 +91,8 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
|||
org?.canEditPaymentMethods,
|
||||
),
|
||||
);
|
||||
|
||||
this.hideNewOrgButton$ = this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
|
|
@ -571,7 +571,8 @@ export class PeopleComponent
|
|||
}
|
||||
|
||||
async bulkEnableSM() {
|
||||
const users = this.getCheckedUsers();
|
||||
const users = this.getCheckedUsers().filter((ou) => !ou.accessSecretsManager);
|
||||
|
||||
if (users.length === 0) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
|
@ -588,6 +589,7 @@ export class PeopleComponent
|
|||
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
this.selectAll(false);
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async events(user: OrganizationUserView) {
|
||||
|
|
|
@ -6,10 +6,13 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
|||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { Send } from "@bitwarden/common/tools/send/models/domain/send";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
|
@ -41,6 +44,9 @@ describe("KeyRotationService", () => {
|
|||
let mockStateService: MockProxy<StateService>;
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
beforeAll(() => {
|
||||
mockApiService = mock<UserKeyRotationApiService>();
|
||||
mockCipherService = mock<CipherService>();
|
||||
|
@ -65,6 +71,7 @@ describe("KeyRotationService", () => {
|
|||
mockCryptoService,
|
||||
mockEncryptService,
|
||||
mockStateService,
|
||||
mockAccountService,
|
||||
mockConfigService,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
@ -34,6 +35,7 @@ export class UserKeyRotationService {
|
|||
private cryptoService: CryptoService,
|
||||
private encryptService: EncryptService,
|
||||
private stateService: StateService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
|
@ -90,7 +92,12 @@ export class UserKeyRotationService {
|
|||
await this.rotateUserKeyAndEncryptedDataLegacy(request);
|
||||
}
|
||||
|
||||
await this.deviceTrustCryptoService.rotateDevicesTrust(newUserKey, masterPasswordHash);
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.deviceTrustCryptoService.rotateDevicesTrust(
|
||||
activeAccount.id,
|
||||
newUserKey,
|
||||
masterPasswordHash,
|
||||
);
|
||||
}
|
||||
|
||||
private async encryptPrivateKey(newUserKey: UserKey): Promise<EncryptedString | null> {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul
|
|||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
@ -47,6 +48,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
userVerificationService: UserVerificationService,
|
||||
pinCryptoService: PinCryptoServiceAbstraction,
|
||||
biometricStateService: BiometricStateService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
|
@ -69,6 +71,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
userVerificationService,
|
||||
pinCryptoService,
|
||||
biometricStateService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -124,10 +124,7 @@ export class PremiumComponent implements OnInit {
|
|||
await this.apiService.refreshIdentityToken();
|
||||
await this.syncService.fullSync(true);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("premiumUpdated"));
|
||||
this.messagingService.send("purchasedPremium");
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/settings/subscription/user-subscription"]);
|
||||
await this.router.navigate(["/settings/subscription/user-subscription"]);
|
||||
}
|
||||
|
||||
get additionalStorageTotal(): number {
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@Component({
|
||||
templateUrl: "organization-billing-tab.component.html",
|
||||
})
|
||||
export class OrganizationBillingTabComponent implements OnInit {
|
||||
showPaymentAndHistory$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.showPaymentAndHistory$ = this.route.params.pipe(
|
||||
switchMap((params) => this.organizationService.get$(params.organizationId)),
|
||||
map(
|
||||
(org) =>
|
||||
!this.platformUtilsService.isSelfHost() &&
|
||||
org.canViewBillingHistory &&
|
||||
org.canEditPaymentMethods,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscr
|
|||
import { SecretsManagerAdjustSubscriptionComponent } from "./sm-adjust-subscription.component";
|
||||
import { SecretsManagerSubscribeStandaloneComponent } from "./sm-subscribe-standalone.component";
|
||||
import { SubscriptionHiddenComponent } from "./subscription-hidden.component";
|
||||
import { SubscriptionStatusComponent } from "./subscription-status.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -38,6 +39,7 @@ import { SubscriptionHiddenComponent } from "./subscription-hidden.component";
|
|||
SecretsManagerAdjustSubscriptionComponent,
|
||||
SecretsManagerSubscribeStandaloneComponent,
|
||||
SubscriptionHiddenComponent,
|
||||
SubscriptionStatusComponent,
|
||||
],
|
||||
})
|
||||
export class OrganizationBillingModule {}
|
||||
|
|
|
@ -12,45 +12,58 @@
|
|||
></app-org-subscription-hidden>
|
||||
|
||||
<ng-container *ngIf="sub && firstLoaded">
|
||||
<bit-callout
|
||||
type="warning"
|
||||
title="{{ 'canceled' | i18n }}"
|
||||
*ngIf="subscription && subscription.cancelled"
|
||||
>
|
||||
{{ "subscriptionCanceled" | i18n }}</bit-callout
|
||||
>
|
||||
<bit-callout
|
||||
type="warning"
|
||||
title="{{ 'pendingCancellation' | i18n }}"
|
||||
*ngIf="subscriptionMarkedForCancel"
|
||||
>
|
||||
<p>{{ "subscriptionPendingCanceled" | i18n }}</p>
|
||||
<button bitButton buttonType="secondary" [bitAction]="reinstate" type="button">
|
||||
{{ "reinstateSubscription" | i18n }}
|
||||
</button>
|
||||
</bit-callout>
|
||||
<ng-container *ngIf="!(showUpdatedSubscriptionStatusSection$ | async)">
|
||||
<bit-callout
|
||||
type="warning"
|
||||
title="{{ 'canceled' | i18n }}"
|
||||
*ngIf="subscription && subscription.cancelled"
|
||||
>
|
||||
{{ "subscriptionCanceled" | i18n }}</bit-callout
|
||||
>
|
||||
<bit-callout
|
||||
type="warning"
|
||||
title="{{ 'pendingCancellation' | i18n }}"
|
||||
*ngIf="subscriptionMarkedForCancel"
|
||||
>
|
||||
<p>{{ "subscriptionPendingCanceled" | i18n }}</p>
|
||||
<button
|
||||
*ngIf="userOrg.canEditSubscription"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
[bitAction]="reinstate"
|
||||
type="button"
|
||||
>
|
||||
{{ "reinstateSubscription" | i18n }}
|
||||
</button>
|
||||
</bit-callout>
|
||||
|
||||
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
|
||||
<dt>{{ "billingPlan" | i18n }}</dt>
|
||||
<dd>{{ sub.plan.name }}</dd>
|
||||
<ng-container *ngIf="subscription">
|
||||
<dt>{{ "status" | i18n }}</dt>
|
||||
<dd>
|
||||
<span class="tw-capitalize">{{
|
||||
isSponsoredSubscription ? "sponsored" : subscription.status || "-"
|
||||
}}</span>
|
||||
<span bitBadge variant="warning" *ngIf="subscriptionMarkedForCancel">{{
|
||||
"pendingCancellation" | i18n
|
||||
}}</span>
|
||||
</dd>
|
||||
<dt [ngClass]="{ 'tw-text-danger': isExpired }">
|
||||
{{ "subscriptionExpiration" | i18n }}
|
||||
</dt>
|
||||
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
|
||||
{{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }}
|
||||
</dd>
|
||||
</ng-container>
|
||||
</dl>
|
||||
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
|
||||
<dt>{{ "billingPlan" | i18n }}</dt>
|
||||
<dd>{{ sub.plan.name }}</dd>
|
||||
<ng-container *ngIf="subscription">
|
||||
<dt>{{ "status" | i18n }}</dt>
|
||||
<dd>
|
||||
<span class="tw-capitalize">{{
|
||||
isSponsoredSubscription ? "sponsored" : subscription.status || "-"
|
||||
}}</span>
|
||||
<span bitBadge variant="warning" *ngIf="subscriptionMarkedForCancel">{{
|
||||
"pendingCancellation" | i18n
|
||||
}}</span>
|
||||
</dd>
|
||||
<dt [ngClass]="{ 'tw-text-danger': isExpired }">
|
||||
{{ "subscriptionExpiration" | i18n }}
|
||||
</dt>
|
||||
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
|
||||
{{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }}
|
||||
</dd>
|
||||
</ng-container>
|
||||
</dl>
|
||||
</ng-container>
|
||||
<app-subscription-status
|
||||
*ngIf="showUpdatedSubscriptionStatusSection$ | async"
|
||||
[organizationSubscriptionResponse]="sub"
|
||||
(reinstatementRequested)="reinstate()"
|
||||
></app-subscription-status>
|
||||
<ng-container *ngIf="userOrg.canEditSubscription">
|
||||
<div class="tw-flex-col">
|
||||
<strong class="tw-block tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300">{{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { concatMap, firstValueFrom, lastValueFrom, Subject, takeUntil } from "rxjs";
|
||||
import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
|
@ -11,6 +11,8 @@ import { PlanType } from "@bitwarden/common/billing/enums";
|
|||
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||
import { BillingSubscriptionItemResponse } from "@bitwarden/common/billing/models/response/subscription.response";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
@ -41,6 +43,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||
showSecretsManagerSubscribe = false;
|
||||
firstLoaded = false;
|
||||
loading: boolean;
|
||||
locale: string;
|
||||
showUpdatedSubscriptionStatusSection$: Observable<boolean>;
|
||||
|
||||
protected readonly teamsStarter = ProductType.TeamsStarter;
|
||||
|
||||
|
@ -55,6 +59,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private route: ActivatedRoute,
|
||||
private dialogService: DialogService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -74,6 +79,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AC1795_UpdatedSubscriptionStatusSection,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
@ -86,6 +96,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
|||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
this.locale = await firstValueFrom(this.i18nService.locale$);
|
||||
this.userOrg = await this.organizationService.get(this.organizationId);
|
||||
if (this.userOrg.canViewSubscription) {
|
||||
this.sub = await this.organizationApiService.getSubscription(this.organizationId);
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<ng-container>
|
||||
<bit-callout *ngIf="data.callout" [type]="data.callout.severity" [title]="data.callout.header">
|
||||
<p>{{ data.callout.body }}</p>
|
||||
<button
|
||||
*ngIf="data.callout.showReinstatementButton"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
[bitAction]="requestReinstatement"
|
||||
type="button"
|
||||
>
|
||||
{{ "reinstateSubscription" | i18n }}
|
||||
</button>
|
||||
</bit-callout>
|
||||
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
|
||||
<dt>{{ "billingPlan" | i18n }}</dt>
|
||||
<dd>{{ planName }}</dd>
|
||||
<ng-container>
|
||||
<dt>{{ data.status.label }}</dt>
|
||||
<dd>
|
||||
<span class="tw-capitalize">
|
||||
{{ displayedStatus }}
|
||||
</span>
|
||||
</dd>
|
||||
<dt>
|
||||
{{ data.date.label | titlecase }}
|
||||
</dt>
|
||||
<dd>
|
||||
{{ data.date.value | date: "mediumDate" }}
|
||||
</dd>
|
||||
</ng-container>
|
||||
</dl>
|
||||
</ng-container>
|
|
@ -0,0 +1,184 @@
|
|||
import { DatePipe } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
type ComponentData = {
|
||||
status: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
date: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
callout?: {
|
||||
severity: "danger" | "warning";
|
||||
header: string;
|
||||
body: string;
|
||||
showReinstatementButton: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-subscription-status",
|
||||
templateUrl: "subscription-status.component.html",
|
||||
})
|
||||
export class SubscriptionStatusComponent {
|
||||
@Input({ required: true }) organizationSubscriptionResponse: OrganizationSubscriptionResponse;
|
||||
@Output() reinstatementRequested = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private datePipe: DatePipe,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
get displayedStatus(): string {
|
||||
const sponsored = this.subscription.items.some((item) => item.sponsoredSubscriptionItem);
|
||||
return sponsored ? this.i18nService.t("sponsored") : this.data.status.value;
|
||||
}
|
||||
|
||||
get planName() {
|
||||
return this.organizationSubscriptionResponse.plan.name;
|
||||
}
|
||||
|
||||
get status(): string {
|
||||
return this.subscription.status != "canceled" && this.subscription.cancelAtEndDate
|
||||
? "pending_cancellation"
|
||||
: this.subscription.status;
|
||||
}
|
||||
|
||||
get subscription() {
|
||||
return this.organizationSubscriptionResponse.subscription;
|
||||
}
|
||||
|
||||
get data(): ComponentData {
|
||||
const defaultStatusLabel = this.i18nService.t("status");
|
||||
|
||||
const nextChargeDateLabel = this.i18nService.t("nextCharge");
|
||||
const subscriptionExpiredDateLabel = this.i18nService.t("subscriptionExpired");
|
||||
const cancellationDateLabel = this.i18nService.t("cancellationDate");
|
||||
|
||||
switch (this.status) {
|
||||
case "trialing": {
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: this.i18nService.t("trial"),
|
||||
},
|
||||
date: {
|
||||
label: nextChargeDateLabel,
|
||||
value: this.subscription.periodEndDate,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "active": {
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: this.i18nService.t("active"),
|
||||
},
|
||||
date: {
|
||||
label: nextChargeDateLabel,
|
||||
value: this.subscription.periodEndDate,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "past_due": {
|
||||
const pastDueText = this.i18nService.t("pastDue");
|
||||
const suspensionDate = this.datePipe.transform(
|
||||
this.subscription.suspensionDate,
|
||||
"mediumDate",
|
||||
);
|
||||
const calloutBody =
|
||||
this.subscription.collectionMethod === "charge_automatically"
|
||||
? this.i18nService.t(
|
||||
"pastDueWarningForChargeAutomatically",
|
||||
this.subscription.gracePeriod,
|
||||
suspensionDate,
|
||||
)
|
||||
: this.i18nService.t(
|
||||
"pastDueWarningForSendInvoice",
|
||||
this.subscription.gracePeriod,
|
||||
suspensionDate,
|
||||
);
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: pastDueText,
|
||||
},
|
||||
date: {
|
||||
label: subscriptionExpiredDateLabel,
|
||||
value: this.subscription.unpaidPeriodEndDate,
|
||||
},
|
||||
callout: {
|
||||
severity: "warning",
|
||||
header: pastDueText,
|
||||
body: calloutBody,
|
||||
showReinstatementButton: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "unpaid": {
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: this.i18nService.t("unpaid"),
|
||||
},
|
||||
date: {
|
||||
label: subscriptionExpiredDateLabel,
|
||||
value: this.subscription.unpaidPeriodEndDate,
|
||||
},
|
||||
callout: {
|
||||
severity: "danger",
|
||||
header: this.i18nService.t("unpaidInvoice"),
|
||||
body: this.i18nService.t("toReactivateYourSubscription"),
|
||||
showReinstatementButton: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "pending_cancellation": {
|
||||
const pendingCancellationText = this.i18nService.t("pendingCancellation");
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: pendingCancellationText,
|
||||
},
|
||||
date: {
|
||||
label: cancellationDateLabel,
|
||||
value: this.subscription.periodEndDate,
|
||||
},
|
||||
callout: {
|
||||
severity: "warning",
|
||||
header: pendingCancellationText,
|
||||
body: this.i18nService.t("subscriptionPendingCanceled"),
|
||||
showReinstatementButton: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "incomplete_expired":
|
||||
case "canceled": {
|
||||
const canceledText = this.i18nService.t("canceled");
|
||||
return {
|
||||
status: {
|
||||
label: defaultStatusLabel,
|
||||
value: canceledText,
|
||||
},
|
||||
date: {
|
||||
label: cancellationDateLabel,
|
||||
value: this.subscription.periodEndDate,
|
||||
},
|
||||
callout: {
|
||||
severity: "danger",
|
||||
header: canceledText,
|
||||
body: this.i18nService.t("subscriptionCanceled"),
|
||||
showReinstatementButton: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requestReinstatement = () => this.reinstatementRequested.emit();
|
||||
}
|
|
@ -1,14 +1,31 @@
|
|||
import { Injectable } from "@angular/core";
|
||||
import { Title } from "@angular/platform-browser";
|
||||
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
|
||||
import { filter } from "rxjs";
|
||||
import { filter, firstValueFrom } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
KeyDefinition,
|
||||
ROUTER_DISK,
|
||||
StateProvider,
|
||||
GlobalState,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
|
||||
const DEEP_LINK_REDIRECT_URL = new KeyDefinition(ROUTER_DISK, "deepLinkRedirectUrl", {
|
||||
deserializer: (value: string) => value,
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class RouterService {
|
||||
/**
|
||||
* The string value of the URL the user tried to navigate to while unauthenticated.
|
||||
*
|
||||
* Developed to allow users to deep link even when the navigation gets interrupted
|
||||
* by the authentication process.
|
||||
*/
|
||||
private deepLinkRedirectUrlState: GlobalState<string>;
|
||||
|
||||
private previousUrl: string = undefined;
|
||||
private currentUrl: string = undefined;
|
||||
|
||||
|
@ -16,9 +33,11 @@ export class RouterService {
|
|||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private titleService: Title,
|
||||
private stateService: StateService,
|
||||
private stateProvider: StateProvider,
|
||||
i18nService: I18nService,
|
||||
) {
|
||||
this.deepLinkRedirectUrlState = this.stateProvider.getGlobal(DEEP_LINK_REDIRECT_URL);
|
||||
|
||||
this.currentUrl = this.router.url;
|
||||
|
||||
router.events
|
||||
|
@ -67,14 +86,14 @@ export class RouterService {
|
|||
* @param url URL being saved to the Global State
|
||||
*/
|
||||
async persistLoginRedirectUrl(url: string): Promise<void> {
|
||||
await this.stateService.setDeepLinkRedirectUrl(url);
|
||||
await this.deepLinkRedirectUrlState.update(() => url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and clear persisted LoginRedirectUrl if present in state
|
||||
*/
|
||||
async getAndClearLoginRedirectUrl(): Promise<string> | undefined {
|
||||
const persistedPreLoginUrl = await this.stateService.getDeepLinkRedirectUrl();
|
||||
const persistedPreLoginUrl = await firstValueFrom(this.deepLinkRedirectUrlState.state$);
|
||||
|
||||
if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) {
|
||||
await this.persistLoginRedirectUrl(null);
|
||||
|
|
|
@ -18,7 +18,6 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
|
|||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
||||
import { SendData } from "@bitwarden/common/tools/send/models/data/send.data";
|
||||
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
|
||||
|
||||
import { Account } from "./account";
|
||||
|
@ -71,19 +70,6 @@ export class StateService extends BaseStateService<GlobalState, Account> {
|
|||
return await super.setEncryptedCiphers(value, options);
|
||||
}
|
||||
|
||||
async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData }> {
|
||||
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||
return await super.getEncryptedSends(options);
|
||||
}
|
||||
|
||||
async setEncryptedSends(
|
||||
value: { [id: string]: SendData },
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||
return await super.setEncryptedSends(value, options);
|
||||
}
|
||||
|
||||
override async getLastSync(options?: StorageOptions): Promise<string> {
|
||||
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||
return await super.getLastSync(options);
|
||||
|
|
|
@ -46,7 +46,6 @@ export class OrgSwitcherComponent {
|
|||
|
||||
/**
|
||||
* Visibility of the New Organization button
|
||||
* (Temporary; will be removed when ability to create organizations is added to SM.)
|
||||
*/
|
||||
@Input()
|
||||
hideNewButton = false;
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-toggle-width",
|
||||
template: `<bit-nav-item
|
||||
text="Toggle Width"
|
||||
icon="bwi-bug"
|
||||
*ngIf="isDev"
|
||||
class="tw-absolute tw-bottom-0 tw-w-full"
|
||||
(click)="toggleWidth()"
|
||||
></bit-nav-item>`,
|
||||
standalone: true,
|
||||
imports: [CommonModule, NavigationModule],
|
||||
})
|
||||
export class ToggleWidthComponent {
|
||||
protected isDev: boolean;
|
||||
|
||||
constructor(platformUtilsService: PlatformUtilsService) {
|
||||
this.isDev = platformUtilsService.isDev();
|
||||
}
|
||||
|
||||
protected toggleWidth() {
|
||||
if (document.body.style.minWidth === "unset") {
|
||||
document.body.style.minWidth = "";
|
||||
} else {
|
||||
document.body.style.minWidth = "unset";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@
|
|||
<bit-nav-item
|
||||
[text]="'subscription' | i18n"
|
||||
route="settings/subscription"
|
||||
*ngIf="!hideSubscription"
|
||||
*ngIf="showSubscription$ | async"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item [text]="'domainRules' | i18n" route="settings/domain-rules"></bit-nav-item>
|
||||
<bit-nav-item
|
||||
|
@ -29,9 +29,11 @@
|
|||
<bit-nav-item
|
||||
[text]="'sponsoredFamilies' | i18n"
|
||||
route="settings/sponsored-families"
|
||||
*ngIf="hasFamilySponsorshipAvailable"
|
||||
*ngIf="hasFamilySponsorshipAvailable$ | async"
|
||||
></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
|
||||
<app-toggle-width></app-toggle-width>
|
||||
</nav>
|
||||
<app-payment-method-warnings
|
||||
*ngIf="showPaymentMethodWarningBanners$ | async"
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { Observable, combineLatest, concatMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
@ -17,8 +16,7 @@ import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/compon
|
|||
import { PaymentMethodWarningsModule } from "../billing/shared";
|
||||
|
||||
import { PasswordManagerLogo } from "./password-manager-logo";
|
||||
|
||||
const BroadcasterSubscriptionId = "UserLayoutComponent";
|
||||
import { ToggleWidthComponent } from "./toggle-width.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-user-layout",
|
||||
|
@ -32,12 +30,13 @@ const BroadcasterSubscriptionId = "UserLayoutComponent";
|
|||
IconModule,
|
||||
NavigationModule,
|
||||
PaymentMethodWarningsModule,
|
||||
ToggleWidthComponent,
|
||||
],
|
||||
})
|
||||
export class UserLayoutComponent implements OnInit, OnDestroy {
|
||||
export class UserLayoutComponent implements OnInit {
|
||||
protected readonly logo = PasswordManagerLogo;
|
||||
hasFamilySponsorshipAvailable: boolean;
|
||||
hideSubscription: boolean;
|
||||
protected hasFamilySponsorshipAvailable$: Observable<boolean>;
|
||||
protected showSubscription$: Observable<boolean>;
|
||||
|
||||
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||
|
@ -45,8 +44,6 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
|
||||
constructor(
|
||||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private organizationService: OrganizationService,
|
||||
private apiService: ApiService,
|
||||
|
@ -58,43 +55,28 @@ export class UserLayoutComponent implements OnInit, OnDestroy {
|
|||
async ngOnInit() {
|
||||
document.body.classList.remove("layout_frontend");
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "purchasedPremium":
|
||||
await this.load();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await this.syncService.fullSync(false);
|
||||
await this.load();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
this.hasFamilySponsorshipAvailable$ = this.organizationService.canManageSponsorships$;
|
||||
|
||||
async load() {
|
||||
const hasPremiumPersonally = await firstValueFrom(
|
||||
// We want to hide the subscription menu for organizations that provide premium.
|
||||
// Except if the user has premium personally or has a billing history.
|
||||
this.showSubscription$ = combineLatest([
|
||||
this.billingAccountProfileStateService.hasPremiumPersonally$,
|
||||
);
|
||||
const hasPremiumFromOrg = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$,
|
||||
);
|
||||
const selfHosted = this.platformUtilsService.isSelfHost();
|
||||
]).pipe(
|
||||
concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => {
|
||||
const isCloud = !this.platformUtilsService.isSelfHost();
|
||||
|
||||
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
|
||||
let billing = null;
|
||||
if (!selfHosted) {
|
||||
// TODO: We should remove the need to call this!
|
||||
billing = await this.apiService.getUserBillingHistory();
|
||||
}
|
||||
this.hideSubscription =
|
||||
!hasPremiumPersonally && hasPremiumFromOrg && (selfHosted || billing?.hasNoHistory);
|
||||
let billing = null;
|
||||
if (isCloud) {
|
||||
// TODO: We should remove the need to call this!
|
||||
billing = await this.apiService.getUserBillingHistory();
|
||||
}
|
||||
|
||||
const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory;
|
||||
return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
const BroadcasterSubscriptionId = "SettingsComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-settings",
|
||||
templateUrl: "settings.component.html",
|
||||
})
|
||||
export class SettingsComponent implements OnInit, OnDestroy {
|
||||
premium: boolean;
|
||||
selfHosted: boolean;
|
||||
hasFamilySponsorshipAvailable: boolean;
|
||||
hideSubscription: boolean;
|
||||
|
||||
constructor(
|
||||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private organizationService: OrganizationService,
|
||||
private apiService: ApiService,
|
||||
private billingAccountProfileStateServiceAbstraction: BillingAccountProfileStateService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "purchasedPremium":
|
||||
await this.load();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.selfHosted = await this.platformUtilsService.isSelfHost();
|
||||
await this.load();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.premium = await firstValueFrom(
|
||||
this.billingAccountProfileStateServiceAbstraction.hasPremiumPersonally$,
|
||||
);
|
||||
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
|
||||
const hasPremiumFromOrg = await firstValueFrom(
|
||||
this.billingAccountProfileStateServiceAbstraction.hasPremiumFromAnyOrganization$,
|
||||
);
|
||||
let billing = null;
|
||||
if (!this.selfHosted) {
|
||||
billing = await this.apiService.getUserBillingHistory();
|
||||
}
|
||||
this.hideSubscription =
|
||||
!this.premium && hasPremiumFromOrg && (this.selfHosted || billing?.hasNoHistory);
|
||||
}
|
||||
}
|
|
@ -45,6 +45,7 @@ export class VaultItemsComponent {
|
|||
@Input() showBulkAddToCollections = false;
|
||||
@Input() showPermissionsColumn = false;
|
||||
@Input() viewingOrgVault: boolean;
|
||||
@Input({ required: true }) flexibleCollectionsV1Enabled = false;
|
||||
|
||||
private _ciphers?: CipherView[] = [];
|
||||
@Input() get ciphers(): CipherView[] {
|
||||
|
@ -101,7 +102,7 @@ export class VaultItemsComponent {
|
|||
}
|
||||
|
||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||
return collection.canEdit(organization);
|
||||
return collection.canEdit(organization, this.flexibleCollectionsV1Enabled);
|
||||
}
|
||||
|
||||
protected canDeleteCollection(collection: CollectionView): boolean {
|
||||
|
|
|
@ -31,10 +31,11 @@ export class CollectionAdminView extends CollectionView {
|
|||
this.assigned = response.assigned;
|
||||
}
|
||||
|
||||
override canEdit(org: Organization): boolean {
|
||||
override canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||
return org?.flexibleCollections
|
||||
? org?.canEditAnyCollection || this.manage
|
||||
: org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
|
||||
? org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || this.manage
|
||||
: org?.canEditAnyCollection(flexibleCollectionsV1Enabled) ||
|
||||
(org?.canEditAssignedCollections && this.assigned);
|
||||
}
|
||||
|
||||
override canDelete(org: Organization): boolean {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
@ -49,6 +52,11 @@ export class BulkDeleteDialogComponent {
|
|||
organizations: Organization[];
|
||||
collections: CollectionView[];
|
||||
|
||||
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
false,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) params: BulkDeleteDialogParams,
|
||||
private dialogRef: DialogRef<BulkDeleteDialogResult>,
|
||||
|
@ -57,6 +65,7 @@ export class BulkDeleteDialogComponent {
|
|||
private i18nService: I18nService,
|
||||
private apiService: ApiService,
|
||||
private collectionService: CollectionService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.cipherIds = params.cipherIds ?? [];
|
||||
this.permanent = params.permanent;
|
||||
|
@ -72,7 +81,12 @@ export class BulkDeleteDialogComponent {
|
|||
protected submit = async () => {
|
||||
const deletePromises: Promise<void>[] = [];
|
||||
if (this.cipherIds.length) {
|
||||
if (!this.organization || !this.organization.canEditAnyCollection) {
|
||||
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
||||
|
||||
if (
|
||||
!this.organization ||
|
||||
!this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled)
|
||||
) {
|
||||
deletePromises.push(this.deleteCiphers());
|
||||
} else {
|
||||
deletePromises.push(this.deleteCiphersAdmin());
|
||||
|
@ -104,7 +118,8 @@ export class BulkDeleteDialogComponent {
|
|||
};
|
||||
|
||||
private async deleteCiphers(): Promise<any> {
|
||||
const asAdmin = this.organization?.canEditAnyCollection;
|
||||
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
||||
const asAdmin = this.organization?.canEditAllCiphers(flexibleCollectionsV1Enabled);
|
||||
if (this.permanent) {
|
||||
await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin);
|
||||
} else {
|
||||
|
|
|
@ -41,7 +41,7 @@ export class BulkMoveDialogComponent implements OnInit {
|
|||
cipherIds: string[] = [];
|
||||
|
||||
formGroup = this.formBuilder.group({
|
||||
folderId: ["", [Validators.required]],
|
||||
folderId: ["", [Validators.nullValidator]],
|
||||
});
|
||||
folders$: Observable<FolderView[]>;
|
||||
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
} from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||
|
@ -17,7 +27,7 @@ import {
|
|||
templateUrl: "./vault-header.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class VaultHeaderComponent {
|
||||
export class VaultHeaderComponent implements OnInit {
|
||||
protected Unassigned = Unassigned;
|
||||
protected All = All;
|
||||
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||
|
@ -55,7 +65,18 @@ export class VaultHeaderComponent {
|
|||
/** Emits an event when the delete collection button is clicked in the header */
|
||||
@Output() onDeleteCollection = new EventEmitter<void>();
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
private flexibleCollectionsV1Enabled = false;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.flexibleCollectionsV1Enabled = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of the organization that is currently being filtered on.
|
||||
|
@ -137,7 +158,7 @@ export class VaultHeaderComponent {
|
|||
const organization = this.organizations.find(
|
||||
(o) => o.id === this.collection?.node.organizationId,
|
||||
);
|
||||
return this.collection.node.canEdit(organization);
|
||||
return this.collection.node.canEdit(organization, this.flexibleCollectionsV1Enabled);
|
||||
}
|
||||
|
||||
async editCollection(tab: CollectionDialogTabType): Promise<void> {
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
[cloneableOrganizationCiphers]="false"
|
||||
[showAdminActions]="false"
|
||||
(onEvent)="onVaultItemsEvent($event)"
|
||||
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async"
|
||||
>
|
||||
</app-vault-items>
|
||||
<div
|
||||
|
|
|
@ -39,6 +39,7 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
|
|||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
@ -144,6 +145,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
protected selectedCollection: TreeNode<CollectionView> | undefined;
|
||||
protected canCreateCollections = false;
|
||||
protected currentSearchText$: Observable<string>;
|
||||
protected flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
false,
|
||||
);
|
||||
|
||||
private searchText$ = new Subject<string>();
|
||||
private refresh$ = new BehaviorSubject<void>(null);
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { Component } from "@angular/core";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
@ -21,10 +24,12 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "../individual-
|
|||
selector: "app-org-vault-attachments",
|
||||
templateUrl: "../individual-vault/attachments.component.html",
|
||||
})
|
||||
export class AttachmentsComponent extends BaseAttachmentsComponent {
|
||||
export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit {
|
||||
viewOnly = false;
|
||||
organization: Organization;
|
||||
|
||||
private flexibleCollectionsV1Enabled = false;
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
i18nService: I18nService,
|
||||
|
@ -36,6 +41,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
|
|||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
|
@ -51,14 +57,24 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
|
|||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
this.flexibleCollectionsV1Enabled = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false),
|
||||
);
|
||||
}
|
||||
|
||||
protected async reupload(attachment: AttachmentView) {
|
||||
if (this.organization.canEditAnyCollection && this.showFixOldAttachments(attachment)) {
|
||||
if (
|
||||
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
|
||||
this.showFixOldAttachments(attachment)
|
||||
) {
|
||||
await super.reuploadCipherAttachment(attachment, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
if (!this.organization.canEditAnyCollection) {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
return await super.loadCipher();
|
||||
}
|
||||
const response = await this.apiService.getCipherAdmin(this.cipherId);
|
||||
|
@ -69,18 +85,21 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
|
|||
return this.cipherService.saveAttachmentWithServer(
|
||||
this.cipherDomain,
|
||||
file,
|
||||
this.organization.canEditAnyCollection,
|
||||
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled),
|
||||
);
|
||||
}
|
||||
|
||||
protected deleteCipherAttachment(attachmentId: string) {
|
||||
if (!this.organization.canEditAnyCollection) {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
return super.deleteCipherAttachment(attachmentId);
|
||||
}
|
||||
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(attachment: AttachmentView) {
|
||||
return attachment.key == null && this.organization.canEditAnyCollection;
|
||||
return (
|
||||
attachment.key == null &&
|
||||
this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
||||
|
@ -22,7 +24,7 @@ import {
|
|||
selector: "app-org-vault-header",
|
||||
templateUrl: "./vault-header.component.html",
|
||||
})
|
||||
export class VaultHeaderComponent {
|
||||
export class VaultHeaderComponent implements OnInit {
|
||||
protected All = All;
|
||||
protected Unassigned = Unassigned;
|
||||
|
||||
|
@ -56,14 +58,23 @@ export class VaultHeaderComponent {
|
|||
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||
protected organizations$ = this.organizationService.organizations$;
|
||||
|
||||
private flexibleCollectionsV1Enabled = false;
|
||||
|
||||
constructor(
|
||||
private organizationService: OrganizationService,
|
||||
private i18nService: I18nService,
|
||||
private dialogService: DialogService,
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.flexibleCollectionsV1Enabled = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
|
||||
);
|
||||
}
|
||||
|
||||
get title() {
|
||||
const headerType = this.organization?.flexibleCollections
|
||||
? this.i18nService.t("collections").toLowerCase()
|
||||
|
@ -153,7 +164,7 @@ export class VaultHeaderComponent {
|
|||
}
|
||||
|
||||
// Otherwise, check if we can edit the specified collection
|
||||
return this.collection.node.canEdit(this.organization);
|
||||
return this.collection.node.canEdit(this.organization, this.flexibleCollectionsV1Enabled);
|
||||
}
|
||||
|
||||
addCipher() {
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
[showBulkEditCollectionAccess]="organization?.flexibleCollections"
|
||||
[showBulkAddToCollections]="organization?.flexibleCollections"
|
||||
[viewingOrgVault]="true"
|
||||
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled"
|
||||
>
|
||||
</app-vault-items>
|
||||
<ng-container *ngIf="!flexibleCollectionsV1Enabled">
|
||||
|
@ -98,7 +99,10 @@
|
|||
</bit-no-items>
|
||||
<collection-access-restricted
|
||||
*ngIf="showCollectionAccessRestricted"
|
||||
[canEdit]="selectedCollection != null && selectedCollection.node.canEdit(organization)"
|
||||
[canEdit]="
|
||||
selectedCollection != null &&
|
||||
selectedCollection.node.canEdit(organization, flexibleCollectionsV1Enabled)
|
||||
"
|
||||
(editInfoClicked)="editCollection(selectedCollection.node, CollectionDialogTabType.Info)"
|
||||
>
|
||||
</collection-access-restricted>
|
||||
|
|
|
@ -213,7 +213,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
switchMap(async ([organization]) => {
|
||||
this.organization = organization;
|
||||
|
||||
if (!organization.canUseAdminCollections) {
|
||||
if (!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) {
|
||||
await this.syncService.fullSync(false);
|
||||
}
|
||||
|
||||
|
@ -322,7 +322,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
} else {
|
||||
// Pre-flexible collections logic, to be removed after flexible collections is fully released
|
||||
if (organization.canEditAnyCollection) {
|
||||
if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
|
||||
} else {
|
||||
ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
||||
|
@ -407,7 +407,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
]).pipe(
|
||||
map(([filter, collection, organization]) => {
|
||||
return (
|
||||
(filter.collectionId === Unassigned && !organization.canUseAdminCollections) ||
|
||||
(filter.collectionId === Unassigned &&
|
||||
!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) ||
|
||||
(!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
|
||||
collection != undefined &&
|
||||
!collection.node.assigned)
|
||||
|
@ -453,11 +454,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
map(([filter, collection, organization]) => {
|
||||
return (
|
||||
// Filtering by unassigned, show message if not admin
|
||||
(filter.collectionId === Unassigned && !organization.canUseAdminCollections) ||
|
||||
(filter.collectionId === Unassigned &&
|
||||
!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled)) ||
|
||||
// Filtering by a collection, so show message if user is not assigned
|
||||
(collection != undefined &&
|
||||
!collection.node.assigned &&
|
||||
!organization.canUseAdminCollections)
|
||||
!organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled))
|
||||
);
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
|
@ -480,7 +482,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
(await firstValueFrom(allCipherMap$))[cipherId] != undefined;
|
||||
} else {
|
||||
canEditCipher =
|
||||
organization.canUseAdminCollections ||
|
||||
organization.canUseAdminCollections(this.flexibleCollectionsV1Enabled) ||
|
||||
(await this.cipherService.get(cipherId)) != null;
|
||||
}
|
||||
|
||||
|
@ -856,7 +858,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
try {
|
||||
const asAdmin = this.organization?.canEditAnyCollection;
|
||||
const asAdmin = this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled);
|
||||
await this.cipherService.restoreWithServer(c.id, asAdmin);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
|
||||
this.refresh();
|
||||
|
@ -1143,7 +1145,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
protected deleteCipherWithServer(id: string, permanent: boolean) {
|
||||
const asAdmin = this.organization?.canEditAnyCollection;
|
||||
const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled);
|
||||
return permanent
|
||||
? this.cipherService.deleteWithServer(id, asAdmin)
|
||||
: this.cipherService.softDeleteWithServer(id, asAdmin);
|
||||
|
|
|
@ -4956,6 +4956,9 @@
|
|||
"addExistingOrganization": {
|
||||
"message": "Add existing organization"
|
||||
},
|
||||
"addNewOrganization": {
|
||||
"message": "Add new organization"
|
||||
},
|
||||
"myProvider": {
|
||||
"message": "My Provider"
|
||||
},
|
||||
|
@ -7642,5 +7645,90 @@
|
|||
},
|
||||
"items": {
|
||||
"message": "Items"
|
||||
},
|
||||
"assignedSeats": {
|
||||
"message": "Assigned seats"
|
||||
},
|
||||
"assigned": {
|
||||
"message": "Assigned"
|
||||
},
|
||||
"used": {
|
||||
"message": "Used"
|
||||
},
|
||||
"remaining": {
|
||||
"message": "Remaining"
|
||||
},
|
||||
"unlinkOrganization": {
|
||||
"message": "Unlink organization"
|
||||
},
|
||||
"manageSeats": {
|
||||
"message": "MANAGE SEATS"
|
||||
},
|
||||
"manageSeatsDescription": {
|
||||
"message": "Adjustments to seats will be reflected in the next billing cycle."
|
||||
},
|
||||
"unassignedSeatsDescription": {
|
||||
"message": "Unassigned subscription seats"
|
||||
},
|
||||
"purchaseSeatDescription": {
|
||||
"message": "Additional seats purchased"
|
||||
},
|
||||
"assignedSeatCannotUpdate": {
|
||||
"message": "Assigned Seats can not be updated. Please contact your organization owner for assistance."
|
||||
},
|
||||
"subscriptionUpdateFailed": {
|
||||
"message": "Subscription update failed"
|
||||
},
|
||||
"trial": {
|
||||
"message": "Trial",
|
||||
"description": "A subscription status label."
|
||||
},
|
||||
"pastDue": {
|
||||
"message": "Past due",
|
||||
"description": "A subscription status label"
|
||||
},
|
||||
"subscriptionExpired": {
|
||||
"message": "Subscription expired",
|
||||
"description": "The date header used when a subscription is past due."
|
||||
},
|
||||
"pastDueWarningForChargeAutomatically": {
|
||||
"message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.",
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"content": "$1",
|
||||
"example": "11"
|
||||
},
|
||||
"suspension_date": {
|
||||
"content": "$2",
|
||||
"example": "01/10/2024"
|
||||
}
|
||||
},
|
||||
"description": "A warning shown to the user when their subscription is past due and they are charged automatically."
|
||||
},
|
||||
"pastDueWarningForSendInvoice": {
|
||||
"message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.",
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"content": "$1",
|
||||
"example": "11"
|
||||
},
|
||||
"suspension_date": {
|
||||
"content": "$2",
|
||||
"example": "01/10/2024"
|
||||
}
|
||||
},
|
||||
"description": "A warning shown to the user when their subscription is past due and they pay via invoice."
|
||||
},
|
||||
"unpaidInvoice": {
|
||||
"message": "Unpaid invoice",
|
||||
"description": "The header of a warning box shown to a user whose subscription is unpaid."
|
||||
},
|
||||
"toReactivateYourSubscription": {
|
||||
"message": "To reactivate your subscription, please resolve the past due invoices.",
|
||||
"description": "The body of a warning box shown to a user whose subscription is unpaid."
|
||||
},
|
||||
"cancellationDate": {
|
||||
"message": "Cancellation date",
|
||||
"description": "The date header used when a subscription is cancelled."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
|
@ -13,6 +13,8 @@ import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
|||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
@ -50,8 +52,14 @@ export class ClientsComponent implements OnInit {
|
|||
protected actionPromise: Promise<unknown>;
|
||||
private pagedClientsCount = 0;
|
||||
|
||||
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.EnableConsolidatedBilling,
|
||||
false,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private providerService: ProviderService,
|
||||
private apiService: ApiService,
|
||||
private searchService: SearchService,
|
||||
|
@ -64,20 +72,29 @@ export class ClientsComponent implements OnInit {
|
|||
private organizationService: OrganizationService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.providerId = params.providerId;
|
||||
|
||||
await this.load();
|
||||
const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$);
|
||||
|
||||
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
this.searchText = qParams.search;
|
||||
if (enableConsolidatedBilling) {
|
||||
await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route });
|
||||
} else {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.providerId = params.providerId;
|
||||
|
||||
await this.load();
|
||||
|
||||
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
this.searchText = qParams.search;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
<bit-icon [icon]="logo"></bit-icon>
|
||||
</a>
|
||||
|
||||
<bit-nav-item icon="bwi-bank" [text]="'clients' | i18n" route="clients"></bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-bank"
|
||||
[text]="'clients' | i18n"
|
||||
[route]="(enableConsolidatedBilling$ | async) ? 'manage-client-organizations' : 'clients'"
|
||||
></bit-nav-item>
|
||||
<bit-nav-group icon="bwi-sliders" [text]="'manage' | i18n" route="manage" *ngIf="showManageTab">
|
||||
<bit-nav-item
|
||||
[text]="'people' | i18n"
|
||||
|
@ -23,6 +27,8 @@
|
|||
route="settings"
|
||||
*ngIf="showSettingsTab"
|
||||
></bit-nav-item>
|
||||
|
||||
<app-toggle-width></app-toggle-width>
|
||||
</nav>
|
||||
<app-payment-method-warnings
|
||||
*ngIf="showPaymentMethodWarningBanners$ | async"
|
||||
|
|
|
@ -10,6 +10,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
|||
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||
import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo";
|
||||
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
||||
import { ToggleWidthComponent } from "@bitwarden/web-vault/app/layouts/toggle-width.component";
|
||||
|
||||
@Component({
|
||||
selector: "providers-layout",
|
||||
|
@ -23,6 +24,7 @@ import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/sh
|
|||
IconModule,
|
||||
NavigationModule,
|
||||
PaymentMethodWarningsModule,
|
||||
ToggleWidthComponent,
|
||||
],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
|
@ -37,6 +39,11 @@ export class ProvidersLayoutComponent {
|
|||
false,
|
||||
);
|
||||
|
||||
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.EnableConsolidatedBilling,
|
||||
false,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private providerService: ProviderService,
|
||||
|
|
|
@ -7,6 +7,8 @@ import { ProvidersComponent } from "@bitwarden/web-vault/app/admin-console/provi
|
|||
import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component";
|
||||
import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component";
|
||||
|
||||
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";
|
||||
|
||||
import { ClientsComponent } from "./clients/clients.component";
|
||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||
import { ProviderPermissionsGuard } from "./guards/provider-permissions.guard";
|
||||
|
@ -64,6 +66,11 @@ const routes: Routes = [
|
|||
{ path: "", pathMatch: "full", redirectTo: "clients" },
|
||||
{ path: "clients/create", component: CreateOrganizationComponent },
|
||||
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
|
||||
{
|
||||
path: "manage-client-organizations",
|
||||
component: ManageClientOrganizationsComponent,
|
||||
data: { titleId: "manage-client-organizations" },
|
||||
},
|
||||
{
|
||||
path: "manage",
|
||||
children: [
|
||||
|
|
|
@ -8,6 +8,9 @@ import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing";
|
|||
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
||||
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
||||
|
||||
import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component";
|
||||
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";
|
||||
|
||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||
import { ClientsComponent } from "./clients/clients.component";
|
||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||
|
@ -50,6 +53,8 @@ import { SetupComponent } from "./setup/setup.component";
|
|||
SetupComponent,
|
||||
SetupProviderComponent,
|
||||
UserAddEditComponent,
|
||||
ManageClientOrganizationsComponent,
|
||||
ManageClientOrganizationSubscriptionComponent,
|
||||
],
|
||||
providers: [WebProviderService, ProviderPermissionsGuard],
|
||||
})
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,49 @@
|
|||
<bit-dialog dialogSize="large" [loading]="loading">
|
||||
<span bitDialogTitle>
|
||||
{{ "manageSeats" | i18n }}
|
||||
<small class="tw-text-muted" *ngIf="clientName">{{ clientName }}</small>
|
||||
</span>
|
||||
<div bitDialogContent>
|
||||
<p>
|
||||
{{ "manageSeatsDescription" | i18n }}
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label>
|
||||
{{ "assignedSeats" | i18n }}
|
||||
</bit-label>
|
||||
<input
|
||||
id="assignedSeats"
|
||||
type="number"
|
||||
appAutoFocus
|
||||
bitInput
|
||||
required
|
||||
[(ngModel)]="assignedSeats"
|
||||
/>
|
||||
</bit-form-field>
|
||||
<ng-container *ngIf="remainingOpenSeats > 0">
|
||||
<p>
|
||||
<small class="tw-text-muted">{{ unassignedSeats }}</small>
|
||||
<small class="tw-text-muted">{{ "unassignedSeatsDescription" | i18n }}</small>
|
||||
</p>
|
||||
<p>
|
||||
<small class="tw-text-muted">{{ AdditionalSeatPurchased }}</small>
|
||||
<small class="tw-text-muted">{{ "purchaseSeatDescription" | i18n }}</small>
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
type="submit"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
bitFormButton
|
||||
(click)="updateSubscription(assignedSeats)"
|
||||
>
|
||||
<i class="bwi bwi-refresh bwi-fw" aria-hidden="true"></i>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
|
@ -0,0 +1,115 @@
|
|||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||
import { ProviderSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/provider-subscription-update.request";
|
||||
import { Plans } from "@bitwarden/common/billing/models/response/provider-subscription-response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
type ManageClientOrganizationDialogParams = {
|
||||
organization: ProviderOrganizationOrganizationDetailsResponse;
|
||||
};
|
||||
|
||||
@Component({
|
||||
templateUrl: "manage-client-organization-subscription.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class ManageClientOrganizationSubscriptionComponent implements OnInit {
|
||||
loading = true;
|
||||
providerOrganizationId: string;
|
||||
providerId: string;
|
||||
|
||||
clientName: string;
|
||||
assignedSeats: number;
|
||||
unassignedSeats: number;
|
||||
planName: string;
|
||||
AdditionalSeatPurchased: number;
|
||||
remainingOpenSeats: number;
|
||||
|
||||
constructor(
|
||||
public dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) protected data: ManageClientOrganizationDialogParams,
|
||||
private billingApiService: BillingApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {
|
||||
this.providerOrganizationId = data.organization.id;
|
||||
this.providerId = data.organization.providerId;
|
||||
this.clientName = data.organization.organizationName;
|
||||
this.assignedSeats = data.organization.seats;
|
||||
this.planName = data.organization.plan;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const response = await this.billingApiService.getProviderClientSubscriptions(this.providerId);
|
||||
this.AdditionalSeatPurchased = this.getPurchasedSeatsByPlan(this.planName, response.plans);
|
||||
const seatMinimum = this.getProviderSeatMinimumByPlan(this.planName, response.plans);
|
||||
const assignedByPlan = this.getAssignedByPlan(this.planName, response.plans);
|
||||
this.remainingOpenSeats = seatMinimum - assignedByPlan;
|
||||
this.unassignedSeats = Math.abs(this.remainingOpenSeats);
|
||||
} catch (error) {
|
||||
this.remainingOpenSeats = 0;
|
||||
this.AdditionalSeatPurchased = 0;
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async updateSubscription(assignedSeats: number) {
|
||||
this.loading = true;
|
||||
if (!assignedSeats) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("assignedSeatCannotUpdate"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new ProviderSubscriptionUpdateRequest();
|
||||
request.assignedSeats = assignedSeats;
|
||||
|
||||
await this.billingApiService.putProviderClientSubscriptions(
|
||||
this.providerId,
|
||||
this.providerOrganizationId,
|
||||
request,
|
||||
);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated"));
|
||||
this.loading = false;
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
getPurchasedSeatsByPlan(planName: string, plans: Plans[]): number {
|
||||
const plan = plans.find((plan) => plan.planName === planName);
|
||||
if (plan) {
|
||||
return plan.purchasedSeats;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
getAssignedByPlan(planName: string, plans: Plans[]): number {
|
||||
const plan = plans.find((plan) => plan.planName === planName);
|
||||
if (plan) {
|
||||
return plan.assignedSeats;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
getProviderSeatMinimumByPlan(planName: string, plans: Plans[]) {
|
||||
const plan = plans.find((plan) => plan.planName === planName);
|
||||
if (plan) {
|
||||
return plan.seatMinimum;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, data: ManageClientOrganizationDialogParams) {
|
||||
return dialogService.open(ManageClientOrganizationSubscriptionComponent, { data });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
<app-header>
|
||||
<bit-search [placeholder]="'search' | i18n" [(ngModel)]="searchText"></bit-search>
|
||||
<a bitButton routerLink="create" *ngIf="manageOrganizations" buttonType="primary">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "addNewOrganization" | i18n }}
|
||||
</a>
|
||||
</app-header>
|
||||
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container
|
||||
*ngIf="!loading && (clients | search: searchText : 'organizationName' : 'id') as searchedClients"
|
||||
>
|
||||
<p *ngIf="!searchedClients.length">{{ "noClientsInList" | i18n }}</p>
|
||||
<ng-container *ngIf="searchedClients.length">
|
||||
<bit-table
|
||||
*ngIf="searchedClients?.length >= 1"
|
||||
[dataSource]="dataSource"
|
||||
class="table table-hover table-list"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="1"
|
||||
[infiniteScrollDisabled]="!isPaging()"
|
||||
(scrolled)="loadMore()"
|
||||
>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th colspan="2" bitCell bitSortable="organizationName" default>{{ "client" | i18n }}</th>
|
||||
<th bitCell bitSortable="seats">{{ "assigned" | i18n }}</th>
|
||||
<th bitCell bitSortable="userCount">{{ "used" | i18n }}</th>
|
||||
<th bitCell bitSortable="userCount">{{ "remaining" | i18n }}</th>
|
||||
<th bitCell bitSortable="plan">{{ "billingPlan" | i18n }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let client of rows$ | async">
|
||||
<td bitCell width="30">
|
||||
<bit-avatar [text]="client.organizationName" [id]="client.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<div class="tw-flex tw-items-center tw-gap-4 tw-break-all">
|
||||
<a bitLink [routerLink]="['/organizations', client.organizationId]">{{
|
||||
client.organizationName
|
||||
}}</a>
|
||||
</div>
|
||||
</td>
|
||||
<td bitCell class="tw-whitespace-nowrap">
|
||||
<span>{{ client.seats }}</span>
|
||||
</td>
|
||||
<td bitCell class="tw-whitespace-nowrap">
|
||||
<span>{{ client.userCount }}</span>
|
||||
</td>
|
||||
<td bitCell class="tw-whitespace-nowrap">
|
||||
<span>{{ client.seats - client.userCount }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ client.plan }}</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
[bitMenuTriggerFor]="rowMenu"
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
size="small"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #rowMenu>
|
||||
<button type="button" bitMenuItem (click)="manageSubscription(client)">
|
||||
<i aria-hidden="true" class="bwi bwi-question-circle"></i>
|
||||
{{ "manageSubscription" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="remove(client)">
|
||||
<span class="tw-text-danger">
|
||||
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "unlinkOrganization" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</ng-container>
|
||||
</ng-container>
|
|
@ -0,0 +1,160 @@
|
|||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||
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";
|
||||
import { DialogService, TableDataSource } from "@bitwarden/components";
|
||||
|
||||
import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service";
|
||||
|
||||
import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-organization-subscription.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "manage-client-organizations.component.html",
|
||||
})
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class ManageClientOrganizationsComponent implements OnInit {
|
||||
providerId: string;
|
||||
loading = true;
|
||||
manageOrganizations = false;
|
||||
|
||||
set searchText(search: string) {
|
||||
this.selection.clear();
|
||||
this.dataSource.filter = search;
|
||||
}
|
||||
|
||||
clients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
|
||||
|
||||
protected didScroll = false;
|
||||
protected pageSize = 100;
|
||||
protected actionPromise: Promise<unknown>;
|
||||
private pagedClientsCount = 0;
|
||||
selection = new SelectionModel<string>(true, []);
|
||||
protected dataSource = new TableDataSource<ProviderOrganizationOrganizationDetailsResponse>();
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private providerService: ProviderService,
|
||||
private apiService: ApiService,
|
||||
private searchService: SearchService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private validationService: ValidationService,
|
||||
private webProviderService: WebProviderService,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
this.providerId = params.providerId;
|
||||
|
||||
await this.load();
|
||||
|
||||
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
this.searchText = qParams.search;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
const response = await this.apiService.getProviderClients(this.providerId);
|
||||
this.clients = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
this.dataSource.data = this.clients;
|
||||
this.manageOrganizations =
|
||||
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin;
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
isPaging() {
|
||||
const searching = this.isSearching();
|
||||
if (searching && this.didScroll) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.clients && this.clients.length > this.pageSize;
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return this.searchService.isSearchable(this.searchText);
|
||||
}
|
||||
|
||||
async resetPaging() {
|
||||
this.pagedClients = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (!this.clients || this.clients.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedClients.length;
|
||||
let pagedSize = this.pageSize;
|
||||
if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) {
|
||||
pagedSize = this.pagedClientsCount;
|
||||
}
|
||||
if (this.clients.length > pagedLength) {
|
||||
this.pagedClients = this.pagedClients.concat(
|
||||
this.clients.slice(pagedLength, pagedLength + pagedSize),
|
||||
);
|
||||
}
|
||||
this.pagedClientsCount = this.pagedClients.length;
|
||||
this.didScroll = this.pagedClients.length > this.pageSize;
|
||||
}
|
||||
|
||||
async manageSubscription(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
||||
if (organization == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = ManageClientOrganizationSubscriptionComponent.open(this.dialogService, {
|
||||
organization: organization,
|
||||
});
|
||||
|
||||
await firstValueFrom(dialogRef.closed);
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: organization.organizationName,
|
||||
content: { key: "detachOrganizationConfirmation" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.actionPromise = this.webProviderService.detachOrganization(
|
||||
this.providerId,
|
||||
organization.id,
|
||||
);
|
||||
try {
|
||||
await this.actionPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("detachedOrganization", organization.organizationName),
|
||||
);
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
}
|
||||
this.actionPromise = null;
|
||||
}
|
||||
}
|
|
@ -2,13 +2,20 @@ import { NgModule } from "@angular/core";
|
|||
|
||||
import { LayoutComponent as BitLayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||
import { OrgSwitcherComponent } from "@bitwarden/web-vault/app/layouts/org-switcher/org-switcher.component";
|
||||
import { ToggleWidthComponent } from "@bitwarden/web-vault/app/layouts/toggle-width.component";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module";
|
||||
|
||||
import { LayoutComponent } from "./layout.component";
|
||||
import { NavigationComponent } from "./navigation.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, NavigationModule, BitLayoutComponent, OrgSwitcherComponent],
|
||||
imports: [
|
||||
SharedModule,
|
||||
NavigationModule,
|
||||
BitLayoutComponent,
|
||||
OrgSwitcherComponent,
|
||||
ToggleWidthComponent,
|
||||
],
|
||||
declarations: [LayoutComponent, NavigationComponent],
|
||||
})
|
||||
export class LayoutModule {}
|
||||
|
|
|
@ -41,4 +41,6 @@
|
|||
[relativeTo]="route.parent"
|
||||
></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
|
||||
<app-toggle-width></app-toggle-width>
|
||||
</nav>
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||
|
@ -34,6 +35,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
enum State {
|
||||
NewUser,
|
||||
|
@ -65,6 +67,8 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||
protected data?: Data;
|
||||
protected loading = true;
|
||||
|
||||
activeAccountId: UserId;
|
||||
|
||||
// Remember device means for the user to trust the device
|
||||
rememberDeviceForm = this.formBuilder.group({
|
||||
rememberDevice: [true],
|
||||
|
@ -94,10 +98,12 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.loading = true;
|
||||
this.activeAccountId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
this.setupRememberDeviceValueChanges();
|
||||
|
||||
|
@ -150,7 +156,9 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private async setRememberDeviceDefaultValue() {
|
||||
const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice();
|
||||
const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice(
|
||||
this.activeAccountId,
|
||||
);
|
||||
|
||||
const rememberDevice = rememberDeviceFromState ?? true;
|
||||
|
||||
|
@ -161,7 +169,9 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||
this.rememberDevice.valueChanges
|
||||
.pipe(
|
||||
switchMap((value) =>
|
||||
defer(() => this.deviceTrustCryptoService.setShouldTrustDevice(value)),
|
||||
defer(() =>
|
||||
this.deviceTrustCryptoService.setShouldTrustDevice(this.activeAccountId, value),
|
||||
),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
|
@ -278,7 +288,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
|||
await this.passwordResetEnrollmentService.enroll(this.data.organizationId);
|
||||
|
||||
if (this.rememberDeviceForm.value.rememberDevice) {
|
||||
await this.deviceTrustCryptoService.trustDevice();
|
||||
await this.deviceTrustCryptoService.trustDevice(this.activeAccountId);
|
||||
}
|
||||
} catch (error) {
|
||||
this.validationService.showError(error);
|
||||
|
|
|
@ -1,79 +1,81 @@
|
|||
<div class="environment-selector-btn">
|
||||
{{ "loggingInOn" | i18n }}:
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggle(null)"
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="cdk-overlay-container"
|
||||
>
|
||||
<span class="text-primary">
|
||||
<ng-container *ngIf="selectedRegion$ | async as selectedRegion; else fallback">
|
||||
{{ selectedRegion.domain }}
|
||||
</ng-container>
|
||||
<ng-template #fallback>
|
||||
{{ "selfHostedServer" | i18n }}
|
||||
</ng-template>
|
||||
</span>
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayOpen]="isOpen"
|
||||
[cdkConnectedOverlayPositions]="overlayPosition"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||
(backdropClick)="isOpen = false"
|
||||
(detach)="close()"
|
||||
<ng-container
|
||||
*ngIf="{
|
||||
selectedRegion: selectedRegion$ | async
|
||||
} as data"
|
||||
>
|
||||
<div class="box-content">
|
||||
<div
|
||||
class="environment-selector-dialog"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
<div class="environment-selector-btn">
|
||||
{{ "loggingInOn" | i18n }}:
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggle(null)"
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="cdk-overlay-container"
|
||||
>
|
||||
<ng-container *ngFor="let region of availableRegions">
|
||||
<span class="text-primary">
|
||||
<ng-container *ngIf="data.selectedRegion; else fallback">
|
||||
{{ data.selectedRegion.domain }}
|
||||
</ng-container>
|
||||
<ng-template #fallback>
|
||||
{{ "selfHostedServer" | i18n }}
|
||||
</ng-template>
|
||||
</span>
|
||||
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayOpen]="isOpen"
|
||||
[cdkConnectedOverlayPositions]="overlayPosition"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||
(backdropClick)="isOpen = false"
|
||||
(detach)="close()"
|
||||
>
|
||||
<div class="box-content">
|
||||
<div
|
||||
class="environment-selector-dialog"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<ng-container *ngFor="let region of availableRegions">
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
(click)="toggle(region.key)"
|
||||
[attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="data.selectedRegion === region ? 'visible' : 'hidden'"
|
||||
></i>
|
||||
<span>{{ region.domain }}</span>
|
||||
</button>
|
||||
<br />
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
(click)="toggle(region.key)"
|
||||
[attr.aria-pressed]="selectedEnvironment === region.key ? 'true' : 'false'"
|
||||
(click)="toggle(ServerEnvironmentType.SelfHosted)"
|
||||
[attr.aria-pressed]="data.selectedRegion ? 'false' : 'true'"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="selectedEnvironment === region.key ? 'visible' : 'hidden'"
|
||||
[style.visibility]="data.selectedRegion ? 'hidden' : 'visible'"
|
||||
></i>
|
||||
<span>{{ region.domain }}</span>
|
||||
<span>{{ "selfHostedServer" | i18n }}</span>
|
||||
</button>
|
||||
<br />
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
(click)="toggle(ServerEnvironmentType.SelfHosted)"
|
||||
[attr.aria-pressed]="
|
||||
selectedEnvironment === ServerEnvironmentType.SelfHosted ? 'true' : 'false'
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
style="padding-bottom: 1px"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="
|
||||
selectedEnvironment === ServerEnvironmentType.SelfHosted ? 'visible' : 'hidden'
|
||||
"
|
||||
></i>
|
||||
<span>{{ "selfHostedServer" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
|
|
@ -36,11 +36,9 @@ import {
|
|||
})
|
||||
export class EnvironmentSelectorComponent {
|
||||
@Output() onOpenSelfHostedSettings = new EventEmitter();
|
||||
isOpen = false;
|
||||
showingModal = false;
|
||||
selectedEnvironment: Region;
|
||||
ServerEnvironmentType = Region;
|
||||
overlayPosition: ConnectedPosition[] = [
|
||||
protected isOpen = false;
|
||||
protected ServerEnvironmentType = Region;
|
||||
protected overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "bottom",
|
||||
|
|
|
@ -10,6 +10,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou
|
|||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
|
@ -75,6 +76,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||
protected userVerificationService: UserVerificationService,
|
||||
protected pinCryptoService: PinCryptoServiceAbstraction,
|
||||
protected biometricStateService: BiometricStateService,
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
@ -269,7 +271,8 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
// need to establish trust on the current device
|
||||
await this.deviceTrustCryptoService.trustDeviceIfRequired();
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id);
|
||||
|
||||
await this.doContinue(evaluatePasswordAfterUnlock);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { IsActiveMatchOptions, Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { Subject, firstValueFrom, takeUntil } from "rxjs";
|
||||
|
||||
import {
|
||||
AuthRequestLoginCredentials,
|
||||
|
@ -9,6 +9,7 @@ import {
|
|||
LoginEmailServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
|
@ -87,6 +88,7 @@ export class LoginViaAuthRequestComponent
|
|||
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService);
|
||||
|
||||
|
@ -388,7 +390,8 @@ export class LoginViaAuthRequestComponent
|
|||
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
// need to establish trust on the current device
|
||||
await this.deviceTrustCryptoService.trustDeviceIfRequired();
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id);
|
||||
|
||||
// TODO: don't forget to use auto enrollment service everywhere we trust device
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { ErrorHandler, Injectable, Injector, inject } from "@angular/core";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@Injectable()
|
||||
export class LoggingErrorHandler extends ErrorHandler {
|
||||
/**
|
||||
* When injecting services into an `ErrorHandler`, we must use the `Injector` manually to avoid circular dependency errors.
|
||||
*
|
||||
* https://stackoverflow.com/a/57115053
|
||||
*/
|
||||
private injector = inject(Injector);
|
||||
|
||||
override handleError(error: any): void {
|
||||
try {
|
||||
const logService = this.injector.get(LogService, null);
|
||||
logService.error(error);
|
||||
} catch {
|
||||
super.handleError(error);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue