1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-22 11:45:59 +01:00

Merge branch 'main' into vault/pm-5273

# Conflicts:
#	libs/common/src/platform/abstractions/state.service.ts
#	libs/common/src/platform/services/state.service.ts
#	libs/common/src/state-migrations/migrate.ts
This commit is contained in:
Carlos Gonçalves 2024-03-25 18:38:10 +00:00
commit 8a1df6671a
No known key found for this signature in database
GPG Key ID: 8147F618E732EF25
364 changed files with 9254 additions and 2586 deletions

72
.github/workflows/scan.yml vendored Normal file
View File

@ -0,0 +1,72 @@
name: Scan
on:
workflow_dispatch:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
pull_request_target:
types: [opened, synchronize]
permissions: read-all
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
sast:
name: SAST scan
runs-on: ubuntu-22.04
needs: check-run
permissions:
security-events: write
steps:
- name: Check out repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
project_name: ${{ github.repository }}
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
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 }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
with:
sarif_file: cx_result.sarif
quality:
name: Quality scan
runs-on: ubuntu-22.04
needs: check-run
steps:
- name: Check out repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: >
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.test.inclusions=**/*.spec.ts
-Dsonar.tests=.

View File

@ -119,7 +119,7 @@
"message": "Дадаць лагін"
},
"addCardMenu": {
"message": "Add card"
"message": "Дадаць картку"
},
"addIdentityMenu": {
"message": "Add identity"
@ -269,7 +269,7 @@
"message": "Даўжыня"
},
"passwordMinLength": {
"message": "Minimum password length"
"message": "Мінімальная даўжыня пароля"
},
"uppercase": {
"message": "Вялікія літары (A-Z)"
@ -369,7 +369,7 @@
"message": "Iншае"
},
"unlockMethodNeededToChangeTimeoutActionDesc": {
"message": "Set up an unlock method to change your vault timeout action."
"message": "Наладзіць метад разблакіроўкі для змянення дзеяння часу чакання вашага сховішча."
},
"unlockMethodNeeded": {
"message": "Set up an unlock method in Settings"
@ -415,7 +415,7 @@
"message": "Заблакіраваць зараз"
},
"lockAll": {
"message": "Lock all"
"message": "Заблакаваць усе"
},
"immediately": {
"message": "Адразу"
@ -494,7 +494,7 @@
"message": "Ваш уліковы запіс створаны! Цяпер вы можаце ўвайсці ў яго."
},
"youSuccessfullyLoggedIn": {
"message": "You successfully logged in"
"message": "Вы паспяхова аўтарызаваны"
},
"youMayCloseThisWindow": {
"message": "You may close this window"
@ -2005,7 +2005,7 @@
"message": "Выбраць папку..."
},
"noFoldersFound": {
"message": "No folders found",
"message": "Папкі не знойдзены",
"description": "Used as a message within the notification bar when no folders are found"
},
"orgPermissionsUpdatedMustSetPassword": {
@ -2215,7 +2215,7 @@
"message": "Версія сервера"
},
"selfHostedServer": {
"message": "self-hosted"
"message": "уласнае размяшчэнне"
},
"thirdParty": {
"message": "Іншы пастаўшчык"
@ -2356,25 +2356,25 @@
}
},
"loggingInOn": {
"message": "Logging in on"
"message": "Увайсці на"
},
"opensInANewWindow": {
"message": "Адкрываць у новым акне"
},
"deviceApprovalRequired": {
"message": "Device approval required. Select an approval option below:"
"message": "Патрабуецца ўхваленне прылады. Выберыце параметры ўхвалення ніжэй:"
},
"rememberThisDevice": {
"message": "Remember this device"
"message": "Запомніць гэту прыладу"
},
"uncheckIfPublicDevice": {
"message": "Uncheck if using a public device"
"message": "Здыміце пазнаку, калі выкарыстоўваеце агульнадаступную прыладу"
},
"approveFromYourOtherDevice": {
"message": "Approve from your other device"
"message": "Ухваліць з іншай вашай прылады"
},
"requestAdminApproval": {
"message": "Request admin approval"
"message": "Запытаць ухваленне адміністратара"
},
"approveWithMasterPassword": {
"message": "Approve with master password"
@ -2402,7 +2402,7 @@
"message": "Адлюстраванне"
},
"accountSuccessfullyCreated": {
"message": "Account successfully created!"
"message": "Уліковы запіс паспяхова створаны!"
},
"adminApprovalRequested": {
"message": "Admin approval requested"

View File

@ -688,7 +688,7 @@
"message": "A bejelentkezési jelszó frissítésének kérése, ha változást lett érzékelve egy webhelyen. Minden bejelentkezett fiókra vonatkozik."
},
"enableUsePasskeys": {
"message": "Kérés a jhozzáférési kulcs mentésére és használatára"
"message": "Kérés a hozzáférési kulcs mentésére és használatára"
},
"usePasskeysDesc": {
"message": "Kérés az új hozzáféréi kulcsok mentésére vagy bejelentkezés a széfben tárolt hozzáférési kulcsokkal. Minden bejelentkezett fiókra vonatkozik."

View File

@ -1,4 +1,5 @@
import { OrganizationService as AbstractOrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
import {
FactoryOptions,
@ -6,11 +7,7 @@ import {
factory,
} from "../../../platform/background/service-factories/factory-options";
import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory";
import {
stateServiceFactory,
StateServiceInitOptions,
} from "../../../platform/background/service-factories/state-service.factory";
import { BrowserOrganizationService } from "../../services/browser-organization.service";
import { StateServiceInitOptions } from "../../../platform/background/service-factories/state-service.factory";
type OrganizationServiceFactoryOptions = FactoryOptions;
@ -25,10 +22,6 @@ export function organizationServiceFactory(
cache,
"organizationService",
opts,
async () =>
new BrowserOrganizationService(
await stateServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
),
async () => new OrganizationService(await stateProviderFactory(cache, opts)),
);
}

View File

@ -1,12 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
import { browserSession, sessionSync } from "../../platform/decorators/session-sync-observable";
@browserSession
export class BrowserOrganizationService extends OrganizationService {
@sessionSync({ initializer: Organization.fromJSON, initializeAs: "array" })
protected _organizations: BehaviorSubject<Organization[]>;
}

View File

@ -0,0 +1,38 @@
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
import {
ApiServiceInitOptions,
apiServiceFactory,
} from "../../../platform/background/service-factories/api-service.factory";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
type AvatarServiceFactoryOptions = FactoryOptions;
export type AvatarServiceInitOptions = AvatarServiceFactoryOptions &
ApiServiceInitOptions &
StateProviderInitOptions;
export function avatarServiceFactory(
cache: { avatarService?: AvatarServiceAbstraction } & CachedServices,
opts: AvatarServiceInitOptions,
): Promise<AvatarServiceAbstraction> {
return factory(
cache,
"avatarService",
opts,
async () =>
new AvatarService(
await apiServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
),
);
}

View File

@ -9,6 +9,7 @@ import {
ApiServiceInitOptions,
} from "../../../platform/background/service-factories/api-service.factory";
import { appIdServiceFactory } from "../../../platform/background/service-factories/app-id-service.factory";
import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory";
import {
CryptoServiceInitOptions,
cryptoServiceFactory,
@ -119,6 +120,7 @@ export function loginStrategyServiceFactory(
await deviceTrustCryptoServiceFactory(cache, opts),
await authRequestServiceFactory(cache, opts),
await globalStateProviderFactory(cache, opts),
await billingAccountProfileStateServiceFactory(cache, opts),
),
);
}

View File

@ -7,13 +7,29 @@ import {
factory,
} from "../../../platform/background/service-factories/factory-options";
import {
stateServiceFactory,
StateServiceInitOptions,
} from "../../../platform/background/service-factories/state-service.factory";
GlobalStateProviderInitOptions,
globalStateProviderFactory,
} from "../../../platform/background/service-factories/global-state-provider.factory";
import {
PlatformUtilsServiceInitOptions,
platformUtilsServiceFactory,
} from "../../../platform/background/service-factories/platform-utils-service.factory";
import {
SingleUserStateProviderInitOptions,
singleUserStateProviderFactory,
} from "../../../platform/background/service-factories/single-user-state-provider.factory";
import {
SecureStorageServiceInitOptions,
secureStorageServiceFactory,
} from "../../../platform/background/service-factories/storage-service.factory";
type TokenServiceFactoryOptions = FactoryOptions;
export type TokenServiceInitOptions = TokenServiceFactoryOptions & StateServiceInitOptions;
export type TokenServiceInitOptions = TokenServiceFactoryOptions &
SingleUserStateProviderInitOptions &
GlobalStateProviderInitOptions &
PlatformUtilsServiceInitOptions &
SecureStorageServiceInitOptions;
export function tokenServiceFactory(
cache: { tokenService?: AbstractTokenService } & CachedServices,
@ -23,6 +39,12 @@ export function tokenServiceFactory(
cache,
"tokenService",
opts,
async () => new TokenService(await stateServiceFactory(cache, opts)),
async () =>
new TokenService(
await singleUserStateProviderFactory(cache, opts),
await globalStateProviderFactory(cache, opts),
(await platformUtilsServiceFactory(cache, opts)).supportsSecureStorage(),
await secureStorageServiceFactory(cache, opts),
),
);
}

View File

@ -1,32 +1,61 @@
import { Location } from "@angular/common";
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { Observable, combineLatest, switchMap } from "rxjs";
import { CurrentAccountService } from "./services/current-account.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UserId } from "@bitwarden/common/types/guid";
export type CurrentAccount = {
id: UserId;
name: string | undefined;
email: string;
status: AuthenticationStatus;
avatarColor: string;
};
@Component({
selector: "app-current-account",
templateUrl: "current-account.component.html",
})
export class CurrentAccountComponent {
currentAccount$: Observable<CurrentAccount>;
constructor(
private currentAccountService: CurrentAccountService,
private accountService: AccountService,
private avatarService: AvatarService,
private router: Router,
private location: Location,
private route: ActivatedRoute,
) {}
) {
this.currentAccount$ = combineLatest([
this.accountService.activeAccount$,
this.avatarService.avatarColor$,
]).pipe(
switchMap(async ([account, avatarColor]) => {
if (account == null) {
return null;
}
const currentAccount: CurrentAccount = {
id: account.id,
name: account.name || account.email,
email: account.email,
status: account.status,
avatarColor,
};
get currentAccount$() {
return this.currentAccountService.currentAccount$;
return currentAccount;
}),
);
}
async currentAccountClicked() {
if (this.route.snapshot.data.state.includes("account-switcher")) {
this.location.back();
} else {
// 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(["/account-switcher"]);
await this.router.navigate(["/account-switcher"]);
}
}
}

View File

@ -1,12 +1,12 @@
import { matches, mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, timeout } from "rxjs";
import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs";
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserId } from "@bitwarden/common/types/guid";
import { AccountSwitcherService } from "./account-switcher.service";
@ -16,7 +16,7 @@ describe("AccountSwitcherService", () => {
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null);
const accountService = mock<AccountService>();
const stateService = mock<StateService>();
const avatarService = mock<AvatarService>();
const messagingService = mock<MessagingService>();
const environmentService = mock<EnvironmentService>();
const logService = mock<LogService>();
@ -25,11 +25,13 @@ describe("AccountSwitcherService", () => {
beforeEach(() => {
jest.resetAllMocks();
accountService.accounts$ = accountsSubject;
accountService.activeAccount$ = activeAccountSubject;
accountSwitcherService = new AccountSwitcherService(
accountService,
stateService,
avatarService,
messagingService,
environmentService,
logService,
@ -44,6 +46,7 @@ describe("AccountSwitcherService", () => {
status: AuthenticationStatus.Unlocked,
};
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
accountsSubject.next({
"1": user1AccountInfo,
} as Record<UserId, AccountInfo>);
@ -72,6 +75,7 @@ describe("AccountSwitcherService", () => {
status: AuthenticationStatus.Unlocked,
};
}
avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc"));
accountsSubject.next(seedAccounts);
activeAccountSubject.next(
Object.assign(seedAccounts["1" as UserId], { id: "1" as UserId }),

View File

@ -11,11 +11,11 @@ import {
} from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserId } from "@bitwarden/common/types/guid";
import { fromChromeEvent } from "../../../../platform/browser/from-chrome-event";
@ -44,7 +44,7 @@ export class AccountSwitcherService {
constructor(
private accountService: AccountService,
private stateService: StateService,
private avatarService: AvatarService,
private messagingService: MessagingService,
private environmentService: EnvironmentService,
private logService: LogService,
@ -68,7 +68,9 @@ export class AccountSwitcherService {
server: await this.environmentService.getHost(id),
status: account.status,
isActive: id === activeAccount?.id,
avatarColor: await this.stateService.getAvatarColor({ userId: id }),
avatarColor: await firstValueFrom(
this.avatarService.getUserAvatarColor$(id as UserId),
),
};
}),
);

View File

@ -1,44 +0,0 @@
import { Injectable } from "@angular/core";
import { Observable, switchMap } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserId } from "@bitwarden/common/types/guid";
export type CurrentAccount = {
id: UserId;
name: string | undefined;
email: string;
status: AuthenticationStatus;
avatarColor: string;
};
@Injectable({
providedIn: "root",
})
export class CurrentAccountService {
currentAccount$: Observable<CurrentAccount>;
constructor(
private accountService: AccountService,
private stateService: StateService,
) {
this.currentAccount$ = this.accountService.activeAccount$.pipe(
switchMap(async (account) => {
if (account == null) {
return null;
}
const currentAccount: CurrentAccount = {
id: account.id,
name: account.name || account.email,
email: account.email,
status: account.status,
avatarColor: await this.stateService.getAvatarColor({ userId: account.id }),
};
return currentAccount;
}),
);
}
}

View File

@ -91,6 +91,8 @@ export class LoginComponent extends BaseLoginComponent {
}
async launchSsoBrowser() {
// Save off email for SSO
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
await this.loginService.saveEmailSettings();
// Generate necessary sso params
const passwordOptions: any = {

View File

@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -16,9 +17,10 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
private activatedRoute: ActivatedRoute,
) {
super(twoFactorService, router, i18nService, platformUtilsService, window);
super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService);
}
close() {

View File

@ -8,11 +8,21 @@ import {
AutofillOverlayVisibility,
} from "@bitwarden/common/autofill/constants";
import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service";
import {
DefaultDomainSettingsService,
DomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
import { I18nService } from "@bitwarden/common/platform/services/i18n.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import {
FakeStateProvider,
FakeAccountService,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -41,6 +51,10 @@ import OverlayBackground from "./overlay.background";
const iconServerUrl = "https://icons.bitwarden.com/";
describe("OverlayBackground", () => {
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
let domainSettingsService: DomainSettingsService;
let buttonPortSpy: chrome.runtime.Port;
let listPortSpy: chrome.runtime.Port;
let overlayBackground: OverlayBackground;
@ -50,7 +64,6 @@ describe("OverlayBackground", () => {
const environmentService = mock<EnvironmentService>({
getIconsUrl: () => iconServerUrl,
});
const settingsService = mock<SettingsService>();
const stateService = mock<BrowserStateService>();
const autofillSettingsService = mock<AutofillSettingsService>();
const i18nService = mock<I18nService>();
@ -72,12 +85,13 @@ describe("OverlayBackground", () => {
};
beforeEach(() => {
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
overlayBackground = new OverlayBackground(
cipherService,
autofillService,
authService,
environmentService,
settingsService,
domainSettingsService,
stateService,
autofillSettingsService,
i18nService,
@ -90,6 +104,7 @@ describe("OverlayBackground", () => {
.mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus);
themeStateService.selectedTheme$ = of(ThemeType.Light);
domainSettingsService.showFavicons$ = of(true);
void overlayBackground.init();
});
@ -274,7 +289,7 @@ describe("OverlayBackground", () => {
card: { subTitle: "Mastercard, *1234" },
});
it("formats and returns the cipher data", () => {
it("formats and returns the cipher data", async () => {
overlayBackground["overlayLoginCiphers"] = new Map([
["overlay-cipher-0", cipher2],
["overlay-cipher-1", cipher1],
@ -282,7 +297,7 @@ describe("OverlayBackground", () => {
["overlay-cipher-3", cipher4],
]);
const overlayCipherData = overlayBackground["getOverlayCipherData"]();
const overlayCipherData = await overlayBackground["getOverlayCipherData"]();
expect(overlayCipherData).toStrictEqual([
{

View File

@ -1,10 +1,10 @@
import { firstValueFrom } from "rxjs";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -92,7 +92,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
private autofillService: AutofillService,
private authService: AuthService,
private environmentService: EnvironmentService,
private settingsService: SettingsService,
private domainSettingsService: DomainSettingsService,
private stateService: StateService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private i18nService: I18nService,
@ -145,7 +145,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]);
}
const ciphers = this.getOverlayCipherData();
const ciphers = await this.getOverlayCipherData();
this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers });
await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", {
isOverlayCiphersPopulated: Boolean(ciphers.length),
@ -156,8 +156,8 @@ class OverlayBackground implements OverlayBackgroundInterface {
* Strips out unnecessary data from the ciphers and returns an array of
* objects that contain the cipher data needed for the overlay list.
*/
private getOverlayCipherData(): OverlayCipherData[] {
const isFaviconDisabled = this.settingsService.getDisableFavicon();
private async getOverlayCipherData(): Promise<OverlayCipherData[]> {
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
const overlayCiphersArray = Array.from(this.overlayLoginCiphers);
const overlayCipherData = [];
let loginCipherIcon: WebsiteIconData;
@ -165,7 +165,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) {
const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex];
if (!loginCipherIcon && cipher.type === CipherType.Login) {
loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, isFaviconDisabled);
loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons);
}
overlayCipherData.push({
@ -177,7 +177,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
icon:
cipher.type === CipherType.Login
? loginCipherIcon
: buildCipherIcon(this.iconsServerUrl, cipher, isFaviconDisabled),
: buildCipherIcon(this.iconsServerUrl, cipher, showFavicons),
login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null,
card: cipher.type === CipherType.Card ? cipher.card.subTitle : null,
});
@ -699,7 +699,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`),
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
translations: this.getTranslations(),
ciphers: isOverlayListPort ? this.getOverlayCipherData() : null,
ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null,
});
this.updateOverlayPosition({
overlayElement: isOverlayListPort

View File

@ -6,6 +6,7 @@ import {
EventCollectionServiceInitOptions,
eventCollectionServiceFactory,
} from "../../../background/service-factories/event-collection-service.factory";
import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory";
import {
CachedServices,
factory,
@ -69,6 +70,7 @@ export function autofillServiceFactory(
await logServiceFactory(cache, opts),
await domainSettingsServiceFactory(cache, opts),
await userVerificationServiceFactory(cache, opts),
await billingAccountProfileStateServiceFactory(cache, opts),
),
);
}

View File

@ -3,6 +3,7 @@ import { of } from "rxjs";
import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@ -18,6 +19,7 @@ describe("context-menu", () => {
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
let i18nService: MockProxy<I18nService>;
let logService: MockProxy<LogService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let removeAllSpy: jest.SpyInstance<void, [callback?: () => void]>;
let createSpy: jest.SpyInstance<
@ -32,6 +34,7 @@ describe("context-menu", () => {
autofillSettingsService = mock();
i18nService = mock();
logService = mock();
billingAccountProfileStateService = mock();
removeAllSpy = jest
.spyOn(chrome.contextMenus, "removeAll")
@ -50,6 +53,7 @@ describe("context-menu", () => {
autofillSettingsService,
i18nService,
logService,
billingAccountProfileStateService,
);
autofillSettingsService.enableContextMenu$ = of(true);
});
@ -66,7 +70,7 @@ describe("context-menu", () => {
});
it("has menu enabled, but does not have premium", async () => {
stateService.getCanAccessPremium.mockResolvedValue(false);
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false);
const createdMenu = await sut.init();
expect(createdMenu).toBeTruthy();
@ -74,7 +78,7 @@ describe("context-menu", () => {
});
it("has menu enabled and has premium", async () => {
stateService.getCanAccessPremium.mockResolvedValue(true);
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
const createdMenu = await sut.init();
expect(createdMenu).toBeTruthy();
@ -128,7 +132,7 @@ describe("context-menu", () => {
});
it("create entry for each cipher piece", async () => {
stateService.getCanAccessPremium.mockResolvedValue(true);
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
await sut.loadOptions("TEST_TITLE", "1", createCipher());
@ -137,7 +141,7 @@ describe("context-menu", () => {
});
it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => {
stateService.getCanAccessPremium.mockResolvedValue(true);
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
await sut.loadOptions("TEST_TITLE", "NOOP");

View File

@ -17,6 +17,7 @@ import {
SEPARATOR_ID,
} from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
@ -27,6 +28,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory";
import { Account } from "../../models/account";
import { billingAccountProfileStateServiceFactory } from "../../platform/background/service-factories/billing-account-profile-state-service.factory";
import { CachedServices } from "../../platform/background/service-factories/factory-options";
import {
i18nServiceFactory,
@ -163,6 +165,7 @@ export class MainContextMenuHandler {
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private i18nService: I18nService,
private logService: LogService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {}
static async mv3Create(cachedServices: CachedServices) {
@ -184,6 +187,11 @@ export class MainContextMenuHandler {
stateServiceOptions: {
stateFactory: stateFactory,
},
platformUtilsServiceOptions: {
clipboardWriteCallback: () => Promise.resolve(),
biometricCallback: () => Promise.resolve(false),
win: self,
},
};
return new MainContextMenuHandler(
@ -191,6 +199,7 @@ export class MainContextMenuHandler {
await autofillSettingsServiceFactory(cachedServices, serviceOptions),
await i18nServiceFactory(cachedServices, serviceOptions),
await logServiceFactory(cachedServices, serviceOptions),
await billingAccountProfileStateServiceFactory(cachedServices, serviceOptions),
);
}
@ -212,7 +221,10 @@ export class MainContextMenuHandler {
try {
for (const options of this.initContextMenuItems) {
if (options.checkPremiumAccess && !(await this.stateService.getCanAccessPremium())) {
if (
options.checkPremiumAccess &&
!(await firstValueFrom(this.billingAccountProfileStateService.hasPremiumFromAnySource$))
) {
continue;
}
@ -307,7 +319,9 @@ export class MainContextMenuHandler {
await createChildItem(COPY_USERNAME_ID);
}
const canAccessPremium = await this.stateService.getCanAccessPremium();
const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
);
if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) {
await createChildItem(COPY_VERIFICATION_CODE_ID);
}

View File

@ -1,8 +1,15 @@
import AutofillOverlayButtonIframe from "./autofill-overlay-button-iframe";
import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element";
describe("AutofillOverlayButtonIframe", () => {
window.customElements.define("autofill-overlay-button-iframe", AutofillOverlayButtonIframe);
window.customElements.define(
"autofill-overlay-button-iframe",
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayButtonIframe(this);
}
},
);
afterAll(() => {
jest.clearAllMocks();
@ -13,7 +20,7 @@ describe("AutofillOverlayButtonIframe", () => {
const iframe = document.querySelector("autofill-overlay-button-iframe");
expect(iframe).toBeInstanceOf(AutofillOverlayButtonIframe);
expect(iframe).toBeInstanceOf(AutofillOverlayIframeElement);
expect(iframe).toBeInstanceOf(HTMLElement);
expect(iframe.shadowRoot).toBeDefined();
});
});

View File

@ -3,8 +3,9 @@ import { AutofillOverlayPort } from "../../utils/autofill-overlay.enum";
import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element";
class AutofillOverlayButtonIframe extends AutofillOverlayIframeElement {
constructor() {
constructor(element: HTMLElement) {
super(
element,
"overlay/button.html",
AutofillOverlayPort.Button,
{

View File

@ -4,7 +4,21 @@ import AutofillOverlayIframeService from "./autofill-overlay-iframe.service";
jest.mock("./autofill-overlay-iframe.service");
describe("AutofillOverlayIframeElement", () => {
window.customElements.define("autofill-overlay-iframe", AutofillOverlayIframeElement);
window.customElements.define(
"autofill-overlay-iframe",
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayIframeElement(
this,
"overlay/button.html",
"overlay/button",
{ background: "transparent", border: "none" },
"bitwardenOverlayButton",
);
}
},
);
afterAll(() => {
jest.clearAllMocks();

View File

@ -1,16 +1,15 @@
import AutofillOverlayIframeService from "./autofill-overlay-iframe.service";
class AutofillOverlayIframeElement extends HTMLElement {
class AutofillOverlayIframeElement {
constructor(
element: HTMLElement,
iframePath: string,
portName: string,
initStyles: Partial<CSSStyleDeclaration>,
iframeTitle: string,
ariaAlert?: string,
) {
super();
const shadow: ShadowRoot = this.attachShadow({ mode: "closed" });
const shadow: ShadowRoot = element.attachShadow({ mode: "closed" });
const autofillOverlayIframeService = new AutofillOverlayIframeService(
iframePath,
portName,

View File

@ -1,8 +1,15 @@
import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element";
import AutofillOverlayListIframe from "./autofill-overlay-list-iframe";
describe("AutofillOverlayListIframe", () => {
window.customElements.define("autofill-overlay-list-iframe", AutofillOverlayListIframe);
window.customElements.define(
"autofill-overlay-list-iframe",
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayListIframe(this);
}
},
);
afterAll(() => {
jest.clearAllMocks();
@ -13,7 +20,7 @@ describe("AutofillOverlayListIframe", () => {
const iframe = document.querySelector("autofill-overlay-list-iframe");
expect(iframe).toBeInstanceOf(AutofillOverlayListIframe);
expect(iframe).toBeInstanceOf(AutofillOverlayIframeElement);
expect(iframe).toBeInstanceOf(HTMLElement);
expect(iframe.shadowRoot).toBeDefined();
});
});

View File

@ -3,8 +3,9 @@ import { AutofillOverlayPort } from "../../utils/autofill-overlay.enum";
import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element";
class AutofillOverlayListIframe extends AutofillOverlayIframeElement {
constructor() {
constructor(element: HTMLElement) {
super(
element,
"overlay/list.html",
AutofillOverlayPort.List,
{

View File

@ -877,6 +877,44 @@ describe("AutofillOverlayContentService", () => {
sender: "autofillOverlayContentService",
});
});
it("builds the overlay elements as custom web components if the user's browser is not Firefox", () => {
let namesIndex = 0;
const customNames = ["op-autofill-overlay-button", "op-autofill-overlay-list"];
jest
.spyOn(autofillOverlayContentService as any, "generateRandomCustomElementName")
.mockImplementation(() => {
if (namesIndex > 1) {
return "";
}
const customName = customNames[namesIndex];
namesIndex++;
return customName;
});
autofillOverlayContentService["isFirefoxBrowser"] = false;
autofillOverlayContentService.openAutofillOverlay();
expect(autofillOverlayContentService["overlayButtonElement"]).toBeInstanceOf(HTMLElement);
expect(autofillOverlayContentService["overlayButtonElement"].tagName).toEqual(
customNames[0].toUpperCase(),
);
expect(autofillOverlayContentService["overlayListElement"]).toBeInstanceOf(HTMLElement);
expect(autofillOverlayContentService["overlayListElement"].tagName).toEqual(
customNames[1].toUpperCase(),
);
});
it("builds the overlay elements as `div` elements if the user's browser is Firefox", () => {
autofillOverlayContentService["isFirefoxBrowser"] = true;
autofillOverlayContentService.openAutofillOverlay();
expect(autofillOverlayContentService["overlayButtonElement"]).toBeInstanceOf(HTMLDivElement);
expect(autofillOverlayContentService["overlayListElement"]).toBeInstanceOf(HTMLDivElement);
});
});
describe("focusMostRecentOverlayField", () => {

View File

@ -30,6 +30,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
isOverlayCiphersPopulated = false;
pageDetailsUpdateRequired = false;
autofillOverlayVisibility: number;
private isFirefoxBrowser =
globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 ||
globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1;
private readonly generateRandomCustomElementName = generateRandomCustomElementName;
private readonly findTabs = tabbable;
private readonly sendExtensionMessage = sendExtensionMessage;
private formFieldElements: Set<ElementWithOpId<FormFieldElement>> = new Set([]);
@ -593,6 +597,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private updateOverlayButtonPosition() {
if (!this.overlayButtonElement) {
this.createAutofillOverlayButton();
this.updateCustomElementDefaultStyles(this.overlayButtonElement);
}
if (!this.isOverlayButtonVisible) {
@ -613,6 +618,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private updateOverlayListPosition() {
if (!this.overlayListElement) {
this.createAutofillOverlayList();
this.updateCustomElementDefaultStyles(this.overlayListElement);
}
if (!this.isOverlayListVisible) {
@ -765,11 +771,24 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
return;
}
const customElementName = generateRandomCustomElementName();
globalThis.customElements?.define(customElementName, AutofillOverlayButtonIframe);
this.overlayButtonElement = globalThis.document.createElement(customElementName);
if (this.isFirefoxBrowser) {
this.overlayButtonElement = globalThis.document.createElement("div");
new AutofillOverlayButtonIframe(this.overlayButtonElement);
this.updateCustomElementDefaultStyles(this.overlayButtonElement);
return;
}
const customElementName = this.generateRandomCustomElementName();
globalThis.customElements?.define(
customElementName,
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayButtonIframe(this);
}
},
);
this.overlayButtonElement = globalThis.document.createElement(customElementName);
}
/**
@ -781,11 +800,24 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
return;
}
const customElementName = generateRandomCustomElementName();
globalThis.customElements?.define(customElementName, AutofillOverlayListIframe);
this.overlayListElement = globalThis.document.createElement(customElementName);
if (this.isFirefoxBrowser) {
this.overlayListElement = globalThis.document.createElement("div");
new AutofillOverlayListIframe(this.overlayListElement);
this.updateCustomElementDefaultStyles(this.overlayListElement);
return;
}
const customElementName = this.generateRandomCustomElementName();
globalThis.customElements?.define(
customElementName,
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayListIframe(this);
}
},
);
this.overlayListElement = globalThis.document.createElement(customElementName);
}
/**

View File

@ -8,6 +8,7 @@ import {
DefaultDomainSettingsService,
DomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -72,6 +73,7 @@ describe("AutofillService", () => {
const eventCollectionService = mock<EventCollectionService>();
const logService = mock<LogService>();
const userVerificationService = mock<UserVerificationService>();
const billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
beforeEach(() => {
autofillService = new AutofillService(
@ -83,6 +85,7 @@ describe("AutofillService", () => {
logService,
domainSettingsService,
userVerificationService,
billingAccountProfileStateService,
);
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
@ -476,6 +479,7 @@ describe("AutofillService", () => {
it("throws an error if an autofill did not occur for any of the passed pages", async () => {
autofillOptions.tab.url = "https://a-different-url.com";
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
try {
await autofillService.doAutoFill(autofillOptions);
@ -487,7 +491,6 @@ describe("AutofillService", () => {
});
it("will autofill login data for a page", async () => {
jest.spyOn(stateService, "getCanAccessPremium");
jest.spyOn(autofillService as any, "generateFillScript");
jest.spyOn(autofillService as any, "generateLoginFillScript");
jest.spyOn(logService, "info");
@ -497,8 +500,6 @@ describe("AutofillService", () => {
const autofillResult = await autofillService.doAutoFill(autofillOptions);
const currentAutofillPageDetails = autofillOptions.pageDetails[0];
expect(stateService.getCanAccessPremium).toHaveBeenCalled();
expect(autofillService["getDefaultUriMatchStrategy"]).toHaveBeenCalled();
expect(autofillService["generateFillScript"]).toHaveBeenCalledWith(
currentAutofillPageDetails.details,
{
@ -660,7 +661,7 @@ describe("AutofillService", () => {
it("returns a TOTP value", async () => {
const totpCode = "123456";
autofillOptions.cipher.login.totp = "totp";
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(true);
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true);
jest.spyOn(totpService, "getCode").mockResolvedValue(totpCode);
@ -673,7 +674,7 @@ describe("AutofillService", () => {
it("does not return a TOTP value if the user does not have premium features", async () => {
autofillOptions.cipher.login.totp = "totp";
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(false);
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false);
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true);
const autofillResult = await autofillService.doAutoFill(autofillOptions);
@ -707,7 +708,7 @@ describe("AutofillService", () => {
it("returns a null value if the user cannot access premium and the organization does not use TOTP", async () => {
autofillOptions.cipher.login.totp = "totp";
autofillOptions.cipher.organizationUseTotp = false;
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValueOnce(false);
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false);
const autofillResult = await autofillService.doAutoFill(autofillOptions);
@ -717,13 +718,12 @@ describe("AutofillService", () => {
it("returns a null value if the user has disabled `auto TOTP copy`", async () => {
autofillOptions.cipher.login.totp = "totp";
autofillOptions.cipher.organizationUseTotp = true;
jest.spyOn(stateService, "getCanAccessPremium").mockResolvedValue(true);
billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true);
jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(false);
jest.spyOn(totpService, "getCode");
const autofillResult = await autofillService.doAutoFill(autofillOptions);
expect(stateService.getCanAccessPremium).toHaveBeenCalled();
expect(autofillService.getShouldAutoCopyTotp).toHaveBeenCalled();
expect(totpService.getCode).not.toHaveBeenCalled();
expect(autofillResult).toBeNull();
@ -3380,6 +3380,34 @@ describe("AutofillService", () => {
expect(value).toBe(false);
});
it("validates attribute identifiers with mixed camel case and non-alpha characters", () => {
const attributes: Record<string, boolean> = {
_$1_go_look: true,
go_look: true,
goLook: true,
go1look: true,
"go look": true,
look_go: true,
findPerson: true,
query$1: true,
look_goo: false,
golook: false,
lookgo: false,
logonField: false,
ego_input: false,
"Gold Password": false,
searching_for: false,
person_finder: false,
};
const autofillFieldMocks = Object.keys(attributes).map((key) =>
createAutofillFieldMock({ htmlID: key }),
);
autofillFieldMocks.forEach((field) => {
const value = AutofillService["isSearchField"](field);
expect(value).toBe(attributes[field.htmlID]);
});
});
});
describe("isFieldMatch", () => {

View File

@ -5,6 +5,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import {
UriMatchStrategySetting,
@ -44,6 +45,7 @@ export default class AutofillService implements AutofillServiceInterface {
private openPasswordRepromptPopoutDebounce: NodeJS.Timeout;
private currentlyOpeningPasswordRepromptPopout = false;
private autofillScriptPortsSet = new Set<chrome.runtime.Port>();
static searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);
constructor(
private cipherService: CipherService,
@ -54,6 +56,7 @@ export default class AutofillService implements AutofillServiceInterface {
private logService: LogService,
private domainSettingsService: DomainSettingsService,
private userVerificationService: UserVerificationService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {}
/**
@ -239,7 +242,9 @@ export default class AutofillService implements AutofillServiceInterface {
let totp: string | null = null;
const canAccessPremium = await this.stateService.getCanAccessPremium();
const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
);
const defaultUriMatch = await this.getDefaultUriMatchStrategy();
if (!canAccessPremium) {
@ -1380,11 +1385,33 @@ export default class AutofillService implements AutofillServiceInterface {
return excludedTypes.indexOf(type) > -1;
}
/**
* Identifies if a passed field contains text artifacts that identify it as a search field.
*
* @param field - The autofill field that we are validating as a search field
*/
private static isSearchField(field: AutofillField) {
const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
const matchPattern = new RegExp(AutoFillConstants.SearchFieldNames.join("|"), "gi");
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
if (!matchFieldAttributeValues[attrIndex]) {
continue;
}
return Boolean(matchFieldAttributeValues.join(" ").match(matchPattern));
// Separate camel case words and case them to lower case values
const camelCaseSeparatedFieldAttribute = matchFieldAttributeValues[attrIndex]
.replace(/([a-z])([A-Z])/g, "$1 $2")
.toLowerCase();
// Split the attribute by non-alphabetical characters to get the keywords
const attributeKeywords = camelCaseSeparatedFieldAttribute.split(/[^a-z]/gi);
for (let keywordIndex = 0; keywordIndex < attributeKeywords.length; keywordIndex++) {
if (AutofillService.searchFieldNamesSet.has(attributeKeywords[keywordIndex])) {
return true;
}
}
}
return false;
}
static isExcludedFieldType(field: AutofillField, excludedTypes: string[]) {
@ -1397,11 +1424,7 @@ export default class AutofillService implements AutofillServiceInterface {
}
// Check if the input is an untyped/mistyped search input
if (this.isSearchField(field)) {
return true;
}
return false;
return this.isSearchField(field);
}
/**
@ -1525,11 +1548,7 @@ export default class AutofillService implements AutofillServiceInterface {
return false;
}
if (AutoFillConstants.PasswordFieldExcludeList.some((i) => cleanedValue.indexOf(i) > -1)) {
return false;
}
return true;
return !AutoFillConstants.PasswordFieldExcludeList.some((i) => cleanedValue.indexOf(i) > -1);
}
static fieldHasDisqualifyingAttributeValue(field: AutofillField) {
@ -1572,7 +1591,11 @@ export default class AutofillService implements AutofillServiceInterface {
const arr: AutofillField[] = [];
pageDetails.fields.forEach((f) => {
if (AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillLoginTypes)) {
const isPassword = f.type === "password";
if (
!isPassword &&
AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillLoginTypes)
) {
return;
}
@ -1581,23 +1604,16 @@ export default class AutofillService implements AutofillServiceInterface {
return;
}
const isPassword = f.type === "password";
const isLikePassword = () => {
if (f.type !== "text") {
return false;
}
if (AutofillService.valueIsLikePassword(f.htmlID)) {
return true;
}
if (AutofillService.valueIsLikePassword(f.htmlName)) {
return true;
}
if (AutofillService.valueIsLikePassword(f.placeholder)) {
return true;
const testedValues = [f.htmlID, f.htmlName, f.placeholder];
for (let i = 0; i < testedValues.length; i++) {
if (AutofillService.valueIsLikePassword(testedValues[i])) {
return true;
}
}
return false;

View File

@ -755,6 +755,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
// Prioritize capturing text content from elements rather than nodes.
currentElement = currentElement.parentElement || currentElement.parentNode;
if (!currentElement) {
return textContentItems;
}
let siblingElement = nodeIsElement(currentElement)
? currentElement.previousElementSibling

View File

@ -553,17 +553,30 @@ describe("InsertAutofillContentService", () => {
insertAutofillContentService as any,
"simulateUserMouseClickAndFocusEventInteractions",
);
jest.spyOn(targetInput, "blur");
insertAutofillContentService["handleFocusOnFieldByOpidAction"]("__0");
expect(
insertAutofillContentService["collectAutofillContentService"].getAutofillFieldElementByOpid,
).toBeCalledWith("__0");
expect(targetInput.blur).not.toHaveBeenCalled();
expect(
insertAutofillContentService["simulateUserMouseClickAndFocusEventInteractions"],
).toHaveBeenCalledWith(targetInput, true);
expect(elementEventCount).toEqual(expectedElementEventCount);
});
it("blurs the element if it is currently the active element before simulating click and focus events", () => {
const targetInput = document.querySelector('input[type="text"]') as FormElementWithAttribute;
targetInput.opid = "__0";
targetInput.focus();
jest.spyOn(targetInput, "blur");
insertAutofillContentService["handleFocusOnFieldByOpidAction"]("__0");
expect(targetInput.blur).toHaveBeenCalled();
});
});
describe("insertValueIntoField", () => {
@ -710,7 +723,7 @@ describe("InsertAutofillContentService", () => {
});
describe("triggerPostInsertEventsOnElement", () => {
it("triggers simulated event interactions and blurs the element after", () => {
it("triggers simulated event interactions", () => {
const elementValue = "test";
document.body.innerHTML = `<input type="text" id="username" value="${elementValue}"/>`;
const element = document.getElementById("username") as FillableFormFieldElement;
@ -726,7 +739,6 @@ describe("InsertAutofillContentService", () => {
expect(insertAutofillContentService["simulateInputElementChangedEvent"]).toHaveBeenCalledWith(
element,
);
expect(element.blur).toHaveBeenCalled();
expect(element.value).toBe(elementValue);
});
});

View File

@ -185,11 +185,18 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
/**
* Handles finding an element by opid and triggering click and focus events on the element.
* @param {string} opid
* @private
* To ensure that we trigger a blur event correctly on a filled field, we first check if the
* element is already focused. If it is, we blur the element before focusing on it again.
*
* @param {string} opid - The opid of the element to focus on.
*/
private handleFocusOnFieldByOpidAction(opid: string) {
const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid);
if (document.activeElement === element) {
element.blur();
}
this.simulateUserMouseClickAndFocusEventInteractions(element, true);
}
@ -282,7 +289,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
}
this.simulateInputElementChangedEvent(element);
element.blur();
}
/**
@ -379,10 +385,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
element.dispatchEvent(new Event(simulatedInputEvents[index], { bubbles: true }));
}
}
private nodeIsElement(node: Node): node is HTMLElement {
return node.nodeType === Node.ELEMENT_NODE;
}
}
export default InsertAutofillContentService;

View File

@ -8,19 +8,18 @@ import {
AuthRequestServiceAbstraction,
AuthRequestService,
} from "@bitwarden/auth/common";
import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service";
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
@ -39,6 +38,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
@ -64,6 +64,8 @@ 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 { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -117,7 +119,6 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
/* eslint-enable import/no-restricted-paths */
import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
import { ApiService } from "@bitwarden/common/services/api.service";
import { AuditService } from "@bitwarden/common/services/audit.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
@ -125,6 +126,7 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa
import { NotificationsService } from "@bitwarden/common/services/notifications.service";
import { SearchService } from "@bitwarden/common/services/search.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/avatar.service";
import {
PasswordGenerationService,
PasswordGenerationServiceAbstraction,
@ -180,7 +182,6 @@ import {
VaultExportServiceAbstraction,
} from "@bitwarden/vault-export-core";
import { BrowserOrganizationService } from "../admin-console/services/browser-organization.service";
import ContextMenusBackground from "../autofill/background/context-menus.background";
import NotificationBackground from "../autofill/background/notification.background";
import OverlayBackground from "../autofill/background/overlay.background";
@ -211,7 +212,6 @@ import { BrowserPlatformUtilsService } from "../platform/services/platform-utils
import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider";
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
import { BrowserSendService } from "../services/browser-send.service";
import { BrowserSettingsService } from "../services/browser-settings.service";
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
import FilelessImporterBackground from "../tools/background/fileless-importer.background";
import { BrowserFido2UserInterfaceService } from "../vault/fido2/browser-fido2-user-interface.service";
@ -240,7 +240,6 @@ export default class MainBackground {
appIdService: AppIdServiceAbstraction;
apiService: ApiServiceAbstraction;
environmentService: BrowserEnvironmentService;
settingsService: SettingsServiceAbstraction;
cipherService: CipherServiceAbstraction;
folderService: InternalFolderServiceAbstraction;
collectionService: CollectionServiceAbstraction;
@ -288,7 +287,7 @@ export default class MainBackground {
fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction;
fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction;
fido2ClientService: Fido2ClientServiceAbstraction;
avatarUpdateService: AvatarUpdateServiceAbstraction;
avatarService: AvatarServiceAbstraction;
mainContextMenuHandler: MainContextMenuHandler;
cipherContextMenuHandler: CipherContextMenuHandler;
configService: BrowserConfigService;
@ -311,6 +310,7 @@ export default class MainBackground {
biometricStateService: BiometricStateService;
stateEventRunnerService: StateEventRunnerService;
ssoLoginService: SsoLoginServiceAbstraction;
billingAccountProfileStateService: BillingAccountProfileStateService;
onUpdatedRan: boolean;
onReplacedRan: boolean;
@ -409,8 +409,7 @@ export default class MainBackground {
);
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService,
storageServiceProvider,
stateEventRegistrarService,
this.singleUserStateProvider,
);
this.derivedStateProvider = new BackgroundDerivedStateProvider(
this.memoryStorageForStateProviders,
@ -428,6 +427,21 @@ export default class MainBackground {
);
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider);
this.platformUtilsService = new BackgroundPlatformUtilsService(
this.messagingService,
(clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs),
async () => this.biometricUnlock(),
self,
);
this.tokenService = new TokenService(
this.singleUserStateProvider,
this.globalStateProvider,
this.platformUtilsService.supportsSecureStorage(),
this.secureStorageService,
);
const migrationRunner = new MigrationRunner(
this.storageService,
this.logService,
@ -442,15 +456,9 @@ export default class MainBackground {
new StateFactory(GlobalState, Account),
this.accountService,
this.environmentService,
this.tokenService,
migrationRunner,
);
this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider);
this.platformUtilsService = new BackgroundPlatformUtilsService(
this.messagingService,
(clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs),
async () => this.biometricUnlock(),
self,
);
const themeStateService = new DefaultThemeStateService(this.globalStateProvider);
@ -466,17 +474,17 @@ export default class MainBackground {
this.stateProvider,
this.biometricStateService,
);
this.tokenService = new TokenService(this.stateService);
this.appIdService = new AppIdService(this.globalStateProvider);
this.apiService = new ApiService(
this.tokenService,
this.platformUtilsService,
this.environmentService,
this.appIdService,
this.stateService,
(expired: boolean) => this.logout(expired),
);
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
this.settingsService = new BrowserSettingsService(this.stateService);
this.fileUploadService = new FileUploadService(this.logService);
this.cipherFileUploadService = new CipherFileUploadService(
this.apiService,
@ -490,10 +498,7 @@ export default class MainBackground {
this.stateProvider,
);
this.syncNotifierService = new SyncNotifierService();
this.organizationService = new BrowserOrganizationService(
this.stateService,
this.stateProvider,
);
this.organizationService = new OrganizationService(this.stateProvider);
this.policyService = new PolicyService(this.stateProvider, this.organizationService);
this.autofillSettingsService = new AutofillSettingsService(
this.stateProvider,
@ -563,6 +568,10 @@ export default class MainBackground {
this.stateService,
);
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
this.activeUserStateProvider,
);
this.loginStrategyService = new LoginStrategyService(
this.cryptoService,
this.apiService,
@ -582,6 +591,7 @@ export default class MainBackground {
this.deviceTrustCryptoService,
this.authRequestService,
this.globalStateProvider,
this.billingAccountProfileStateService,
);
this.ssoLoginService = new SsoLoginService(this.stateProvider);
@ -685,7 +695,11 @@ export default class MainBackground {
this.fileUploadService,
this.sendService,
);
this.avatarService = new AvatarService(this.apiService, this.stateProvider);
this.providerService = new ProviderService(this.stateProvider);
this.syncService = new SyncService(
this.apiService,
this.domainSettingsService,
@ -703,18 +717,22 @@ export default class MainBackground {
this.folderApiService,
this.organizationService,
this.sendApiService,
this.avatarService,
logoutCallback,
this.billingAccountProfileStateService,
);
this.eventUploadService = new EventUploadService(
this.apiService,
this.stateService,
this.stateProvider,
this.logService,
this.accountService,
);
this.eventCollectionService = new EventCollectionService(
this.cipherService,
this.stateService,
this.stateProvider,
this.organizationService,
this.eventUploadService,
this.accountService,
);
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
@ -727,6 +745,7 @@ export default class MainBackground {
this.logService,
this.domainSettingsService,
this.userVerificationService,
this.billingAccountProfileStateService,
);
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
@ -787,7 +806,6 @@ export default class MainBackground {
this.fido2AuthenticatorService,
this.configService,
this.authService,
this.stateService,
this.vaultSettingsService,
this.domainSettingsService,
this.logService,
@ -835,7 +853,6 @@ export default class MainBackground {
this.cryptoService,
this.cryptoFunctionService,
this.runtimeBackground,
this.i18nService,
this.messagingService,
this.appIdService,
this.platformUtilsService,
@ -869,7 +886,7 @@ export default class MainBackground {
this.autofillService,
this.authService,
this.environmentService,
this.settingsService,
this.domainSettingsService,
this.stateService,
this.autofillSettingsService,
this.i18nService,
@ -943,14 +960,13 @@ export default class MainBackground {
this.apiService,
);
this.avatarUpdateService = new AvatarUpdateService(this.apiService, this.stateService);
if (!this.popupOnlyContext) {
this.mainContextMenuHandler = new MainContextMenuHandler(
this.stateService,
this.autofillSettingsService,
this.i18nService,
this.logService,
this.billingAccountProfileStateService,
);
this.cipherContextMenuHandler = new CipherContextMenuHandler(
@ -1090,7 +1106,7 @@ export default class MainBackground {
async logout(expired: boolean, userId?: UserId) {
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.eventUploadService.uploadEvents(userId);
await this.eventUploadService.uploadEvents(userId as UserId);
await Promise.all([
this.syncService.setLastSync(new Date(0), userId),

View File

@ -5,7 +5,6 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -75,7 +74,6 @@ export class NativeMessagingBackground {
private cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService,
private runtimeBackground: RuntimeBackground,
private i18nService: I18nService,
private messagingService: MessagingService,
private appIdService: AppIdService,
private platformUtilsService: PlatformUtilsService,

View File

@ -97,6 +97,10 @@ export default class RuntimeBackground {
case "unlocked": {
let item: LockedVaultPendingNotificationsData;
if (msg.command === "loggedIn") {
await this.sendBwInstalledMessageToVault();
}
if (this.lockedVaultPendingNotifications?.length > 0) {
item = this.lockedVaultPendingNotifications.pop();
await closeUnlockPopout();
@ -130,9 +134,6 @@ export default class RuntimeBackground {
await this.main.refreshBadge();
await this.main.refreshMenu();
}, 2000);
// 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.main.avatarUpdateService.loadColorFromState();
this.configService.triggerServerConfigFetch();
}
break;
@ -354,8 +355,6 @@ export default class RuntimeBackground {
if (await this.environmentService.hasManagedEnvironment()) {
await this.environmentService.setUrlsToManagedEnvironment();
}
await this.sendBwInstalledMessageToVault();
}
this.onInstalledReason = null;

View File

@ -5,15 +5,14 @@ import {
organizationServiceFactory,
OrganizationServiceInitOptions,
} from "../../admin-console/background/service-factories/organization-service.factory";
import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory";
import {
FactoryOptions,
CachedServices,
factory,
} from "../../platform/background/service-factories/factory-options";
import {
stateServiceFactory,
StateServiceInitOptions,
} from "../../platform/background/service-factories/state-service.factory";
import { stateProviderFactory } from "../../platform/background/service-factories/state-provider.factory";
import { StateServiceInitOptions } from "../../platform/background/service-factories/state-service.factory";
import {
cipherServiceFactory,
CipherServiceInitOptions,
@ -43,9 +42,10 @@ export function eventCollectionServiceFactory(
async () =>
new EventCollectionService(
await cipherServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
await organizationServiceFactory(cache, opts),
await eventUploadServiceFactory(cache, opts),
await accountServiceFactory(cache, opts),
),
);
}

View File

@ -1,6 +1,7 @@
import { EventUploadService as AbstractEventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
import { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory";
import {
ApiServiceInitOptions,
apiServiceFactory,
@ -14,10 +15,8 @@ import {
logServiceFactory,
LogServiceInitOptions,
} from "../../platform/background/service-factories/log-service.factory";
import {
stateServiceFactory,
StateServiceInitOptions,
} from "../../platform/background/service-factories/state-service.factory";
import { stateProviderFactory } from "../../platform/background/service-factories/state-provider.factory";
import { StateServiceInitOptions } from "../../platform/background/service-factories/state-service.factory";
type EventUploadServiceOptions = FactoryOptions;
@ -37,8 +36,9 @@ export function eventUploadServiceFactory(
async () =>
new EventUploadService(
await apiServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await stateProviderFactory(cache, opts),
await logServiceFactory(cache, opts),
await accountServiceFactory(cache, opts),
),
);
}

View File

@ -1,28 +0,0 @@
import { SettingsService as AbstractSettingsService } from "@bitwarden/common/abstractions/settings.service";
import {
FactoryOptions,
CachedServices,
factory,
} from "../../platform/background/service-factories/factory-options";
import {
stateServiceFactory,
StateServiceInitOptions,
} from "../../platform/background/service-factories/state-service.factory";
import { BrowserSettingsService } from "../../services/browser-settings.service";
type SettingsServiceFactoryOptions = FactoryOptions;
export type SettingsServiceInitOptions = SettingsServiceFactoryOptions & StateServiceInitOptions;
export function settingsServiceFactory(
cache: { settingsService?: AbstractSettingsService } & CachedServices,
opts: SettingsServiceInitOptions,
): Promise<AbstractSettingsService> {
return factory(
cache,
"settingsService",
opts,
async () => new BrowserSettingsService(await stateServiceFactory(cache, opts)),
);
}

View File

@ -9,20 +9,15 @@ import {
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
StateEventRegistrarServiceInitOptions,
stateEventRegistrarServiceFactory,
} from "./state-event-registrar-service.factory";
import {
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
SingleUserStateProviderInitOptions,
singleUserStateProviderFactory,
} from "./single-user-state-provider.factory";
type ActiveUserStateProviderFactory = FactoryOptions;
export type ActiveUserStateProviderInitOptions = ActiveUserStateProviderFactory &
AccountServiceInitOptions &
StorageServiceProviderInitOptions &
StateEventRegistrarServiceInitOptions;
SingleUserStateProviderInitOptions;
export async function activeUserStateProviderFactory(
cache: { activeUserStateProvider?: ActiveUserStateProvider } & CachedServices,
@ -35,8 +30,7 @@ export async function activeUserStateProviderFactory(
async () =>
new DefaultActiveUserStateProvider(
await accountServiceFactory(cache, opts),
await storageServiceProviderFactory(cache, opts),
await stateEventRegistrarServiceFactory(cache, opts),
await singleUserStateProviderFactory(cache, opts),
),
);
}

View File

@ -20,6 +20,7 @@ import {
PlatformUtilsServiceInitOptions,
platformUtilsServiceFactory,
} from "./platform-utils-service.factory";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
type ApiServiceFactoryOptions = FactoryOptions & {
apiServiceOptions: {
@ -32,7 +33,8 @@ export type ApiServiceInitOptions = ApiServiceFactoryOptions &
TokenServiceInitOptions &
PlatformUtilsServiceInitOptions &
EnvironmentServiceInitOptions &
AppIdServiceInitOptions;
AppIdServiceInitOptions &
StateServiceInitOptions;
export function apiServiceFactory(
cache: { apiService?: AbstractApiService } & CachedServices,
@ -48,6 +50,7 @@ export function apiServiceFactory(
await platformUtilsServiceFactory(cache, opts),
await environmentServiceFactory(cache, opts),
await appIdServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
opts.apiServiceOptions.logoutCallback,
opts.apiServiceOptions.customUserAgent,
),

View File

@ -0,0 +1,28 @@
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
import { activeUserStateProviderFactory } from "./active-user-state-provider.factory";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
import { StateProviderInitOptions } from "./state-provider.factory";
type BillingAccountProfileStateServiceFactoryOptions = FactoryOptions;
export type BillingAccountProfileStateServiceInitOptions =
BillingAccountProfileStateServiceFactoryOptions & StateProviderInitOptions;
export function billingAccountProfileStateServiceFactory(
cache: {
billingAccountProfileStateService?: BillingAccountProfileStateService;
} & CachedServices,
opts: BillingAccountProfileStateServiceInitOptions,
): Promise<BillingAccountProfileStateService> {
return factory(
cache,
"billingAccountProfileStateService",
opts,
async () =>
new DefaultBillingAccountProfileStateService(
await activeUserStateProviderFactory(cache, opts),
),
);
}

View File

@ -5,6 +5,10 @@ import {
accountServiceFactory,
AccountServiceInitOptions,
} from "../../../auth/background/service-factories/account-service.factory";
import {
tokenServiceFactory,
TokenServiceInitOptions,
} from "../../../auth/background/service-factories/token-service.factory";
import { Account } from "../../../models/account";
import { BrowserStateService } from "../../services/browser-state.service";
@ -38,6 +42,7 @@ export type StateServiceInitOptions = StateServiceFactoryOptions &
LogServiceInitOptions &
AccountServiceInitOptions &
EnvironmentServiceInitOptions &
TokenServiceInitOptions &
MigrationRunnerInitOptions;
export async function stateServiceFactory(
@ -57,6 +62,7 @@ export async function stateServiceFactory(
opts.stateServiceOptions.stateFactory,
await accountServiceFactory(cache, opts),
await environmentServiceFactory(cache, opts),
await tokenServiceFactory(cache, opts),
await migrationRunnerFactory(cache, opts),
opts.stateServiceOptions.useAccountCache,
),

View File

@ -7,6 +7,7 @@ import localeBn from "@angular/common/locales/bn";
import localeBs from "@angular/common/locales/bs";
import localeCa from "@angular/common/locales/ca";
import localeCs from "@angular/common/locales/cs";
import localeCy from "@angular/common/locales/cy";
import localeDa from "@angular/common/locales/da";
import localeDe from "@angular/common/locales/de";
import localeEl from "@angular/common/locales/el";
@ -19,6 +20,7 @@ import localeFa from "@angular/common/locales/fa";
import localeFi from "@angular/common/locales/fi";
import localeFil from "@angular/common/locales/fil";
import localeFr from "@angular/common/locales/fr";
import localeGl from "@angular/common/locales/gl";
import localeHe from "@angular/common/locales/he";
import localeHi from "@angular/common/locales/hi";
import localeHr from "@angular/common/locales/hr";
@ -33,9 +35,13 @@ import localeKo from "@angular/common/locales/ko";
import localeLt from "@angular/common/locales/lt";
import localeLv from "@angular/common/locales/lv";
import localeMl from "@angular/common/locales/ml";
import localeMr from "@angular/common/locales/mr";
import localeMy from "@angular/common/locales/my";
import localeNb from "@angular/common/locales/nb";
import localeNe from "@angular/common/locales/ne";
import localeNl from "@angular/common/locales/nl";
import localeNn from "@angular/common/locales/nn";
import localeOr from "@angular/common/locales/or";
import localePl from "@angular/common/locales/pl";
import localePtBr from "@angular/common/locales/pt";
import localePtPt from "@angular/common/locales/pt-PT";
@ -46,6 +52,7 @@ import localeSk from "@angular/common/locales/sk";
import localeSl from "@angular/common/locales/sl";
import localeSr from "@angular/common/locales/sr";
import localeSv from "@angular/common/locales/sv";
import localeTe from "@angular/common/locales/te";
import localeTh from "@angular/common/locales/th";
import localeTr from "@angular/common/locales/tr";
import localeUk from "@angular/common/locales/uk";
@ -61,6 +68,7 @@ registerLocaleData(localeBn, "bn");
registerLocaleData(localeBs, "bs");
registerLocaleData(localeCa, "ca");
registerLocaleData(localeCs, "cs");
registerLocaleData(localeCy, "cy");
registerLocaleData(localeDa, "da");
registerLocaleData(localeDe, "de");
registerLocaleData(localeEl, "el");
@ -73,6 +81,7 @@ registerLocaleData(localeFa, "fa");
registerLocaleData(localeFi, "fi");
registerLocaleData(localeFil, "fil");
registerLocaleData(localeFr, "fr");
registerLocaleData(localeGl, "gl");
registerLocaleData(localeHe, "he");
registerLocaleData(localeHi, "hi");
registerLocaleData(localeHr, "hr");
@ -87,9 +96,13 @@ registerLocaleData(localeKo, "ko");
registerLocaleData(localeLt, "lt");
registerLocaleData(localeLv, "lv");
registerLocaleData(localeMl, "ml");
registerLocaleData(localeMr, "mr");
registerLocaleData(localeMy, "my");
registerLocaleData(localeNb, "nb");
registerLocaleData(localeNe, "ne");
registerLocaleData(localeNl, "nl");
registerLocaleData(localeNn, "nn");
registerLocaleData(localeOr, "or");
registerLocaleData(localePl, "pl");
registerLocaleData(localePtBr, "pt-BR");
registerLocaleData(localePtPt, "pt-PT");
@ -100,6 +113,7 @@ registerLocaleData(localeSk, "sk");
registerLocaleData(localeSl, "sl");
registerLocaleData(localeSr, "sr");
registerLocaleData(localeSv, "sv");
registerLocaleData(localeTe, "te");
registerLocaleData(localeTh, "th");
registerLocaleData(localeTr, "tr");
registerLocaleData(localeUk, "uk");

View File

@ -1,5 +1,6 @@
import { mock, MockProxy } from "jest-mock-extended";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
@ -32,6 +33,7 @@ describe("Browser State Service", () => {
let stateFactory: MockProxy<StateFactory<GlobalState, Account>>;
let useAccountCache: boolean;
let environmentService: MockProxy<EnvironmentService>;
let tokenService: MockProxy<TokenService>;
let migrationRunner: MockProxy<MigrationRunner>;
let state: State<GlobalState, Account>;
@ -46,6 +48,7 @@ describe("Browser State Service", () => {
logService = mock();
stateFactory = mock();
environmentService = mock();
tokenService = mock();
migrationRunner = mock();
// turn off account cache for tests
useAccountCache = false;
@ -77,6 +80,7 @@ describe("Browser State Service", () => {
stateFactory,
accountService,
environmentService,
tokenService,
migrationRunner,
useAccountCache,
);

View File

@ -1,6 +1,7 @@
import { BehaviorSubject } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
@ -45,6 +46,7 @@ export class BrowserStateService
stateFactory: StateFactory<GlobalState, Account>,
accountService: AccountService,
environmentService: EnvironmentService,
tokenService: TokenService,
migrationRunner: MigrationRunner,
useAccountCache = true,
) {
@ -56,6 +58,7 @@ export class BrowserStateService
stateFactory,
accountService,
environmentService,
tokenService,
migrationRunner,
useAccountCache,
);

View File

@ -17,11 +17,8 @@ import {
LoginStrategyServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@ -42,6 +39,10 @@ import {
AutofillSettingsService,
AutofillSettingsServiceAbstraction,
} from "@bitwarden/common/autofill/services/autofill-settings.service";
import {
DefaultDomainSettingsService,
DomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import {
UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction,
@ -98,7 +99,6 @@ import { DialogService } from "@bitwarden/components";
import { ImportServiceAbstraction } from "@bitwarden/importer/core";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { BrowserOrganizationService } from "../../admin-console/services/browser-organization.service";
import { UnauthGuardService } from "../../auth/popup/services";
import { AutofillService } from "../../autofill/services/abstractions/autofill.service";
import MainBackground from "../../background/main.background";
@ -118,7 +118,6 @@ 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 { BrowserSendService } from "../../services/browser-send.service";
import { BrowserSettingsService } from "../../services/browser-settings.service";
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
import { VaultFilterService } from "../../vault/services/vault-filter.service";
@ -233,7 +232,6 @@ function getBgService<T>(service: keyof MainBackground) {
deps: [],
},
{ provide: TotpService, useFactory: getBgService<TotpService>("totpService"), deps: [] },
{ provide: TokenService, useFactory: getBgService<TokenService>("tokenService"), deps: [] },
{
provide: I18nServiceAbstraction,
useFactory: (globalStateProvider: GlobalStateProvider) => {
@ -265,16 +263,6 @@ function getBgService<T>(service: keyof MainBackground) {
useFactory: getBgService<DevicesServiceAbstraction>("devicesService"),
deps: [],
},
{
provide: EventUploadService,
useFactory: getBgService<EventUploadService>("eventUploadService"),
deps: [],
},
{
provide: EventCollectionService,
useFactory: getBgService<EventCollectionService>("eventCollectionService"),
deps: [],
},
{
provide: PlatformUtilsService,
useExisting: ForegroundPlatformUtilsService,
@ -348,11 +336,9 @@ function getBgService<T>(service: keyof MainBackground) {
},
{ provide: SyncService, useFactory: getBgService<SyncService>("syncService"), deps: [] },
{
provide: SettingsService,
useFactory: (stateService: StateServiceAbstraction) => {
return new BrowserSettingsService(stateService);
},
deps: [StateServiceAbstraction],
provide: DomainSettingsService,
useClass: DefaultDomainSettingsService,
deps: [StateProvider],
},
{
provide: AbstractStorageService,
@ -399,13 +385,6 @@ function getBgService<T>(service: keyof MainBackground) {
useFactory: getBgService<NotificationsService>("notificationsService"),
deps: [],
},
{
provide: OrganizationService,
useFactory: (stateService: StateServiceAbstraction, stateProvider: StateProvider) => {
return new BrowserOrganizationService(stateService, stateProvider);
},
deps: [StateServiceAbstraction, StateProvider],
},
{
provide: VaultFilterService,
useClass: VaultFilterService,
@ -445,6 +424,7 @@ function getBgService<T>(service: keyof MainBackground) {
logService: LogServiceAbstraction,
accountService: AccountServiceAbstraction,
environmentService: EnvironmentService,
tokenService: TokenService,
migrationRunner: MigrationRunner,
) => {
return new BrowserStateService(
@ -455,6 +435,7 @@ function getBgService<T>(service: keyof MainBackground) {
new StateFactory(GlobalState, Account),
accountService,
environmentService,
tokenService,
migrationRunner,
);
},
@ -465,6 +446,7 @@ function getBgService<T>(service: keyof MainBackground) {
LogServiceAbstraction,
AccountServiceAbstraction,
EnvironmentService,
TokenService,
MigrationRunner,
],
},

View File

@ -6,7 +6,6 @@ import { DomainSettingsService } from "@bitwarden/common/autofill/services/domai
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { BrowserApi } from "../../platform/browser/browser-api";
@ -31,7 +30,6 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
accountSwitcherEnabled = false;
constructor(
private stateService: StateService,
private domainSettingsService: DomainSettingsService,
private i18nService: I18nService,
private router: Router,

View File

@ -1,7 +1,6 @@
import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
@ -13,7 +12,6 @@ import {
} from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
@ -51,14 +49,12 @@ export class OptionsComponent implements OnInit {
constructor(
private messagingService: MessagingService,
private stateService: StateService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private domainSettingsService: DomainSettingsService,
private badgeSettingsService: BadgeSettingsServiceAbstraction,
i18nService: I18nService,
private themeStateService: ThemeStateService,
private settingsService: SettingsService,
private vaultSettingsService: VaultSettingsService,
) {
this.themeOptions = [
@ -114,12 +110,14 @@ export class OptionsComponent implements OnInit {
this.autofillSettingsService.enableContextMenu$,
);
this.showCardsCurrentTab = !(await this.stateService.getDontShowCardsCurrentTab());
this.showIdentitiesCurrentTab = !(await this.stateService.getDontShowIdentitiesCurrentTab());
this.showCardsCurrentTab = await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$);
this.showIdentitiesCurrentTab = await firstValueFrom(
this.vaultSettingsService.showIdentitiesCurrentTab$,
);
this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$);
this.enableFavicon = !this.settingsService.getDisableFavicon();
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
@ -169,7 +167,7 @@ export class OptionsComponent implements OnInit {
}
async updateFavicon() {
await this.settingsService.setDisableFavicon(!this.enableFavicon);
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
}
async updateBadgeCounter() {
@ -178,11 +176,11 @@ export class OptionsComponent implements OnInit {
}
async updateShowCardsCurrentTab() {
await this.stateService.setDontShowCardsCurrentTab(!this.showCardsCurrentTab);
await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab);
}
async updateShowIdentitiesCurrentTab() {
await this.stateService.setDontShowIdentitiesCurrentTab(!this.showIdentitiesCurrentTab);
await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab);
}
async saveTheme() {

View File

@ -12,7 +12,7 @@
</header>
<main tabindex="-1">
<div class="content">
<ng-container *ngIf="!isPremium">
<ng-container *ngIf="!(isPremium$ | async)">
<p class="text-center lead">{{ "premiumNotCurrentMember" | i18n }}</p>
<p>{{ "premiumSignUpAndGet" | i18n }}</p>
<ul class="bwi-ul">
@ -61,7 +61,7 @@
></i>
</button>
</ng-container>
<ng-container *ngIf="isPremium">
<ng-container *ngIf="isPremium$ | async">
<p class="text-center lead">{{ "premiumCurrentMember" | i18n }}</p>
<p class="text-center">{{ "premiumCurrentMemberThanks" | i18n }}</p>
<button type="button" class="btn block primary" (click)="manage()">

View File

@ -3,6 +3,7 @@ import { Component } from "@angular/core";
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -27,6 +28,7 @@ export class PremiumComponent extends BasePremiumComponent {
private currencyPipe: CurrencyPipe,
dialogService: DialogService,
environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
i18nService,
@ -36,6 +38,7 @@ export class PremiumComponent extends BasePremiumComponent {
stateService,
dialogService,
environmentService,
billingAccountProfileStateService,
);
// Support old price string. Can be removed in future once all translations are properly updated.

View File

@ -16,7 +16,6 @@ import {
takeUntil,
} from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
@ -98,7 +97,6 @@ export class SettingsComponent implements OnInit {
private environmentService: EnvironmentService,
private cryptoService: CryptoService,
private stateService: StateService,
private modalService: ModalService,
private userVerificationService: UserVerificationService,
private dialogService: DialogService,
private changeDetectorRef: ChangeDetectorRef,

View File

@ -1,11 +0,0 @@
import { BehaviorSubject } from "rxjs";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable";
@browserSession
export class BrowserSettingsService extends SettingsService {
@sessionSync({ initializer: (b: boolean) => b })
protected _disableFavicon: BehaviorSubject<boolean>;
}

View File

@ -6,6 +6,7 @@ import { first } from "rxjs/operators";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -49,6 +50,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
dialogService: DialogService,
formBuilder: FormBuilder,
private filePopoutUtilsService: FilePopoutUtilsService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
i18nService,
@ -63,6 +65,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
sendApiService,
dialogService,
formBuilder,
billingAccountProfileStateService,
);
}

View File

@ -1,10 +1,11 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
@ -15,7 +16,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
selector: "app-action-buttons",
templateUrl: "action-buttons.component.html",
})
export class ActionButtonsComponent {
export class ActionButtonsComponent implements OnInit, OnDestroy {
@Output() onView = new EventEmitter<CipherView>();
@Output() launchEvent = new EventEmitter<CipherView>();
@Input() cipher: CipherView;
@ -24,17 +25,28 @@ export class ActionButtonsComponent {
cipherType = CipherType;
userHasPremiumAccess = false;
private componentIsDestroyed$ = new Subject<boolean>();
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private eventCollectionService: EventCollectionService,
private totpService: TotpService,
private stateService: StateService,
private passwordRepromptService: PasswordRepromptService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {}
async ngOnInit() {
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium();
ngOnInit() {
this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.componentIsDestroyed$))
.subscribe((canAccessPremium: boolean) => {
this.userHasPremiumAccess = canAccessPremium;
});
}
ngOnDestroy() {
this.componentIsDestroyed$.next(true);
this.componentIsDestroyed$.complete();
}
launchCipher() {

View File

@ -6,7 +6,6 @@ import { firstValueFrom } from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { BrowserFido2UserInterfaceSession } from "../../../fido2/browser-fido2-user-interface.service";
@ -52,7 +51,6 @@ export class Fido2UseBrowserLinkComponent {
protected fido2PopoutSessionData$ = fido2PopoutSessionData$();
constructor(
private stateService: StateService,
private domainSettingsService: DomainSettingsService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,

View File

@ -5,6 +5,7 @@ import { first } from "rxjs/operators";
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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";
@ -34,6 +35,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
logService: LogService,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
cipherService,
@ -46,6 +48,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
stateService,
fileDownloadService,
dialogService,
billingAccountProfileStateService,
);
}

View File

@ -10,10 +10,10 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -65,11 +65,11 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
private changeDetectorRef: ChangeDetectorRef,
private syncService: SyncService,
private searchService: SearchService,
private stateService: StateService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private passwordRepromptService: PasswordRepromptService,
private organizationService: OrganizationService,
private vaultFilterService: VaultFilterService,
private vaultSettingsService: VaultSettingsService,
) {}
async ngOnInit() {
@ -271,9 +271,11 @@ export class CurrentTabComponent implements OnInit, OnDestroy {
});
const otherTypes: CipherType[] = [];
const dontShowCards = await this.stateService.getDontShowCardsCurrentTab();
const dontShowIdentities = await this.stateService.getDontShowIdentitiesCurrentTab();
this.showOrganizations = this.organizationService.hasOrganizations();
const dontShowCards = !(await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$));
const dontShowIdentities = !(await firstValueFrom(
this.vaultSettingsService.showIdentitiesCurrentTab$,
));
this.showOrganizations = await this.organizationService.hasOrganizations();
if (!dontShowCards) {
otherTypes.push(CipherType.Card);
}

View File

@ -74,7 +74,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
async ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari();
this.showOrganizations = this.organizationService.hasOrganizations();
this.showOrganizations = await this.organizationService.hasOrganizations();
this.vaultFilter = this.vaultFilterService.getVaultFilter();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {

View File

@ -9,6 +9,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@ -95,6 +96,7 @@ export class ViewComponent extends BaseViewComponent {
fileDownloadService: FileDownloadService,
dialogService: DialogService,
datePipe: DatePipe,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
cipherService,
@ -117,6 +119,7 @@ export class ViewComponent extends BaseViewComponent {
fileDownloadService,
dialogService,
datePipe,
billingAccountProfileStateService,
);
}

View File

@ -203,6 +203,7 @@ export class LoginCommand {
ssoCodeVerifier,
this.ssoRedirectUri,
orgIdentifier,
undefined, // email to look up 2FA token not required as CLI can't remember 2FA token
twoFactor,
),
);

View File

@ -23,10 +23,12 @@ import { PolicyApiService } from "@bitwarden/common/admin-console/services/polic
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
@ -39,6 +41,8 @@ import {
DefaultDomainSettingsService,
DomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
import { ClientType } from "@bitwarden/common/enums";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
@ -86,7 +90,6 @@ import { AuditService } from "@bitwarden/common/services/audit.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
import { SearchService } from "@bitwarden/common/services/search.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
import {
@ -155,7 +158,6 @@ export class Main {
appIdService: AppIdService;
apiService: NodeApiService;
environmentService: EnvironmentService;
settingsService: SettingsService;
cipherService: CipherService;
folderService: InternalFolderService;
organizationUserService: OrganizationUserService;
@ -216,8 +218,10 @@ export class Main {
derivedStateProvider: DerivedStateProvider;
stateProvider: StateProvider;
loginStrategyService: LoginStrategyServiceAbstraction;
avatarService: AvatarServiceAbstraction;
stateEventRunnerService: StateEventRunnerService;
biometricStateService: BiometricStateService;
billingAccountProfileStateService: BillingAccountProfileStateService;
constructor() {
let p = null;
@ -291,8 +295,7 @@ export class Main {
this.activeUserStateProvider = new DefaultActiveUserStateProvider(
this.accountService,
storageServiceProvider,
stateEventRegistrarService,
this.singleUserStateProvider,
);
this.derivedStateProvider = new DefaultDerivedStateProvider(
@ -308,6 +311,13 @@ export class Main {
this.environmentService = new EnvironmentService(this.stateProvider, this.accountService);
this.tokenService = new TokenService(
this.singleUserStateProvider,
this.globalStateProvider,
this.platformUtilsService.supportsSecureStorage(),
this.secureStorageService,
);
const migrationRunner = new MigrationRunner(
this.storageService,
this.logService,
@ -322,6 +332,7 @@ export class Main {
new StateFactory(GlobalState, Account),
this.accountService,
this.environmentService,
this.tokenService,
migrationRunner,
);
@ -339,7 +350,6 @@ export class Main {
);
this.appIdService = new AppIdService(this.globalStateProvider);
this.tokenService = new TokenService(this.stateService);
const customUserAgent =
"Bitwarden_CLI/" +
@ -352,6 +362,7 @@ export class Main {
this.platformUtilsService,
this.environmentService,
this.appIdService,
this.stateService,
async (expired: boolean) => await this.logout(),
customUserAgent,
);
@ -362,7 +373,6 @@ export class Main {
this.containerService = new ContainerService(this.cryptoService, this.encryptService);
this.settingsService = new SettingsService(this.stateService);
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
this.fileUploadService = new FileUploadService(this.logService);
@ -397,7 +407,7 @@ export class Main {
this.providerService = new ProviderService(this.stateProvider);
this.organizationService = new OrganizationService(this.stateService, this.stateProvider);
this.organizationService = new OrganizationService(this.stateProvider);
this.organizationUserService = new OrganizationUserServiceImplementation(this.apiService);
@ -446,6 +456,10 @@ export class Main {
this.stateService,
);
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
this.activeUserStateProvider,
);
this.loginStrategyService = new LoginStrategyService(
this.cryptoService,
this.apiService,
@ -465,6 +479,7 @@ export class Main {
this.deviceTrustCryptoService,
this.authRequestService,
this.globalStateProvider,
this.billingAccountProfileStateService,
);
this.authService = new AuthService(
@ -555,6 +570,8 @@ export class Main {
null,
);
this.avatarService = new AvatarService(this.apiService, this.stateProvider);
this.syncService = new SyncService(
this.apiService,
this.domainSettingsService,
@ -572,7 +589,9 @@ export class Main {
this.folderApiService,
this.organizationService,
this.sendApiService,
this.avatarService,
async (expired: boolean) => await this.logout(),
this.billingAccountProfileStateService,
);
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
@ -619,15 +638,17 @@ export class Main {
this.eventUploadService = new EventUploadService(
this.apiService,
this.stateService,
this.stateProvider,
this.logService,
this.accountService,
);
this.eventCollectionService = new EventCollectionService(
this.cipherService,
this.stateService,
this.stateProvider,
this.organizationService,
this.eventUploadService,
this.accountService,
);
}
@ -651,6 +672,7 @@ export class Main {
});
const userId = await this.stateService.getUserId();
await Promise.all([
this.eventUploadService.uploadEvents(userId as UserId),
this.syncService.setLastSync(new Date(0)),
this.cryptoService.clearKeys(),
this.cipherService.clear(userId),

View File

@ -1,9 +1,12 @@
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { CardExport } from "@bitwarden/common/models/export/card.export";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
@ -57,6 +60,7 @@ export class GetCommand extends DownloadCommand {
private apiService: ApiService,
private organizationService: OrganizationService,
private eventCollectionService: EventCollectionService,
private accountProfileService: BillingAccountProfileStateService,
) {
super(cryptoService);
}
@ -251,7 +255,9 @@ export class GetCommand extends DownloadCommand {
return Response.error("Couldn't generate TOTP code.");
}
const canAccessPremium = await this.stateService.getCanAccessPremium();
const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$,
);
if (!canAccessPremium) {
const originalCipher = await this.cipherService.get(cipher.id);
if (
@ -334,7 +340,10 @@ export class GetCommand extends DownloadCommand {
return Response.multipleResults(attachments.map((a) => a.id));
}
if (!(await this.stateService.getCanAccessPremium())) {
const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$,
);
if (!canAccessPremium) {
const originalCipher = await this.cipherService.get(cipher.id);
if (originalCipher == null || originalCipher.organizationId == null) {
return Response.error("Premium status is required to use this feature.");

View File

@ -68,6 +68,7 @@ export class ServeCommand {
this.main.apiService,
this.main.organizationService,
this.main.eventCollectionService,
this.main.billingAccountProfileStateService,
);
this.listCommand = new ListCommand(
this.main.cipherService,
@ -82,10 +83,10 @@ export class ServeCommand {
this.createCommand = new CreateCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.cryptoService,
this.main.apiService,
this.main.folderApiService,
this.main.billingAccountProfileStateService,
);
this.editCommand = new EditCommand(
this.main.cipherService,
@ -108,9 +109,9 @@ export class ServeCommand {
this.deleteCommand = new DeleteCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.apiService,
this.main.folderApiService,
this.main.billingAccountProfileStateService,
);
this.confirmCommand = new ConfirmCommand(
this.main.apiService,
@ -135,9 +136,9 @@ export class ServeCommand {
this.sendCreateCommand = new SendCreateCommand(
this.main.sendService,
this.main.stateService,
this.main.environmentService,
this.main.sendApiService,
this.main.billingAccountProfileStateService,
);
this.sendDeleteCommand = new SendDeleteCommand(this.main.sendService, this.main.sendApiService);
this.sendGetCommand = new SendGetCommand(
@ -148,9 +149,9 @@ export class ServeCommand {
);
this.sendEditCommand = new SendEditCommand(
this.main.sendService,
this.main.stateService,
this.sendGetCommand,
this.main.sendApiService,
this.main.billingAccountProfileStateService,
);
this.sendListCommand = new SendListCommand(
this.main.sendService,

View File

@ -6,6 +6,7 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ApiService } from "@bitwarden/common/services/api.service";
(global as any).fetch = fe.default;
@ -20,6 +21,7 @@ export class NodeApiService extends ApiService {
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
appIdService: AppIdService,
stateService: StateService,
logoutCallback: (expired: boolean) => Promise<void>,
customUserAgent: string = null,
) {
@ -28,6 +30,7 @@ export class NodeApiService extends ApiService {
platformUtilsService,
environmentService,
appIdService,
stateService,
logoutCallback,
customUserAgent,
);

View File

@ -1,6 +1,9 @@
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DefaultPassphraseGenerationOptions } from "@bitwarden/common/tools/generator/passphrase";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import {
DefaultPasswordGenerationOptions,
PasswordGenerationServiceAbstraction,
} from "@bitwarden/common/tools/generator/password";
import { PasswordGeneratorOptions } from "@bitwarden/common/tools/generator/password/password-generator-options";
import { Response } from "../models/response";
@ -64,7 +67,10 @@ class Options {
this.capitalize = CliUtils.convertBooleanOption(passedOptions?.capitalize);
this.includeNumber = CliUtils.convertBooleanOption(passedOptions?.includeNumber);
this.ambiguous = CliUtils.convertBooleanOption(passedOptions?.ambiguous);
this.length = CliUtils.convertNumberOption(passedOptions?.length, 14);
this.length = CliUtils.convertNumberOption(
passedOptions?.length,
DefaultPasswordGenerationOptions.length,
);
this.type = passedOptions?.passphrase ? "passphrase" : "password";
this.separator = CliUtils.convertStringOption(
passedOptions?.separator,
@ -74,8 +80,14 @@ class Options {
passedOptions?.words,
DefaultPassphraseGenerationOptions.numWords,
);
this.minNumber = CliUtils.convertNumberOption(passedOptions?.minNumber, 1);
this.minSpecial = CliUtils.convertNumberOption(passedOptions?.minSpecial, 1);
this.minNumber = CliUtils.convertNumberOption(
passedOptions?.minNumber,
DefaultPasswordGenerationOptions.minNumber,
);
this.minSpecial = CliUtils.convertNumberOption(
passedOptions?.minSpecial,
DefaultPasswordGenerationOptions.minSpecial,
);
if (!this.uppercase && !this.lowercase && !this.special && !this.number) {
this.lowercase = true;

View File

@ -1,8 +1,10 @@
import * as fs from "fs";
import * as path from "path";
import { firstValueFrom } from "rxjs";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
@ -16,9 +18,9 @@ import { SendResponse } from "../models/send.response";
export class SendCreateCommand {
constructor(
private sendService: SendService,
private stateService: StateService,
private environmentService: EnvironmentService,
private sendApiService: SendApiService,
private accountProfileService: BillingAccountProfileStateService,
) {}
async run(requestJson: any, cmdOptions: Record<string, any>) {
@ -82,7 +84,7 @@ export class SendCreateCommand {
);
}
if (!(await this.stateService.getCanAccessPremium())) {
if (!(await firstValueFrom(this.accountProfileService.hasPremiumFromAnySource$))) {
return Response.error("Premium status is required to use this feature.");
}

View File

@ -1,4 +1,6 @@
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { firstValueFrom } from "rxjs";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
@ -12,9 +14,9 @@ import { SendGetCommand } from "./get.command";
export class SendEditCommand {
constructor(
private sendService: SendService,
private stateService: StateService,
private getCommand: SendGetCommand,
private sendApiService: SendApiService,
private accountProfileService: BillingAccountProfileStateService,
) {}
async run(requestJson: string, cmdOptions: Record<string, any>): Promise<Response> {
@ -57,7 +59,10 @@ export class SendEditCommand {
return Response.badRequest("Cannot change a Send's type");
}
if (send.type === SendType.File && !(await this.stateService.getCanAccessPremium())) {
const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$,
);
if (send.type === SendType.File && !canAccessPremium) {
return Response.error("Premium status is required to use this feature.");
}

View File

@ -153,6 +153,7 @@ export class SendProgram extends Program {
this.main.apiService,
this.main.organizationService,
this.main.eventCollectionService,
this.main.billingAccountProfileStateService,
);
const response = await cmd.run("template", object, null);
this.processResponse(response);
@ -253,9 +254,9 @@ export class SendProgram extends Program {
);
const cmd = new SendEditCommand(
this.main.sendService,
this.main.stateService,
getCmd,
this.main.sendApiService,
this.main.billingAccountProfileStateService,
);
const response = await cmd.run(encodedJson, options);
this.processResponse(response);
@ -323,9 +324,9 @@ export class SendProgram extends Program {
await this.exitIfLocked();
const cmd = new SendCreateCommand(
this.main.sendService,
this.main.stateService,
this.main.environmentService,
this.main.sendApiService,
this.main.billingAccountProfileStateService,
);
return await cmd.run(encodedJson, options);
}

View File

@ -188,6 +188,7 @@ export class VaultProgram extends Program {
this.main.apiService,
this.main.organizationService,
this.main.eventCollectionService,
this.main.billingAccountProfileStateService,
);
const response = await command.run(object, id, cmd);
this.processResponse(response);
@ -226,10 +227,10 @@ export class VaultProgram extends Program {
const command = new CreateCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.cryptoService,
this.main.apiService,
this.main.folderApiService,
this.main.billingAccountProfileStateService,
);
const response = await command.run(object, encodedJson, cmd);
this.processResponse(response);
@ -313,9 +314,9 @@ export class VaultProgram extends Program {
const command = new DeleteCommand(
this.main.cipherService,
this.main.folderService,
this.main.stateService,
this.main.apiService,
this.main.folderApiService,
this.main.billingAccountProfileStateService,
);
const response = await command.run(object, id, cmd);
this.processResponse(response);

View File

@ -1,13 +1,15 @@
import * as fs from "fs";
import * as path from "path";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
import { FolderExport } from "@bitwarden/common/models/export/folder.export";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
@ -26,10 +28,10 @@ export class CreateCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private stateService: StateService,
private cryptoService: CryptoService,
private apiService: ApiService,
private folderApiService: FolderApiServiceAbstraction,
private accountProfileService: BillingAccountProfileStateService,
) {}
async run(
@ -124,7 +126,10 @@ export class CreateCommand {
return Response.notFound();
}
if (cipher.organizationId == null && !(await this.stateService.getCanAccessPremium())) {
if (
cipher.organizationId == null &&
!(await firstValueFrom(this.accountProfileService.hasPremiumFromAnySource$))
) {
return Response.error("Premium status is required to use this feature.");
}

View File

@ -1,5 +1,7 @@
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
@ -12,9 +14,9 @@ export class DeleteCommand {
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private stateService: StateService,
private apiService: ApiService,
private folderApiService: FolderApiServiceAbstraction,
private accountProfileService: BillingAccountProfileStateService,
) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
@ -75,7 +77,10 @@ export class DeleteCommand {
return Response.error("Attachment `" + id + "` was not found.");
}
if (cipher.organizationId == null && !(await this.stateService.getCanAccessPremium())) {
const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$,
);
if (cipher.organizationId == null && !canAccessPremium) {
return Response.error("Premium status is required to use this feature.");
}

View File

@ -130,12 +130,6 @@ version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205"
[[package]]
name = "bytes"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cbc"
version = "0.1.2"
@ -333,7 +327,6 @@ dependencies = [
"security-framework-sys",
"sha2",
"thiserror",
"tokio",
"typenum",
"widestring",
"windows",
@ -377,7 +370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
@ -772,17 +765,6 @@ dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
dependencies = [
"libc",
"wasi",
"windows-sys 0.48.0",
]
[[package]]
name = "napi"
version = "2.13.3"
@ -925,7 +907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
@ -1114,7 +1096,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
@ -1204,15 +1186,6 @@ dependencies = [
"digest",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.9"
@ -1228,16 +1201,6 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "socket2"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
dependencies = [
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "str-buf"
version = "1.0.6"
@ -1295,7 +1258,7 @@ dependencies = [
"fastrand",
"redox_syscall",
"rustix",
"windows-sys 0.52.0",
"windows-sys",
]
[[package]]
@ -1329,32 +1292,13 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.32.0"
version = "1.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
]
[[package]]
@ -1606,15 +1550,6 @@ dependencies = [
"windows-targets 0.52.4",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"

View File

@ -25,7 +25,6 @@ retry = "=2.0.0"
scopeguard = "=1.2.0"
sha2 = "=0.10.8"
thiserror = "=1.0.51"
tokio = { version = "=1.32.0", features = ["full"] }
typenum = "=1.17.0"
[build-dependencies]

View File

@ -24,7 +24,7 @@
"**/node_modules/argon2/package.json",
"**/node_modules/argon2/lib/binding/napi-v3/argon2.node"
],
"electronVersion": "28.2.6",
"electronVersion": "28.2.7",
"generateUpdatesFilesForAllChannels": true,
"publish": {
"provider": "generic",

View File

@ -3,13 +3,12 @@ import { FormBuilder } from "@angular/forms";
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs";
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { DeviceType } from "@bitwarden/common/enums";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -115,9 +114,8 @@ export class SettingsComponent implements OnInit {
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private messagingService: MessagingService,
private cryptoService: CryptoService,
private modalService: ModalService,
private themeStateService: ThemeStateService,
private settingsService: SettingsService,
private domainSettingsService: DomainSettingsService,
private dialogService: DialogService,
private userVerificationService: UserVerificationServiceAbstraction,
private biometricStateService: BiometricStateService,
@ -251,7 +249,7 @@ export class SettingsComponent implements OnInit {
approveLoginRequests: (await this.stateService.getApproveLoginRequests()) ?? false,
clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$),
minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(),
enableFavicons: !(await this.stateService.getDisableFavicon()),
enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$),
enableTray: await this.stateService.getEnableTray(),
enableMinToTray: await this.stateService.getEnableMinimizeToTray(),
enableCloseToTray: await this.stateService.getEnableCloseToTray(),
@ -498,7 +496,7 @@ export class SettingsComponent implements OnInit {
}
async saveFavicons() {
await this.settingsService.setDisableFavicon(!this.form.value.enableFavicons);
await this.domainSettingsService.setShowFavicons(this.form.value.enableFavicons);
this.messagingService.send("refreshCiphers");
}

View File

@ -19,9 +19,9 @@ import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
@ -122,7 +122,6 @@ export class AppComponent implements OnInit, OnDestroy {
constructor(
private broadcasterService: BroadcasterService,
private folderService: InternalFolderService,
private settingsService: SettingsService,
private syncService: SyncService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private cipherService: CipherService,
@ -153,6 +152,7 @@ export class AppComponent implements OnInit, OnDestroy {
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
private providerService: ProviderService,
private organizationService: InternalOrganizationServiceAbstraction,
) {}
ngOnInit() {
@ -574,7 +574,8 @@ export class AppComponent implements OnInit, OnDestroy {
let preLogoutActiveUserId;
try {
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
// Provide the userId of the user to upload events for
await this.eventUploadService.uploadEvents(userBeingLoggedOut as UserId);
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
await this.cryptoService.clearKeys(userBeingLoggedOut);
await this.cipherService.clear(userBeingLoggedOut);

View File

@ -2,9 +2,10 @@ import { animate, state, style, transition, trigger } from "@angular/animations"
import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { concatMap, Subject, takeUntil } from "rxjs";
import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
@ -12,6 +13,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { Account } from "@bitwarden/common/platform/models/domain/account";
import { UserId } from "@bitwarden/common/types/guid";
type ActiveAccount = {
id: string;
@ -84,6 +86,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
constructor(
private stateService: StateService,
private authService: AuthService,
private avatarService: AvatarService,
private messagingService: MessagingService,
private router: Router,
private tokenService: TokenService,
@ -101,7 +104,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
id: await this.tokenService.getUserId(),
name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()),
email: await this.tokenService.getEmail(),
avatarColor: await this.stateService.getAvatarColor(),
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
server: await this.environmentService.getHost(),
};
} catch {
@ -154,7 +157,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
name: baseAccounts[userId].profile.name,
email: baseAccounts[userId].profile.email,
authenticationStatus: await this.authService.getAuthStatus(userId),
avatarColor: await this.stateService.getAvatarColor({ userId: userId }),
avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)),
server: await this.environmentService.getHost(userId),
};
}

View File

@ -10,6 +10,7 @@ import {
OBSERVABLE_MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
WINDOW,
SUPPORTS_SECURE_STORAGE,
SYSTEM_THEME_OBSERVABLE,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
@ -18,6 +19,7 @@ import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/adm
import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { LoginService } from "@bitwarden/common/auth/services/login.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service";
@ -54,7 +56,10 @@ import { LoginGuard } from "../../auth/guards/login.guard";
import { Account } from "../../models/account";
import { ElectronCryptoService } from "../../platform/services/electron-crypto.service";
import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service";
import { ElectronPlatformUtilsService } from "../../platform/services/electron-platform-utils.service";
import {
ELECTRON_SUPPORTS_SECURE_STORAGE,
ElectronPlatformUtilsService,
} from "../../platform/services/electron-platform-utils.service";
import { ElectronRendererMessagingService } from "../../platform/services/electron-renderer-messaging.service";
import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service";
import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service";
@ -101,6 +106,13 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
useClass: ElectronPlatformUtilsService,
deps: [I18nServiceAbstraction, MessagingServiceAbstraction],
},
{
// We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid
// the TokenService having to inject the PlatformUtilsService which introduces a
// circular dependency on Desktop only.
provide: SUPPORTS_SECURE_STORAGE,
useValue: ELECTRON_SUPPORTS_SECURE_STORAGE,
},
{
provide: I18nServiceAbstraction,
useClass: I18nRendererService,
@ -140,6 +152,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
STATE_FACTORY,
AccountServiceAbstraction,
EnvironmentService,
TokenService,
MigrationRunner,
STATE_SERVICE_USE_CACHE,
],
@ -170,7 +183,6 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
CryptoServiceAbstraction,
CryptoFunctionServiceAbstraction,
MessagingServiceAbstraction,
I18nServiceAbstraction,
EncryptedMessageHandlerService,
DialogService,
],

View File

@ -4,6 +4,7 @@ import { FormBuilder } from "@angular/forms";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -32,6 +33,7 @@ export class AddEditComponent extends BaseAddEditComponent {
sendApiService: SendApiService,
dialogService: DialogService,
formBuilder: FormBuilder,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
i18nService,
@ -46,6 +48,7 @@ export class AddEditComponent extends BaseAddEditComponent {
sendApiService,
dialogService,
formBuilder,
billingAccountProfileStateService,
);
}

View File

@ -3,6 +3,7 @@ import { Router } from "@angular/router";
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -16,7 +17,8 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
) {
super(twoFactorService, router, i18nService, platformUtilsService, window);
super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService);
}
}

View File

@ -2,7 +2,9 @@ import * as path from "path";
import { app } from "electron";
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
@ -36,6 +38,7 @@ import { ClipboardMain } from "./platform/main/clipboard.main";
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service";
import { ElectronLogMainService } from "./platform/services/electron-log.main.service";
import { ELECTRON_SUPPORTS_SECURE_STORAGE } from "./platform/services/electron-platform-utils.service";
import { ElectronStateService } from "./platform/services/electron-state.service";
import { ElectronStorageService } from "./platform/services/electron-storage.service";
import { I18nMainService } from "./platform/services/i18n.main.service";
@ -53,6 +56,7 @@ export class Main {
mainCryptoFunctionService: MainCryptoFunctionService;
desktopCredentialStorageListener: DesktopCredentialStorageListener;
migrationRunner: MigrationRunner;
tokenService: TokenServiceAbstraction;
windowMain: WindowMain;
messagingMain: MessagingMain;
@ -124,19 +128,32 @@ export class Main {
storageServiceProvider,
);
const singleUserStateProvider = new DefaultSingleUserStateProvider(
storageServiceProvider,
stateEventRegistrarService,
);
const activeUserStateProvider = new DefaultActiveUserStateProvider(
accountService,
singleUserStateProvider,
);
const stateProvider = new DefaultStateProvider(
new DefaultActiveUserStateProvider(
accountService,
storageServiceProvider,
stateEventRegistrarService,
),
new DefaultSingleUserStateProvider(storageServiceProvider, stateEventRegistrarService),
activeUserStateProvider,
singleUserStateProvider,
globalStateProvider,
new DefaultDerivedStateProvider(this.memoryStorageForStateProviders),
);
this.environmentService = new EnvironmentService(stateProvider, accountService);
this.tokenService = new TokenService(
singleUserStateProvider,
globalStateProvider,
ELECTRON_SUPPORTS_SECURE_STORAGE,
this.storageService,
);
this.migrationRunner = new MigrationRunner(
this.storageService,
this.logService,
@ -154,6 +171,7 @@ export class Main {
new StateFactory(GlobalState, Account),
accountService, // will not broadcast logouts. This is a hack until we can remove messaging dependency
this.environmentService,
this.tokenService,
this.migrationRunner,
false, // Do not use disk caching because this will get out of sync with the renderer service
);
@ -175,6 +193,7 @@ export class Main {
this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => {
this.messagingMain.onMessage(message);
});
this.powerMonitorMain = new PowerMonitorMain(this.messagingService);
this.menuMain = new MenuMain(
this.i18nService,

View File

@ -8,6 +8,7 @@ import localeBn from "@angular/common/locales/bn";
import localeBs from "@angular/common/locales/bs";
import localeCa from "@angular/common/locales/ca";
import localeCs from "@angular/common/locales/cs";
import localeCy from "@angular/common/locales/cy";
import localeDa from "@angular/common/locales/da";
import localeDe from "@angular/common/locales/de";
import localeEl from "@angular/common/locales/el";
@ -21,6 +22,7 @@ import localeFa from "@angular/common/locales/fa";
import localeFi from "@angular/common/locales/fi";
import localeFil from "@angular/common/locales/fil";
import localeFr from "@angular/common/locales/fr";
import localeGl from "@angular/common/locales/gl";
import localeHe from "@angular/common/locales/he";
import localeHi from "@angular/common/locales/hi";
import localeHr from "@angular/common/locales/hr";
@ -32,11 +34,16 @@ import localeKa from "@angular/common/locales/ka";
import localeKm from "@angular/common/locales/km";
import localeKn from "@angular/common/locales/kn";
import localeKo from "@angular/common/locales/ko";
import localeLt from "@angular/common/locales/lt";
import localeLv from "@angular/common/locales/lv";
import localeMl from "@angular/common/locales/ml";
import localeMr from "@angular/common/locales/mr";
import localeMy from "@angular/common/locales/my";
import localeNb from "@angular/common/locales/nb";
import localeNe from "@angular/common/locales/ne";
import localeNl from "@angular/common/locales/nl";
import localeNn from "@angular/common/locales/nn";
import localeOr from "@angular/common/locales/or";
import localePl from "@angular/common/locales/pl";
import localePtBr from "@angular/common/locales/pt";
import localePtPt from "@angular/common/locales/pt-PT";
@ -48,6 +55,7 @@ import localeSl from "@angular/common/locales/sl";
import localeSr from "@angular/common/locales/sr";
import localeMe from "@angular/common/locales/sr-Latn-ME";
import localeSv from "@angular/common/locales/sv";
import localeTe from "@angular/common/locales/te";
import localeTh from "@angular/common/locales/th";
import localeTr from "@angular/common/locales/tr";
import localeUk from "@angular/common/locales/uk";
@ -64,6 +72,7 @@ registerLocaleData(localeBn, "bn");
registerLocaleData(localeBs, "bs");
registerLocaleData(localeCa, "ca");
registerLocaleData(localeCs, "cs");
registerLocaleData(localeCy, "cy");
registerLocaleData(localeDa, "da");
registerLocaleData(localeDe, "de");
registerLocaleData(localeEl, "el");
@ -77,6 +86,7 @@ registerLocaleData(localeFa, "fa");
registerLocaleData(localeFi, "fi");
registerLocaleData(localeFil, "fil");
registerLocaleData(localeFr, "fr");
registerLocaleData(localeGl, "gl");
registerLocaleData(localeHe, "he");
registerLocaleData(localeHi, "hi");
registerLocaleData(localeHr, "hr");
@ -88,12 +98,17 @@ registerLocaleData(localeKa, "ka");
registerLocaleData(localeKm, "km");
registerLocaleData(localeKn, "kn");
registerLocaleData(localeKo, "ko");
registerLocaleData(localeLt, "lt");
registerLocaleData(localeLv, "lv");
registerLocaleData(localeMe, "me");
registerLocaleData(localeMl, "ml");
registerLocaleData(localeMr, "mr");
registerLocaleData(localeMy, "my");
registerLocaleData(localeNb, "nb");
registerLocaleData(localeNe, "ne");
registerLocaleData(localeNl, "nl");
registerLocaleData(localeNn, "nn");
registerLocaleData(localeOr, "or");
registerLocaleData(localePl, "pl");
registerLocaleData(localePtBr, "pt-BR");
registerLocaleData(localePtPt, "pt-PT");
@ -104,6 +119,7 @@ registerLocaleData(localeSk, "sk");
registerLocaleData(localeSl, "sl");
registerLocaleData(localeSr, "sr");
registerLocaleData(localeSv, "sv");
registerLocaleData(localeTe, "te");
registerLocaleData(localeTh, "th");
registerLocaleData(localeTr, "tr");
registerLocaleData(localeUk, "uk");

View File

@ -8,6 +8,8 @@ import {
import { ClipboardWriteMessage } from "../types/clipboard";
export const ELECTRON_SUPPORTS_SECURE_STORAGE = true;
export class ElectronPlatformUtilsService implements PlatformUtilsService {
constructor(
protected i18nService: I18nService,
@ -142,7 +144,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
}
supportsSecureStorage(): boolean {
return true;
return ELECTRON_SUPPORTS_SECURE_STORAGE;
}
getAutofillKeyboardShortcut(): Promise<string> {

View File

@ -4,7 +4,6 @@ import { firstValueFrom } from "rxjs";
import { NativeMessagingVersion } from "@bitwarden/common/enums";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
@ -33,7 +32,6 @@ export class NativeMessageHandlerService {
private cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService,
private messagingService: MessagingService,
private i18nService: I18nService,
private encryptedMessageHandlerService: EncryptedMessageHandlerService,
private dialogService: DialogService,
) {}

View File

@ -3,7 +3,6 @@ import { firstValueFrom } from "rxjs";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -35,7 +34,6 @@ export class NativeMessagingService {
private cryptoService: CryptoService,
private platformUtilService: PlatformUtilsService,
private logService: LogService,
private i18nService: I18nService,
private messagingService: MessagingService,
private stateService: StateService,
private biometricStateService: BiometricStateService,

View File

@ -7,7 +7,7 @@
{{ "premiumMembership" | i18n }}
</h1>
<div class="box-content box-content-padded">
<div *ngIf="!isPremium">
<div *ngIf="!(isPremium$ | async)">
<p class="text-center lead">{{ "premiumNotCurrentMember" | i18n }}</p>
<p>{{ "premiumSignUpAndGet" | i18n }}</p>
<ul class="bwi-ul">
@ -40,7 +40,7 @@
{{ "premiumPrice" | i18n: (price | currency: "$") }}
</p>
</div>
<div *ngIf="isPremium">
<div *ngIf="isPremium$ | async">
<p class="text-center lead">{{ "premiumCurrentMember" | i18n }}</p>
<p class="text-center">{{ "premiumCurrentMemberThanks" | i18n }}</p>
</div>
@ -48,7 +48,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="primary" (click)="manage()" *ngIf="isPremium">
<button type="button" class="primary" (click)="manage()" *ngIf="isPremium$ | async">
<b>{{ "premiumManage" | i18n }}</b>
</button>
<button
@ -56,13 +56,13 @@
type="button"
class="primary"
(click)="purchase()"
*ngIf="!isPremium"
*ngIf="!(isPremium$ | async)"
[disabled]="$any(purchaseBtn).loading"
>
<b>{{ "premiumPurchase" | i18n }}</b>
</button>
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
<div class="right" *ngIf="!isPremium">
<div class="right" *ngIf="!(isPremium$ | async)">
<button
#refreshBtn
type="button"

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -22,6 +23,7 @@ export class PremiumComponent extends BasePremiumComponent {
stateService: StateService,
dialogService: DialogService,
environmentService: EnvironmentService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
i18nService,
@ -31,6 +33,7 @@ export class PremiumComponent extends BasePremiumComponent {
stateService,
dialogService,
environmentService,
billingAccountProfileStateService,
);
}
}

View File

@ -2,6 +2,7 @@ import { Component } from "@angular/core";
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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";
@ -26,6 +27,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
stateService: StateService,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
cipherService,
@ -38,6 +40,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
stateService,
fileDownloadService,
dialogService,
billingAccountProfileStateService,
);
}
}

View File

@ -8,6 +8,7 @@ import {
ViewContainerRef,
} from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { first } from "rxjs/operators";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
@ -15,6 +16,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -84,6 +86,7 @@ export class VaultComponent implements OnInit, OnDestroy {
activeFilter: VaultFilter = new VaultFilter();
private modal: ModalRef = null;
private componentIsDestroyed$ = new Subject<boolean>();
constructor(
private route: ActivatedRoute,
@ -103,10 +106,16 @@ export class VaultComponent implements OnInit, OnDestroy {
private searchBarService: SearchBarService,
private apiService: ApiService,
private dialogService: DialogService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {}
async ngOnInit() {
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium();
this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.componentIsDestroyed$))
.subscribe((canAccessPremium: boolean) => {
this.userHasPremiumAccess = canAccessPremium;
});
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
@ -229,6 +238,8 @@ export class VaultComponent implements OnInit, OnDestroy {
ngOnDestroy() {
this.searchBarService.setEnabled(false);
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.componentIsDestroyed$.next(true);
this.componentIsDestroyed$.complete();
}
async load() {

View File

@ -13,6 +13,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@ -58,6 +59,7 @@ export class ViewComponent extends BaseViewComponent implements OnChanges {
fileDownloadService: FileDownloadService,
dialogService: DialogService,
datePipe: DatePipe,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
cipherService,
@ -80,6 +82,7 @@ export class ViewComponent extends BaseViewComponent implements OnChanges {
fileDownloadService,
dialogService,
datePipe,
billingAccountProfileStateService,
);
}
ngOnInit() {

View File

@ -1,10 +1,12 @@
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import {
OrganizationUserStatusType,
OrganizationUserType,
@ -17,7 +19,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DialogService } from "@bitwarden/components";
@ -109,8 +110,8 @@ export abstract class BasePeopleComponent<
private logService: LogService,
private searchPipe: SearchPipe,
protected userNamePipe: UserNamePipe,
protected stateService: StateService,
protected dialogService: DialogService,
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
) {}
abstract edit(user: UserType): void;
@ -351,7 +352,9 @@ export abstract class BasePeopleComponent<
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const autoConfirm = await this.stateService.getAutoConfirmFingerPrints();
const autoConfirm = await firstValueFrom(
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
);
if (autoConfirm == null || !autoConfirm) {
const [modal] = await this.modalService.openViewRef(
UserConfirmComponent,

View File

@ -17,7 +17,7 @@ export class IsPaidOrgGuard implements CanActivate {
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const org = this.organizationService.get(route.params.organizationId);
const org = await this.organizationService.get(route.params.organizationId);
if (org == null) {
return this.router.createUrlTree(["/"]);

View File

@ -66,7 +66,7 @@ describe("Organization Permissions Guard", () => {
it("permits navigation if no permissions are specified", async () => {
const org = orgFactory();
organizationService.get.calledWith(org.id).mockReturnValue(org);
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state);
@ -81,7 +81,7 @@ describe("Organization Permissions Guard", () => {
};
const org = orgFactory();
organizationService.get.calledWith(org.id).mockReturnValue(org);
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state);
@ -104,7 +104,7 @@ describe("Organization Permissions Guard", () => {
});
const org = orgFactory();
organizationService.get.calledWith(org.id).mockReturnValue(org);
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state);
@ -124,7 +124,7 @@ describe("Organization Permissions Guard", () => {
}),
});
const org = orgFactory();
organizationService.get.calledWith(org.id).mockReturnValue(org);
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state);
@ -141,7 +141,7 @@ describe("Organization Permissions Guard", () => {
type: OrganizationUserType.Admin,
enabled: false,
});
organizationService.get.calledWith(org.id).mockReturnValue(org);
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state);
@ -153,7 +153,7 @@ describe("Organization Permissions Guard", () => {
type: OrganizationUserType.Owner,
enabled: false,
});
organizationService.get.calledWith(org.id).mockReturnValue(org);
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const actual = await organizationPermissionsGuard.canActivate(route, state);

View File

@ -28,7 +28,7 @@ export class OrganizationPermissionsGuard implements CanActivate {
await this.syncService.fullSync(false);
}
const org = this.organizationService.get(route.params.organizationId);
const org = await this.organizationService.get(route.params.organizationId);
if (org == null) {
return this.router.createUrlTree(["/"]);
}

View File

@ -16,7 +16,7 @@ export class OrganizationRedirectGuard implements CanActivate {
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const org = this.organizationService.get(route.params.organizationId);
const org = await this.organizationService.get(route.params.organizationId);
const customRedirect = route.data?.autoRedirectCallback;
if (customRedirect) {

View File

@ -1,8 +1,8 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@Component({
selector: "app-user-confirm",
@ -22,7 +22,7 @@ export class UserConfirmComponent implements OnInit {
constructor(
private cryptoService: CryptoService,
private logService: LogService,
private stateService: StateService,
private organizationManagementPreferencesService: OrganizationManagementPreferencesService,
) {}
async ngOnInit() {
@ -45,7 +45,7 @@ export class UserConfirmComponent implements OnInit {
}
if (this.dontAskAgain) {
await this.stateService.setAutoConfirmFingerprints(true);
await this.organizationManagementPreferencesService.autoConfirmFingerPrints.set(true);
}
this.onConfirmedUser.emit();

View File

@ -2,6 +2,7 @@ import { Component, Input } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { BulkUserDetails } from "./bulk-status.component";
@ -14,7 +15,9 @@ export class BulkRemoveComponent {
@Input() organizationId: string;
@Input() set users(value: BulkUserDetails[]) {
this._users = value;
this.showNoMasterPasswordWarning = this._users.some((u) => u.hasMasterPassword === false);
this.showNoMasterPasswordWarning = this._users.some(
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
);
}
get users(): BulkUserDetails[] {

View File

@ -2,6 +2,7 @@ import { DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components";
@ -37,7 +38,9 @@ export class BulkRestoreRevokeComponent {
this.isRevoking = data.isRevoking;
this.organizationId = data.organizationId;
this.users = data.users;
this.showNoMasterPasswordWarning = this.users.some((u) => u.hasMasterPassword === false);
this.showNoMasterPasswordWarning = this.users.some(
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
);
}
get bulkTitle() {

View File

@ -399,7 +399,11 @@
</bit-tab>
<bit-tab *ngIf="organization.useGroups" [label]="'groups' | i18n">
<div class="tw-mb-6">
{{ "groupAccessUserDesc" | i18n }}
{{
(restrictedAccess$ | async)
? ("restrictedGroupAccess" | i18n)
: ("groupAccessUserDesc" | i18n)
}}
</div>
<bit-access-selector
formControlName="groups"
@ -408,10 +412,14 @@
[selectorLabelText]="'selectGroups' | i18n"
[emptySelectionText]="'noGroupsAdded' | i18n"
[flexibleCollectionsEnabled]="organization.flexibleCollections"
[hideMultiSelect]="restrictedAccess$ | async"
></bit-access-selector>
</bit-tab>
<bit-tab [label]="'collections' | i18n">
<div *ngIf="organization.useGroups" class="tw-mb-6">
<div class="tw-mb-6" *ngIf="restrictedAccess$ | async">
{{ "restrictedCollectionAccess" | i18n }}
</div>
<div *ngIf="organization.useGroups && !(restrictedAccess$ | async)" class="tw-mb-6">
{{ "userPermissionOverrideHelper" | i18n }}
</div>
<div *ngIf="!organization.flexibleCollections" class="tw-mb-6">
@ -441,6 +449,7 @@
[selectorLabelText]="'selectCollections' | i18n"
[emptySelectionText]="'noCollectionsAdded' | i18n"
[flexibleCollectionsEnabled]="organization.flexibleCollections"
[hideMultiSelect]="restrictedAccess$ | async"
></bit-access-selector
></bit-tab>
</bit-tab-group>

View File

@ -4,6 +4,7 @@ import { FormBuilder, Validators } from "@angular/forms";
import {
combineLatest,
firstValueFrom,
map,
Observable,
of,
shareReplay,
@ -20,7 +21,9 @@ import {
} from "@bitwarden/common/admin-console/enums";
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ProductType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -99,6 +102,8 @@ export class MemberDialogComponent implements OnDestroy {
groups: [[] as AccessItemValue[]],
});
protected restrictedAccess$: Observable<boolean>;
protected permissionsGroup = this.formBuilder.group({
manageAssignedCollectionsGroup: this.formBuilder.group<Record<string, boolean>>({
manageAssignedCollections: false,
@ -144,6 +149,7 @@ export class MemberDialogComponent implements OnDestroy {
private organizationUserService: OrganizationUserService,
private dialogService: DialogService,
private configService: ConfigServiceAbstraction,
private accountService: AccountService,
organizationService: OrganizationService,
) {
this.organization$ = organizationService
@ -162,12 +168,43 @@ export class MemberDialogComponent implements OnDestroy {
),
);
const userDetails$ = this.params.organizationUserId
? this.userService.get(this.params.organizationId, this.params.organizationUserId)
: of(null);
// The orgUser cannot manage their own Group assignments if collection access is restricted
// TODO: fix disabled state of access-selector rows so that any controls are hidden
this.restrictedAccess$ = combineLatest([
this.organization$,
userDetails$,
this.accountService.activeAccount$,
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
]).pipe(
map(
([organization, userDetails, activeAccount, flexibleCollectionsV1Enabled]) =>
// Feature flag conditionals
flexibleCollectionsV1Enabled &&
organization.flexibleCollections &&
// Business logic conditionals
userDetails != null &&
userDetails.userId == activeAccount.id &&
!organization.allowAdminAccessToAllCollectionItems,
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
this.restrictedAccess$.pipe(takeUntil(this.destroy$)).subscribe((restrictedAccess) => {
if (restrictedAccess) {
this.formGroup.controls.groups.disable();
} else {
this.formGroup.controls.groups.enable();
}
});
combineLatest({
organization: this.organization$,
collections: this.collectionAdminService.getAll(this.params.organizationId),
userDetails: this.params.organizationUserId
? this.userService.get(this.params.organizationId, this.params.organizationUserId)
: of(null),
userDetails: userDetails$,
groups: groups$,
})
.pipe(takeUntil(this.destroy$))
@ -369,7 +406,11 @@ export class MemberDialogComponent implements OnDestroy {
userView.collections = this.formGroup.value.access
.filter((v) => v.type === AccessItemType.Collection)
.map(convertToSelectionView);
userView.groups = this.formGroup.value.groups.map((m) => m.id);
userView.groups = (await firstValueFrom(this.restrictedAccess$))
? null
: this.formGroup.value.groups.map((m) => m.id);
userView.accessSecretsManager = this.formGroup.value.accessSecretsManager;
if (this.editMode) {

Some files were not shown because too many files have changed in this diff Show More