Merge remote-tracking branch 'origin/main' into run-migrations-less-often

This commit is contained in:
Justin Baur 2024-04-04 14:15:52 -04:00
commit 9a83e7e856
No known key found for this signature in database
230 changed files with 14388 additions and 9925 deletions

View File

@ -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/**"

1
.github/CODEOWNERS vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -98,7 +98,7 @@ describe("InsertAutofillContentService", () => {
});
afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
windowLocationSpy.mockRestore();
confirmSpy.mockRestore();
document.body.innerHTML = "";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"] }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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=="
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ export class BulkMoveDialogComponent implements OnInit {
cipherIds: string[] = [];
formGroup = this.formBuilder.group({
folderId: ["", [Validators.required]],
folderId: ["", [Validators.nullValidator]],
});
folders$: Observable<FolderView[]>;

View File

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

View File

@ -50,6 +50,7 @@
[cloneableOrganizationCiphers]="false"
[showAdminActions]="false"
(onEvent)="onVaultItemsEvent($event)"
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async"
>
</app-vault-items>
<div

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,4 +41,6 @@
[relativeTo]="route.parent"
></bit-nav-item>
</bit-nav-group>
<app-toggle-width></app-toggle-width>
</nav>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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