mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
Merge branch 'main' into ps/PM-7846-rust-ipc
This commit is contained in:
1
.github/whitelist-capital-letters.txt
vendored
1
.github/whitelist-capital-letters.txt
vendored
@@ -18,6 +18,7 @@
|
|||||||
./libs/admin-console/README.md
|
./libs/admin-console/README.md
|
||||||
./libs/auth/README.md
|
./libs/auth/README.md
|
||||||
./libs/billing/README.md
|
./libs/billing/README.md
|
||||||
|
./libs/common/src/tools/integration/README.md
|
||||||
./libs/platform/README.md
|
./libs/platform/README.md
|
||||||
./libs/tools/README.md
|
./libs/tools/README.md
|
||||||
./libs/tools/export/vault-export/README.md
|
./libs/tools/export/vault-export/README.md
|
||||||
|
|||||||
151
apps/browser/src/auth/popup/two-factor-auth.component.ts
Normal file
151
apps/browser/src/auth/popup/two-factor-auth.component.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, Inject, OnInit } from "@angular/core";
|
||||||
|
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
|
||||||
|
|
||||||
|
import { TwoFactorAuthAuthenticatorComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-authenticator.component";
|
||||||
|
import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth.component";
|
||||||
|
import { TwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-options.component";
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
|
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
|
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||||
|
import {
|
||||||
|
ButtonModule,
|
||||||
|
FormFieldModule,
|
||||||
|
AsyncActionsModule,
|
||||||
|
CheckboxModule,
|
||||||
|
DialogModule,
|
||||||
|
LinkModule,
|
||||||
|
TypographyModule,
|
||||||
|
DialogService,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
LoginStrategyServiceAbstraction,
|
||||||
|
LoginEmailServiceAbstraction,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
} from "../../../../../libs/auth/src/common/abstractions";
|
||||||
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
|
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
templateUrl:
|
||||||
|
"../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html",
|
||||||
|
selector: "app-two-factor-auth",
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
JslibModule,
|
||||||
|
DialogModule,
|
||||||
|
ButtonModule,
|
||||||
|
LinkModule,
|
||||||
|
TypographyModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormFieldModule,
|
||||||
|
AsyncActionsModule,
|
||||||
|
RouterLink,
|
||||||
|
CheckboxModule,
|
||||||
|
TwoFactorOptionsComponent,
|
||||||
|
TwoFactorAuthAuthenticatorComponent,
|
||||||
|
],
|
||||||
|
providers: [I18nPipe],
|
||||||
|
})
|
||||||
|
export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent implements OnInit {
|
||||||
|
constructor(
|
||||||
|
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||||
|
protected router: Router,
|
||||||
|
i18nService: I18nService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
|
dialogService: DialogService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
logService: LogService,
|
||||||
|
protected twoFactorService: TwoFactorService,
|
||||||
|
loginEmailService: LoginEmailServiceAbstraction,
|
||||||
|
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||||
|
protected configService: ConfigService,
|
||||||
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
|
accountService: AccountService,
|
||||||
|
formBuilder: FormBuilder,
|
||||||
|
@Inject(WINDOW) protected win: Window,
|
||||||
|
private syncService: SyncService,
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
loginStrategyService,
|
||||||
|
router,
|
||||||
|
i18nService,
|
||||||
|
platformUtilsService,
|
||||||
|
environmentService,
|
||||||
|
dialogService,
|
||||||
|
route,
|
||||||
|
logService,
|
||||||
|
twoFactorService,
|
||||||
|
loginEmailService,
|
||||||
|
userDecryptionOptionsService,
|
||||||
|
ssoLoginService,
|
||||||
|
configService,
|
||||||
|
masterPasswordService,
|
||||||
|
accountService,
|
||||||
|
formBuilder,
|
||||||
|
win,
|
||||||
|
);
|
||||||
|
super.onSuccessfulLoginTdeNavigate = async () => {
|
||||||
|
this.win.close();
|
||||||
|
};
|
||||||
|
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
await super.ngOnInit();
|
||||||
|
|
||||||
|
if (this.route.snapshot.paramMap.has("webAuthnResponse")) {
|
||||||
|
// WebAuthn fallback response
|
||||||
|
this.selectedProviderType = TwoFactorProviderType.WebAuthn;
|
||||||
|
this.token = this.route.snapshot.paramMap.get("webAuthnResponse");
|
||||||
|
super.onSuccessfulLogin = async () => {
|
||||||
|
// 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.syncService.fullSync(true);
|
||||||
|
this.messagingService.send("reloadPopup");
|
||||||
|
window.close();
|
||||||
|
};
|
||||||
|
this.remember = this.route.snapshot.paramMap.get("remember") === "true";
|
||||||
|
await this.submit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await BrowserPopupUtils.inPopout(this.win)) {
|
||||||
|
this.selectedProviderType = TwoFactorProviderType.Email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebAuthn prompt appears inside the popup on linux, and requires a larger popup width
|
||||||
|
// than usual to avoid cutting off the dialog.
|
||||||
|
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) {
|
||||||
|
document.body.classList.add("linux-webauthn");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnDestroy() {
|
||||||
|
if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) {
|
||||||
|
document.body.classList.remove("linux-webauthn");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isLinux() {
|
||||||
|
return (await BrowserApi.getPlatformInfo()).os === "linux";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,6 @@ import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
|||||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service";
|
|
||||||
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
||||||
import { FormData } from "../services/abstractions/autofill.service";
|
import { FormData } from "../services/abstractions/autofill.service";
|
||||||
import AutofillService from "../services/autofill.service";
|
import AutofillService from "../services/autofill.service";
|
||||||
@@ -49,7 +48,6 @@ describe("NotificationBackground", () => {
|
|||||||
const authService = mock<AuthService>();
|
const authService = mock<AuthService>();
|
||||||
const policyService = mock<PolicyService>();
|
const policyService = mock<PolicyService>();
|
||||||
const folderService = mock<FolderService>();
|
const folderService = mock<FolderService>();
|
||||||
const stateService = mock<DefaultBrowserStateService>();
|
|
||||||
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
|
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
|
||||||
const domainSettingsService = mock<DomainSettingsService>();
|
const domainSettingsService = mock<DomainSettingsService>();
|
||||||
const environmentService = mock<EnvironmentService>();
|
const environmentService = mock<EnvironmentService>();
|
||||||
@@ -64,7 +62,6 @@ describe("NotificationBackground", () => {
|
|||||||
authService,
|
authService,
|
||||||
policyService,
|
policyService,
|
||||||
folderService,
|
folderService,
|
||||||
stateService,
|
|
||||||
userNotificationSettingsService,
|
userNotificationSettingsService,
|
||||||
domainSettingsService,
|
domainSettingsService,
|
||||||
environmentService,
|
environmentService,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
|||||||
|
|
||||||
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
|
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
|
||||||
import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window";
|
import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window";
|
||||||
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
||||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||||
@@ -76,7 +75,6 @@ export default class NotificationBackground {
|
|||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
private folderService: FolderService,
|
private folderService: FolderService,
|
||||||
private stateService: BrowserStateService,
|
|
||||||
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||||
private domainSettingsService: DomainSettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|||||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service";
|
|
||||||
import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service";
|
import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service";
|
||||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||||
import {
|
import {
|
||||||
@@ -73,7 +72,6 @@ describe("OverlayBackground", () => {
|
|||||||
urls: { icons: "https://icons.bitwarden.com/" },
|
urls: { icons: "https://icons.bitwarden.com/" },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const stateService = mock<DefaultBrowserStateService>();
|
|
||||||
const autofillSettingsService = mock<AutofillSettingsService>();
|
const autofillSettingsService = mock<AutofillSettingsService>();
|
||||||
const i18nService = mock<I18nService>();
|
const i18nService = mock<I18nService>();
|
||||||
const platformUtilsService = mock<BrowserPlatformUtilsService>();
|
const platformUtilsService = mock<BrowserPlatformUtilsService>();
|
||||||
@@ -104,7 +102,6 @@ describe("OverlayBackground", () => {
|
|||||||
authService,
|
authService,
|
||||||
environmentService,
|
environmentService,
|
||||||
domainSettingsService,
|
domainSettingsService,
|
||||||
stateService,
|
|
||||||
autofillSettingsService,
|
autofillSettingsService,
|
||||||
i18nService,
|
i18nService,
|
||||||
platformUtilsService,
|
platformUtilsService,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
|||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -101,7 +100,6 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private domainSettingsService: DomainSettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private stateService: StateService,
|
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
|||||||
@@ -6,16 +6,15 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
|
|||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
|
||||||
|
|
||||||
import { MainContextMenuHandler } from "./main-context-menu-handler";
|
import { MainContextMenuHandler } from "./main-context-menu-handler";
|
||||||
|
|
||||||
describe("context-menu", () => {
|
describe("context-menu", () => {
|
||||||
let stateService: MockProxy<BrowserStateService>;
|
let stateService: MockProxy<StateService>;
|
||||||
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
|
let autofillSettingsService: MockProxy<AutofillSettingsServiceAbstraction>;
|
||||||
let i18nService: MockProxy<I18nService>;
|
let i18nService: MockProxy<I18nService>;
|
||||||
let logService: MockProxy<LogService>;
|
let logService: MockProxy<LogService>;
|
||||||
|
|||||||
@@ -20,12 +20,11 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
|
|||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service";
|
|
||||||
|
|
||||||
import { InitContextMenuItems } from "./abstractions/main-context-menu-handler";
|
import { InitContextMenuItems } from "./abstractions/main-context-menu-handler";
|
||||||
|
|
||||||
export class MainContextMenuHandler {
|
export class MainContextMenuHandler {
|
||||||
@@ -143,7 +142,7 @@ export class MainContextMenuHandler {
|
|||||||
];
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: BrowserStateService,
|
private stateService: StateService,
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||||
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||||
|
|
||||||
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
|
||||||
|
|
||||||
const IdleInterval = 60 * 5; // 5 minutes
|
const IdleInterval = 60 * 5; // 5 minutes
|
||||||
|
|
||||||
export default class IdleBackground {
|
export default class IdleBackground {
|
||||||
@@ -18,7 +16,6 @@ export default class IdleBackground {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private vaultTimeoutService: VaultTimeoutService,
|
private vaultTimeoutService: VaultTimeoutService,
|
||||||
private stateService: BrowserStateService,
|
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
|
|||||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import {
|
import {
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
@@ -101,6 +102,7 @@ import { Message, MessageListener, MessageSender } from "@bitwarden/common/platf
|
|||||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||||
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
|
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
|
||||||
import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize";
|
import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize";
|
||||||
|
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||||
@@ -116,6 +118,7 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo
|
|||||||
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
|
||||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/services/state.service";
|
||||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||||
@@ -206,7 +209,6 @@ import { Fido2Background } from "../autofill/fido2/background/fido2.background";
|
|||||||
import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service";
|
import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service";
|
||||||
import AutofillService from "../autofill/services/autofill.service";
|
import AutofillService from "../autofill/services/autofill.service";
|
||||||
import { SafariApp } from "../browser/safariApp";
|
import { SafariApp } from "../browser/safariApp";
|
||||||
import { Account } from "../models/account";
|
|
||||||
import { BrowserApi } from "../platform/browser/browser-api";
|
import { BrowserApi } from "../platform/browser/browser-api";
|
||||||
import { flagEnabled } from "../platform/flags";
|
import { flagEnabled } from "../platform/flags";
|
||||||
import { UpdateBadge } from "../platform/listeners/update-badge";
|
import { UpdateBadge } from "../platform/listeners/update-badge";
|
||||||
@@ -215,13 +217,11 @@ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender
|
|||||||
/* eslint-enable no-restricted-imports */
|
/* eslint-enable no-restricted-imports */
|
||||||
import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document";
|
import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document";
|
||||||
import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service";
|
import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service";
|
||||||
import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service";
|
|
||||||
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
|
import { BrowserCryptoService } from "../platform/services/browser-crypto.service";
|
||||||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||||
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
||||||
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
|
import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service";
|
||||||
import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service";
|
import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service";
|
||||||
import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service";
|
|
||||||
import I18nService from "../platform/services/i18n.service";
|
import I18nService from "../platform/services/i18n.service";
|
||||||
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
|
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
|
||||||
import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service";
|
import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service";
|
||||||
@@ -540,7 +540,7 @@ export default class MainBackground {
|
|||||||
ClientType.Browser,
|
ClientType.Browser,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.stateService = new DefaultBrowserStateService(
|
this.stateService = new StateService(
|
||||||
this.storageService,
|
this.storageService,
|
||||||
this.secureStorageService,
|
this.secureStorageService,
|
||||||
this.memoryStorageService,
|
this.memoryStorageService,
|
||||||
@@ -968,7 +968,6 @@ export default class MainBackground {
|
|||||||
this.messagingService,
|
this.messagingService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
systemUtilsServiceReloadCallback,
|
systemUtilsServiceReloadCallback,
|
||||||
this.stateService,
|
|
||||||
this.autofillSettingsService,
|
this.autofillSettingsService,
|
||||||
this.vaultTimeoutSettingsService,
|
this.vaultTimeoutSettingsService,
|
||||||
this.biometricStateService,
|
this.biometricStateService,
|
||||||
@@ -1028,7 +1027,6 @@ export default class MainBackground {
|
|||||||
this.authService,
|
this.authService,
|
||||||
this.policyService,
|
this.policyService,
|
||||||
this.folderService,
|
this.folderService,
|
||||||
this.stateService,
|
|
||||||
this.userNotificationSettingsService,
|
this.userNotificationSettingsService,
|
||||||
this.domainSettingsService,
|
this.domainSettingsService,
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
@@ -1042,7 +1040,6 @@ export default class MainBackground {
|
|||||||
this.authService,
|
this.authService,
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
this.domainSettingsService,
|
this.domainSettingsService,
|
||||||
this.stateService,
|
|
||||||
this.autofillSettingsService,
|
this.autofillSettingsService,
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
@@ -1100,7 +1097,6 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.idleBackground = new IdleBackground(
|
this.idleBackground = new IdleBackground(
|
||||||
this.vaultTimeoutService,
|
this.vaultTimeoutService,
|
||||||
this.stateService,
|
|
||||||
this.notificationsService,
|
this.notificationsService,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.vaultTimeoutSettingsService,
|
this.vaultTimeoutSettingsService,
|
||||||
@@ -1227,11 +1223,6 @@ export default class MainBackground {
|
|||||||
async switchAccount(userId: UserId) {
|
async switchAccount(userId: UserId) {
|
||||||
let nextAccountStatus: AuthenticationStatus;
|
let nextAccountStatus: AuthenticationStatus;
|
||||||
try {
|
try {
|
||||||
const currentlyActiveAccount = await firstValueFrom(
|
|
||||||
this.accountService.activeAccount$.pipe(map((account) => account?.id)),
|
|
||||||
);
|
|
||||||
// can be removed once password generation history is migrated to state providers
|
|
||||||
await this.stateService.clearDecryptedData(currentlyActiveAccount);
|
|
||||||
// HACK to ensure account is switched before proceeding
|
// HACK to ensure account is switched before proceeding
|
||||||
const switchPromise = firstValueFrom(
|
const switchPromise = firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(
|
this.accountService.activeAccount$.pipe(
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Jsonify } from "type-fest";
|
|
||||||
|
|
||||||
import { Account as BaseAccount } from "@bitwarden/common/platform/models/domain/account";
|
|
||||||
|
|
||||||
import { BrowserComponentState } from "./browserComponentState";
|
|
||||||
import { BrowserGroupingsComponentState } from "./browserGroupingsComponentState";
|
|
||||||
import { BrowserSendComponentState } from "./browserSendComponentState";
|
|
||||||
|
|
||||||
export class Account extends BaseAccount {
|
|
||||||
groupings?: BrowserGroupingsComponentState;
|
|
||||||
send?: BrowserSendComponentState;
|
|
||||||
ciphers?: BrowserComponentState;
|
|
||||||
sendType?: BrowserComponentState;
|
|
||||||
|
|
||||||
constructor(init: Partial<Account>) {
|
|
||||||
super(init);
|
|
||||||
|
|
||||||
this.groupings = init?.groupings ?? new BrowserGroupingsComponentState();
|
|
||||||
this.send = init?.send ?? new BrowserSendComponentState();
|
|
||||||
this.ciphers = init?.ciphers ?? new BrowserComponentState();
|
|
||||||
this.sendType = init?.sendType ?? new BrowserComponentState();
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJSON(json: Jsonify<Account>): Account {
|
|
||||||
if (json == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.assign(new Account({}), json, super.fromJSON(json), {
|
|
||||||
groupings: BrowserGroupingsComponentState.fromJSON(json.groupings),
|
|
||||||
send: BrowserSendComponentState.fromJSON(json.send),
|
|
||||||
ciphers: BrowserComponentState.fromJSON(json.ciphers),
|
|
||||||
sendType: BrowserComponentState.fromJSON(json.sendType),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
|
|
||||||
import { Account } from "../../../models/account";
|
|
||||||
|
|
||||||
export abstract class BrowserStateService extends BaseStateServiceAbstraction<Account> {}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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 { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
|
||||||
import { State } from "@bitwarden/common/platform/models/domain/state";
|
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
|
||||||
import { mockAccountServiceWith } from "@bitwarden/common/spec";
|
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
|
||||||
|
|
||||||
import { Account } from "../../models/account";
|
|
||||||
|
|
||||||
import { DefaultBrowserStateService } from "./default-browser-state.service";
|
|
||||||
|
|
||||||
describe("Browser State Service", () => {
|
|
||||||
let secureStorageService: MockProxy<AbstractStorageService>;
|
|
||||||
let diskStorageService: MockProxy<AbstractStorageService>;
|
|
||||||
let logService: MockProxy<LogService>;
|
|
||||||
let stateFactory: MockProxy<StateFactory<GlobalState, Account>>;
|
|
||||||
let environmentService: MockProxy<EnvironmentService>;
|
|
||||||
let tokenService: MockProxy<TokenService>;
|
|
||||||
let migrationRunner: MockProxy<MigrationRunner>;
|
|
||||||
|
|
||||||
let state: State<GlobalState, Account>;
|
|
||||||
const userId = "userId" as UserId;
|
|
||||||
const accountService = mockAccountServiceWith(userId);
|
|
||||||
|
|
||||||
let sut: DefaultBrowserStateService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
secureStorageService = mock();
|
|
||||||
diskStorageService = mock();
|
|
||||||
logService = mock();
|
|
||||||
stateFactory = mock();
|
|
||||||
environmentService = mock();
|
|
||||||
tokenService = mock();
|
|
||||||
migrationRunner = mock();
|
|
||||||
|
|
||||||
state = new State(new GlobalState());
|
|
||||||
state.accounts[userId] = new Account({
|
|
||||||
profile: { userId: userId },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("state methods", () => {
|
|
||||||
let memoryStorageService: MockProxy<AbstractStorageService>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
memoryStorageService = mock();
|
|
||||||
const stateGetter = (key: string) => Promise.resolve(state);
|
|
||||||
memoryStorageService.get.mockImplementation(stateGetter);
|
|
||||||
|
|
||||||
sut = new DefaultBrowserStateService(
|
|
||||||
diskStorageService,
|
|
||||||
secureStorageService,
|
|
||||||
memoryStorageService,
|
|
||||||
logService,
|
|
||||||
stateFactory,
|
|
||||||
accountService,
|
|
||||||
environmentService,
|
|
||||||
tokenService,
|
|
||||||
migrationRunner,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("exists", () => {
|
|
||||||
expect(sut).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
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 { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
|
||||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
|
||||||
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
|
||||||
|
|
||||||
import { Account } from "../../models/account";
|
|
||||||
|
|
||||||
import { BrowserStateService } from "./abstractions/browser-state.service";
|
|
||||||
|
|
||||||
export class DefaultBrowserStateService
|
|
||||||
extends BaseStateService<GlobalState, Account>
|
|
||||||
implements BrowserStateService
|
|
||||||
{
|
|
||||||
protected accountDeserializer = Account.fromJSON;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
storageService: AbstractStorageService,
|
|
||||||
secureStorageService: AbstractStorageService,
|
|
||||||
memoryStorageService: AbstractStorageService,
|
|
||||||
logService: LogService,
|
|
||||||
stateFactory: StateFactory<GlobalState, Account>,
|
|
||||||
accountService: AccountService,
|
|
||||||
environmentService: EnvironmentService,
|
|
||||||
tokenService: TokenService,
|
|
||||||
migrationRunner: MigrationRunner,
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
storageService,
|
|
||||||
secureStorageService,
|
|
||||||
memoryStorageService,
|
|
||||||
logService,
|
|
||||||
stateFactory,
|
|
||||||
accountService,
|
|
||||||
environmentService,
|
|
||||||
tokenService,
|
|
||||||
migrationRunner,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async addAccount(account: Account) {
|
|
||||||
// Apply browser overrides to default account values
|
|
||||||
account = new Account(account);
|
|
||||||
await super.addAccount(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overriding the base class to prevent deleting the cache on save. We register a storage listener
|
|
||||||
// to delete the cache in the constructor above.
|
|
||||||
protected override async saveAccountToDisk(
|
|
||||||
account: Account,
|
|
||||||
options: StorageOptions,
|
|
||||||
): Promise<void> {
|
|
||||||
const storageLocation = options.useSecureStorage
|
|
||||||
? this.secureStorageService
|
|
||||||
: this.storageService;
|
|
||||||
|
|
||||||
await storageLocation.save(`${options.userId}`, account, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
|
||||||
|
import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap";
|
||||||
import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard";
|
import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard";
|
||||||
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
|
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
|
||||||
import { EnvironmentComponent } from "../auth/popup/environment.component";
|
import { EnvironmentComponent } from "../auth/popup/environment.component";
|
||||||
@@ -33,6 +34,7 @@ import { RemovePasswordComponent } from "../auth/popup/remove-password.component
|
|||||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||||
import { SsoComponent } from "../auth/popup/sso.component";
|
import { SsoComponent } from "../auth/popup/sso.component";
|
||||||
|
import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component";
|
||||||
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component";
|
||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
@@ -137,12 +139,26 @@ const routes: Routes = [
|
|||||||
canActivate: [lockGuard()],
|
canActivate: [lockGuard()],
|
||||||
data: { state: "lock", doNotSaveUrl: true },
|
data: { state: "lock", doNotSaveUrl: true },
|
||||||
},
|
},
|
||||||
{
|
...twofactorRefactorSwap(
|
||||||
path: "2fa",
|
TwoFactorComponent,
|
||||||
component: TwoFactorComponent,
|
AnonLayoutWrapperComponent,
|
||||||
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
{
|
||||||
data: { state: "2fa" },
|
path: "2fa",
|
||||||
},
|
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||||
|
data: { state: "2fa" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "2fa",
|
||||||
|
canActivate: [unauthGuardFn(unauthRouteOverrides)],
|
||||||
|
data: { state: "2fa" },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: TwoFactorAuthComponent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
{
|
{
|
||||||
path: "2fa-options",
|
path: "2fa-options",
|
||||||
component: TwoFactorOptionsComponent,
|
component: TwoFactorOptionsComponent,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -19,7 +20,6 @@ import {
|
|||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
import { BrowserApi } from "../platform/browser/browser-api";
|
import { BrowserApi } from "../platform/browser/browser-api";
|
||||||
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
|
||||||
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
|
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
|
||||||
import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service";
|
import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service";
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private stateService: BrowserStateService,
|
private stateService: StateService,
|
||||||
private browserSendStateService: BrowserSendStateService,
|
private browserSendStateService: BrowserSendStateService,
|
||||||
private vaultBrowserStateService: VaultBrowserStateService,
|
private vaultBrowserStateService: VaultBrowserStateService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||||
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InitService {
|
export class InitService {
|
||||||
constructor(
|
constructor(
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private stateService: StateServiceAbstraction,
|
private stateService: StateService,
|
||||||
private twoFactorService: TwoFactorService,
|
private twoFactorService: TwoFactorService,
|
||||||
private logService: LogServiceAbstraction,
|
private logService: LogServiceAbstraction,
|
||||||
private themingService: AbstractThemingService,
|
private themingService: AbstractThemingService,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
CLIENT_TYPE,
|
CLIENT_TYPE,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { AuthRequestServiceAbstraction, PinServiceAbstraction } from "@bitwarden/auth/common";
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
@@ -32,7 +32,6 @@ import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config
|
|||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import {
|
import {
|
||||||
AutofillSettingsService,
|
AutofillSettingsService,
|
||||||
@@ -57,20 +56,17 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
|
|||||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import {
|
import {
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
|
||||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
|
||||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
|
||||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||||
import {
|
import {
|
||||||
@@ -94,7 +90,6 @@ import { UnauthGuardService } from "../../auth/popup/services";
|
|||||||
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
||||||
import AutofillService from "../../autofill/services/autofill.service";
|
import AutofillService from "../../autofill/services/autofill.service";
|
||||||
import MainBackground from "../../background/main.background";
|
import MainBackground from "../../background/main.background";
|
||||||
import { Account } from "../../models/account";
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator";
|
import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator";
|
||||||
/* eslint-disable no-restricted-imports */
|
/* eslint-disable no-restricted-imports */
|
||||||
@@ -104,13 +99,11 @@ import { OffscreenDocumentService } from "../../platform/offscreen-document/abst
|
|||||||
import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service";
|
import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service";
|
||||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
||||||
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
|
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
|
||||||
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
|
|
||||||
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
|
||||||
import { BrowserCryptoService } from "../../platform/services/browser-crypto.service";
|
import { BrowserCryptoService } from "../../platform/services/browser-crypto.service";
|
||||||
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
|
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
|
||||||
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
|
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
|
||||||
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
|
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
|
||||||
import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service";
|
|
||||||
import I18nService from "../../platform/services/i18n.service";
|
import I18nService from "../../platform/services/i18n.service";
|
||||||
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
|
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
|
||||||
import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider";
|
import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider";
|
||||||
@@ -219,7 +212,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
encryptService: EncryptService,
|
encryptService: EncryptService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
stateService: StateServiceAbstraction,
|
stateService: StateService,
|
||||||
accountService: AccountServiceAbstraction,
|
accountService: AccountServiceAbstraction,
|
||||||
stateProvider: StateProvider,
|
stateProvider: StateProvider,
|
||||||
biometricStateService: BiometricStateService,
|
biometricStateService: BiometricStateService,
|
||||||
@@ -250,7 +243,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
EncryptService,
|
EncryptService,
|
||||||
PlatformUtilsService,
|
PlatformUtilsService,
|
||||||
LogService,
|
LogService,
|
||||||
StateServiceAbstraction,
|
StateService,
|
||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
StateProvider,
|
StateProvider,
|
||||||
BiometricStateService,
|
BiometricStateService,
|
||||||
@@ -262,11 +255,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: TotpService,
|
useClass: TotpService,
|
||||||
deps: [CryptoFunctionService, LogService],
|
deps: [CryptoFunctionService, LogService],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: AuthRequestServiceAbstraction,
|
|
||||||
useFactory: getBgService<AuthRequestServiceAbstraction>("authRequestService"),
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: DeviceTrustServiceAbstraction,
|
provide: DeviceTrustServiceAbstraction,
|
||||||
useFactory: getBgService<DeviceTrustServiceAbstraction>("deviceTrustService"),
|
useFactory: getBgService<DeviceTrustServiceAbstraction>("deviceTrustService"),
|
||||||
@@ -436,46 +424,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
},
|
},
|
||||||
deps: [StateProvider],
|
deps: [StateProvider],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: StateServiceAbstraction,
|
|
||||||
useFactory: (
|
|
||||||
storageService: AbstractStorageService,
|
|
||||||
secureStorageService: AbstractStorageService,
|
|
||||||
memoryStorageService: AbstractStorageService,
|
|
||||||
logService: LogService,
|
|
||||||
accountService: AccountServiceAbstraction,
|
|
||||||
environmentService: EnvironmentService,
|
|
||||||
tokenService: TokenService,
|
|
||||||
migrationRunner: MigrationRunner,
|
|
||||||
) => {
|
|
||||||
return new DefaultBrowserStateService(
|
|
||||||
storageService,
|
|
||||||
secureStorageService,
|
|
||||||
memoryStorageService,
|
|
||||||
logService,
|
|
||||||
new StateFactory(GlobalState, Account),
|
|
||||||
accountService,
|
|
||||||
environmentService,
|
|
||||||
tokenService,
|
|
||||||
migrationRunner,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
deps: [
|
|
||||||
AbstractStorageService,
|
|
||||||
SECURE_STORAGE,
|
|
||||||
MEMORY_STORAGE,
|
|
||||||
LogService,
|
|
||||||
AccountServiceAbstraction,
|
|
||||||
EnvironmentService,
|
|
||||||
TokenService,
|
|
||||||
MigrationRunner,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
|
||||||
provide: BaseStateServiceAbstraction,
|
|
||||||
useExisting: StateServiceAbstraction,
|
|
||||||
deps: [],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: FileDownloadService,
|
provide: FileDownloadService,
|
||||||
useClass: BrowserFileDownloadService,
|
useClass: BrowserFileDownloadService,
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service";
|
|
||||||
import { FilePopoutUtilsService } from "../services/file-popout-utils.service";
|
import { FilePopoutUtilsService } from "../services/file-popout-utils.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -37,7 +37,7 @@ export class SendAddEditComponent extends BaseAddEditComponent {
|
|||||||
constructor(
|
constructor(
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
stateService: BrowserStateService,
|
stateService: StateService,
|
||||||
messagingService: MessagingService,
|
messagingService: MessagingService,
|
||||||
policyService: PolicyService,
|
policyService: PolicyService,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
|
|||||||
@@ -47,7 +47,13 @@
|
|||||||
(change)="onFileChange($event)"
|
(change)="onFileChange($event)"
|
||||||
/>
|
/>
|
||||||
<div class="tw-flex tw-gap-2 tw-items-center" aria-hidden="true">
|
<div class="tw-flex tw-gap-2 tw-items-center" aria-hidden="true">
|
||||||
<button bitButton buttonType="secondary" type="button" (click)="fileInput.click()">
|
<button
|
||||||
|
bitButton
|
||||||
|
buttonType="secondary"
|
||||||
|
type="button"
|
||||||
|
(click)="fileInput.click()"
|
||||||
|
class="tw-whitespace-nowrap"
|
||||||
|
>
|
||||||
{{ "chooseFile" | i18n }}
|
{{ "chooseFile" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<p bitTypography="body2" class="tw-text-muted tw-mb-0">
|
<p bitTypography="body2" class="tw-text-muted tw-mb-0">
|
||||||
|
|||||||
3
apps/cli/src/admin-console/.eslintrc.json
Normal file
3
apps/cli/src/admin-console/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../../libs/admin-console/.eslintrc.json"
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
|
||||||
|
import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap";
|
||||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||||
import { HintComponent } from "../auth/hint.component";
|
import { HintComponent } from "../auth/hint.component";
|
||||||
@@ -30,6 +31,7 @@ import { RegisterComponent } from "../auth/register.component";
|
|||||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||||
import { SsoComponent } from "../auth/sso.component";
|
import { SsoComponent } from "../auth/sso.component";
|
||||||
|
import { TwoFactorAuthComponent } from "../auth/two-factor-auth.component";
|
||||||
import { TwoFactorComponent } from "../auth/two-factor.component";
|
import { TwoFactorComponent } from "../auth/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||||
import { VaultComponent } from "../vault/app/vault/vault.component";
|
import { VaultComponent } from "../vault/app/vault/vault.component";
|
||||||
@@ -61,7 +63,24 @@ const routes: Routes = [
|
|||||||
path: "admin-approval-requested",
|
path: "admin-approval-requested",
|
||||||
component: LoginViaAuthRequestComponent,
|
component: LoginViaAuthRequestComponent,
|
||||||
},
|
},
|
||||||
{ path: "2fa", component: TwoFactorComponent },
|
...twofactorRefactorSwap(
|
||||||
|
TwoFactorComponent,
|
||||||
|
AnonLayoutWrapperComponent,
|
||||||
|
{
|
||||||
|
path: "2fa",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "2fa",
|
||||||
|
component: AnonLayoutWrapperComponent,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: TwoFactorAuthComponent,
|
||||||
|
canActivate: [unauthGuardFn()],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
{
|
{
|
||||||
path: "login-initiated",
|
path: "login-initiated",
|
||||||
component: LoginDecryptionOptionsComponent,
|
component: LoginDecryptionOptionsComponent,
|
||||||
|
|||||||
@@ -403,7 +403,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
// Clear sequentialized caches
|
// Clear sequentialized caches
|
||||||
clearCaches();
|
clearCaches();
|
||||||
if (message.userId != null) {
|
if (message.userId != null) {
|
||||||
await this.stateService.clearDecryptedData(message.userId);
|
|
||||||
await this.accountService.switchAccount(message.userId);
|
await this.accountService.switchAccount(message.userId);
|
||||||
}
|
}
|
||||||
const locked =
|
const locked =
|
||||||
|
|||||||
@@ -165,8 +165,6 @@ export class AccountSwitcherComponent {
|
|||||||
async addAccount() {
|
async addAccount() {
|
||||||
this.close();
|
this.close();
|
||||||
|
|
||||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
|
||||||
await this.stateService.clearDecryptedData(activeAccount?.id as UserId);
|
|
||||||
await this.accountService.switchAccount(null);
|
await this.accountService.switchAccount(null);
|
||||||
await this.router.navigate(["/login"]);
|
await this.router.navigate(["/login"]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
SUPPORTS_SECURE_STORAGE,
|
SUPPORTS_SECURE_STORAGE,
|
||||||
SYSTEM_THEME_OBSERVABLE,
|
SYSTEM_THEME_OBSERVABLE,
|
||||||
SafeInjectionToken,
|
SafeInjectionToken,
|
||||||
STATE_FACTORY,
|
|
||||||
DEFAULT_VAULT_TIMEOUT,
|
DEFAULT_VAULT_TIMEOUT,
|
||||||
INTRAPROCESS_MESSAGING_SUBJECT,
|
INTRAPROCESS_MESSAGING_SUBJECT,
|
||||||
CLIENT_TYPE,
|
CLIENT_TYPE,
|
||||||
@@ -25,13 +24,11 @@ import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/a
|
|||||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
@@ -45,13 +42,10 @@ import { StateService as StateServiceAbstraction } from "@bitwarden/common/platf
|
|||||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
|
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
|
||||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
// eslint-disable-next-line no-restricted-imports -- Used for dependency injection
|
||||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
|
||||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||||
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
||||||
@@ -63,7 +57,6 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
|
|||||||
|
|
||||||
import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions";
|
import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions";
|
||||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||||
import { Account } from "../../models/account";
|
|
||||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||||
import { ElectronCryptoService } from "../../platform/services/electron-crypto.service";
|
import { ElectronCryptoService } from "../../platform/services/electron-crypto.service";
|
||||||
import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service";
|
import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service";
|
||||||
@@ -74,7 +67,6 @@ import {
|
|||||||
import { ElectronRendererMessageSender } from "../../platform/services/electron-renderer-message.sender";
|
import { ElectronRendererMessageSender } from "../../platform/services/electron-renderer-message.sender";
|
||||||
import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service";
|
import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service";
|
||||||
import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service";
|
import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service";
|
||||||
import { ElectronStateService } from "../../platform/services/electron-state.service";
|
|
||||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||||
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
|
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
|
||||||
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
|
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
|
||||||
@@ -90,11 +82,6 @@ import { RendererCryptoFunctionService } from "./renderer-crypto-function.servic
|
|||||||
|
|
||||||
const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK");
|
const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK");
|
||||||
|
|
||||||
// Desktop has its own Account definition which must be used in its StateService
|
|
||||||
const DESKTOP_STATE_FACTORY = new SafeInjectionToken<StateFactory<GlobalState, Account>>(
|
|
||||||
"DESKTOP_STATE_FACTORY",
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider definitions used in the ngModule.
|
* Provider definitions used in the ngModule.
|
||||||
* Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety.
|
* Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety.
|
||||||
@@ -111,14 +98,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
deps: [InitService],
|
deps: [InitService],
|
||||||
multi: true,
|
multi: true,
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: DESKTOP_STATE_FACTORY,
|
|
||||||
useValue: new StateFactory(GlobalState, Account),
|
|
||||||
}),
|
|
||||||
safeProvider({
|
|
||||||
provide: STATE_FACTORY,
|
|
||||||
useValue: null,
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: RELOAD_CALLBACK,
|
provide: RELOAD_CALLBACK,
|
||||||
useValue: null,
|
useValue: null,
|
||||||
@@ -194,28 +173,12 @@ const safeProviders: SafeProvider[] = [
|
|||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
RELOAD_CALLBACK,
|
RELOAD_CALLBACK,
|
||||||
StateServiceAbstraction,
|
|
||||||
AutofillSettingsServiceAbstraction,
|
AutofillSettingsServiceAbstraction,
|
||||||
VaultTimeoutSettingsService,
|
VaultTimeoutSettingsService,
|
||||||
BiometricStateService,
|
BiometricStateService,
|
||||||
AccountServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: StateServiceAbstraction,
|
|
||||||
useClass: ElectronStateService,
|
|
||||||
deps: [
|
|
||||||
AbstractStorageService,
|
|
||||||
SECURE_STORAGE,
|
|
||||||
MEMORY_STORAGE,
|
|
||||||
LogService,
|
|
||||||
DESKTOP_STATE_FACTORY,
|
|
||||||
AccountServiceAbstraction,
|
|
||||||
EnvironmentService,
|
|
||||||
TokenService,
|
|
||||||
MigrationRunner,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: FileDownloadService,
|
provide: FileDownloadService,
|
||||||
useClass: DesktopFileDownloadService,
|
useClass: DesktopFileDownloadService,
|
||||||
|
|||||||
41
apps/desktop/src/auth/two-factor-auth.component.ts
Normal file
41
apps/desktop/src/auth/two-factor-auth.component.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { DialogModule } from "@angular/cdk/dialog";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { RouterLink } from "@angular/router";
|
||||||
|
|
||||||
|
import { TwoFactorAuthAuthenticatorComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component";
|
||||||
|
import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component";
|
||||||
|
import { TwoFactorOptionsComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component";
|
||||||
|
import { JslibModule } from "../../../../libs/angular/src/jslib.module";
|
||||||
|
import { AsyncActionsModule } from "../../../../libs/components/src/async-actions";
|
||||||
|
import { ButtonModule } from "../../../../libs/components/src/button";
|
||||||
|
import { CheckboxModule } from "../../../../libs/components/src/checkbox";
|
||||||
|
import { FormFieldModule } from "../../../../libs/components/src/form-field";
|
||||||
|
import { LinkModule } from "../../../../libs/components/src/link";
|
||||||
|
import { I18nPipe } from "../../../../libs/components/src/shared/i18n.pipe";
|
||||||
|
import { TypographyModule } from "../../../../libs/components/src/typography";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
templateUrl:
|
||||||
|
"../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html",
|
||||||
|
selector: "app-two-factor-auth",
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
JslibModule,
|
||||||
|
DialogModule,
|
||||||
|
ButtonModule,
|
||||||
|
LinkModule,
|
||||||
|
TypographyModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormFieldModule,
|
||||||
|
AsyncActionsModule,
|
||||||
|
RouterLink,
|
||||||
|
CheckboxModule,
|
||||||
|
TwoFactorOptionsComponent,
|
||||||
|
TwoFactorAuthAuthenticatorComponent,
|
||||||
|
],
|
||||||
|
providers: [I18nPipe],
|
||||||
|
})
|
||||||
|
export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent {}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import {
|
|
||||||
Account as BaseAccount,
|
|
||||||
AccountSettings as BaseAccountSettings,
|
|
||||||
} from "@bitwarden/common/platform/models/domain/account";
|
|
||||||
|
|
||||||
export class AccountSettings extends BaseAccountSettings {
|
|
||||||
dismissedBiometricRequirePasswordOnStartCallout?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Account extends BaseAccount {
|
|
||||||
settings?: AccountSettings = new AccountSettings();
|
|
||||||
|
|
||||||
constructor(init: Partial<Account>) {
|
|
||||||
super(init);
|
|
||||||
Object.assign(this.settings, {
|
|
||||||
...new AccountSettings(),
|
|
||||||
...this.settings,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
|
||||||
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
|
||||||
|
|
||||||
import { Account } from "../../models/account";
|
|
||||||
|
|
||||||
export class ElectronStateService extends BaseStateService<GlobalState, Account> {
|
|
||||||
async addAccount(account: Account) {
|
|
||||||
// Apply desktop overides to default account values
|
|
||||||
account = new Account(account);
|
|
||||||
await super.addAccount(account);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
apps/web/src/app/admin-console/.eslintrc.json
Normal file
3
apps/web/src/app/admin-console/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../../../libs/admin-console/.eslintrc.json"
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ export class PaidOrganizationOnlyComponent {}
|
|||||||
@Component({
|
@Component({
|
||||||
template: "<h1>This is the organization upgrade screen!</h1>",
|
template: "<h1>This is the organization upgrade screen!</h1>",
|
||||||
})
|
})
|
||||||
export class OrganizationUpgradeScreen {}
|
export class OrganizationUpgradeScreenComponent {}
|
||||||
|
|
||||||
const orgFactory = (props: Partial<Organization> = {}) =>
|
const orgFactory = (props: Partial<Organization> = {}) =>
|
||||||
Object.assign(
|
Object.assign(
|
||||||
@@ -62,7 +62,7 @@ describe("Is Paid Org Guard", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "organizations/:organizationId/billing/subscription",
|
path: "organizations/:organizationId/billing/subscription",
|
||||||
component: OrganizationUpgradeScreen,
|
component: OrganizationUpgradeScreenComponent,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { KeyValue } from "@angular/common";
|
import { KeyValue } from "@angular/common";
|
||||||
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from "@angular/core";
|
import { Component, Input, OnInit, OnDestroy } from "@angular/core";
|
||||||
import { FormControl, FormGroup } from "@angular/forms";
|
import { FormControl, FormGroup } from "@angular/forms";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
@@ -14,8 +14,6 @@ export class NestedCheckboxComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
@Input() parentId: string;
|
@Input() parentId: string;
|
||||||
@Input() checkboxes: FormGroup<Record<string, FormControl<boolean>>>;
|
@Input() checkboxes: FormGroup<Record<string, FormControl<boolean>>>;
|
||||||
@Output() onSavedUser = new EventEmitter();
|
|
||||||
@Output() onDeletedUser = new EventEmitter();
|
|
||||||
|
|
||||||
get parentIndeterminate() {
|
get parentIndeterminate() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
|
|||||||
@Input() email: string;
|
@Input() email: string;
|
||||||
@Input() id: string;
|
@Input() id: string;
|
||||||
@Input() organizationId: string;
|
@Input() organizationId: string;
|
||||||
@Output() onPasswordReset = new EventEmitter();
|
@Output() passwordReset = new EventEmitter();
|
||||||
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
|
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
|
||||||
|
|
||||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||||
@@ -156,7 +156,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
|
|||||||
null,
|
null,
|
||||||
this.i18nService.t("resetPasswordSuccess"),
|
this.i18nService.t("resetPasswordSuccess"),
|
||||||
);
|
);
|
||||||
this.onPasswordReset.emit();
|
this.passwordReset.emit();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -635,7 +635,7 @@ export class MembersComponent extends NewBasePeopleComponent<OrganizationUserVie
|
|||||||
comp.id = user != null ? user.id : null;
|
comp.id = user != null ? user.id : null;
|
||||||
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
comp.onPasswordReset.subscribe(() => {
|
comp.passwordReset.subscribe(() => {
|
||||||
modal.close();
|
modal.close();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
||||||
|
|
||||||
import { ControlsOf } from "@bitwarden/angular/types/controls-of";
|
import { ControlsOf } from "@bitwarden/angular/types/controls-of";
|
||||||
@@ -21,7 +21,7 @@ export class MasterPasswordPolicy extends BasePolicy {
|
|||||||
selector: "policy-master-password",
|
selector: "policy-master-password",
|
||||||
templateUrl: "master-password.component.html",
|
templateUrl: "master-password.component.html",
|
||||||
})
|
})
|
||||||
export class MasterPasswordPolicyComponent extends BasePolicyComponent {
|
export class MasterPasswordPolicyComponent extends BasePolicyComponent implements OnInit {
|
||||||
MinPasswordLength = Utils.minimumPasswordLength;
|
MinPasswordLength = Utils.minimumPasswordLength;
|
||||||
|
|
||||||
data: FormGroup<ControlsOf<MasterPasswordPolicyOptions>> = this.formBuilder.group({
|
data: FormGroup<ControlsOf<MasterPasswordPolicyOptions>> = this.formBuilder.group({
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { ChangeDetectorRef, Component, Inject, ViewChild, ViewContainerRef } from "@angular/core";
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Inject,
|
||||||
|
ViewChild,
|
||||||
|
ViewContainerRef,
|
||||||
|
} from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||||
@@ -28,7 +35,7 @@ export enum PolicyEditDialogResult {
|
|||||||
selector: "app-policy-edit",
|
selector: "app-policy-edit",
|
||||||
templateUrl: "policy-edit.component.html",
|
templateUrl: "policy-edit.component.html",
|
||||||
})
|
})
|
||||||
export class PolicyEditComponent {
|
export class PolicyEditComponent implements AfterViewInit {
|
||||||
@ViewChild("policyForm", { read: ViewContainerRef, static: true })
|
@ViewChild("policyForm", { read: ViewContainerRef, static: true })
|
||||||
policyFormRef: ViewContainerRef;
|
policyFormRef: ViewContainerRef;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
@@ -22,7 +22,7 @@ export class ResetPasswordPolicy extends BasePolicy {
|
|||||||
selector: "policy-reset-password",
|
selector: "policy-reset-password",
|
||||||
templateUrl: "reset-password.component.html",
|
templateUrl: "reset-password.component.html",
|
||||||
})
|
})
|
||||||
export class ResetPasswordPolicyComponent extends BasePolicyComponent {
|
export class ResetPasswordPolicyComponent extends BasePolicyComponent implements OnInit {
|
||||||
data = this.formBuilder.group({
|
data = this.formBuilder.group({
|
||||||
autoEnrollEnabled: false,
|
autoEnrollEnabled: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { combineLatest, from, lastValueFrom, of, Subject, switchMap, takeUntil } from "rxjs";
|
import { combineLatest, from, lastValueFrom, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||||
@@ -27,7 +27,7 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./
|
|||||||
selector: "app-org-account",
|
selector: "app-org-account",
|
||||||
templateUrl: "account.component.html",
|
templateUrl: "account.component.html",
|
||||||
})
|
})
|
||||||
export class AccountComponent {
|
export class AccountComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild("apiKeyTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("apiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||||
apiKeyModalRef: ViewContainerRef;
|
apiKeyModalRef: ViewContainerRef;
|
||||||
@ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DialogRef } from "@angular/cdk/dialog";
|
import { DialogRef } from "@angular/cdk/dialog";
|
||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { concatMap, takeUntil, map, lastValueFrom } from "rxjs";
|
import { concatMap, takeUntil, map, lastValueFrom } from "rxjs";
|
||||||
import { first, tap } from "rxjs/operators";
|
import { first, tap } from "rxjs/operators";
|
||||||
@@ -24,7 +24,7 @@ import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-veri
|
|||||||
templateUrl: "../../../auth/settings/two-factor-setup.component.html",
|
templateUrl: "../../../auth/settings/two-factor-setup.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
|
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent implements OnInit {
|
||||||
tabbedHeader = false;
|
tabbedHeader = false;
|
||||||
constructor(
|
constructor(
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -19,7 +19,10 @@ import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent
|
|||||||
templateUrl: "../../../tools/reports/pages/exposed-passwords-report.component.html",
|
templateUrl: "../../../tools/reports/pages/exposed-passwords-report.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
|
export class ExposedPasswordsReportComponent
|
||||||
|
extends BaseExposedPasswordsReportComponent
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
manageableCiphers: Cipher[];
|
manageableCiphers: Cipher[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -18,7 +18,10 @@ import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponen
|
|||||||
templateUrl: "../../../tools/reports/pages/inactive-two-factor-report.component.html",
|
templateUrl: "../../../tools/reports/pages/inactive-two-factor-report.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorReportComponent {
|
export class InactiveTwoFactorReportComponent
|
||||||
|
extends BaseInactiveTwoFactorReportComponent
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
constructor(
|
constructor(
|
||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -18,7 +18,10 @@ import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent }
|
|||||||
templateUrl: "../../../tools/reports/pages/reused-passwords-report.component.html",
|
templateUrl: "../../../tools/reports/pages/reused-passwords-report.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent {
|
export class ReusedPasswordsReportComponent
|
||||||
|
extends BaseReusedPasswordsReportComponent
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
manageableCiphers: Cipher[];
|
manageableCiphers: Cipher[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -17,7 +17,10 @@ import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponen
|
|||||||
templateUrl: "../../../tools/reports/pages/unsecured-websites-report.component.html",
|
templateUrl: "../../../tools/reports/pages/unsecured-websites-report.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesReportComponent {
|
export class UnsecuredWebsitesReportComponent
|
||||||
|
extends BaseUnsecuredWebsitesReportComponent
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
constructor(
|
constructor(
|
||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -19,7 +19,10 @@ import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from
|
|||||||
templateUrl: "../../../tools/reports/pages/weak-passwords-report.component.html",
|
templateUrl: "../../../tools/reports/pages/weak-passwords-report.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent {
|
export class WeakPasswordsReportComponent
|
||||||
|
extends BaseWeakPasswordsReportComponent
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
manageableCiphers: Cipher[];
|
manageableCiphers: Cipher[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -72,7 +72,14 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<hr *ngIf="enabled" />
|
<hr *ngIf="enabled" />
|
||||||
<p class="tw-text-center tw-mb-0">
|
<p class="tw-text-center tw-mb-0">
|
||||||
<canvas id="qr"></canvas><br />
|
<ng-container *ngIf="qrScriptError" class="tw-mt-2">
|
||||||
|
<i class="bwi bwi-error tw-text-3xl tw-text-danger" aria-hidden="true"></i>
|
||||||
|
<p>
|
||||||
|
{{ "twoStepAuthenticatorQRCanvasError" | i18n }}
|
||||||
|
</p>
|
||||||
|
</ng-container>
|
||||||
|
<canvas *ngIf="!qrScriptError" id="qr"></canvas>
|
||||||
|
<br />
|
||||||
<code appA11yTitle="{{ 'key' | i18n }}">{{ key }}</code>
|
<code appA11yTitle="{{ 'key' | i18n }}">{{ key }}</code>
|
||||||
</p>
|
</p>
|
||||||
<bit-form-field *ngIf="!enabled" [disableMargin]="true">
|
<bit-form-field *ngIf="!enabled" [disableMargin]="true">
|
||||||
@@ -90,7 +97,7 @@
|
|||||||
>
|
>
|
||||||
{{ (enabled ? "disable" : "enable") | i18n }}
|
{{ (enabled ? "disable" : "enable") | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button bitButton bitFormButton type="button" buttonType="secondary" [bitAction]="close">
|
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
|
||||||
{{ "close" | i18n }}
|
{{ "close" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ export class TwoFactorAuthenticatorComponent
|
|||||||
@Output() onChangeStatus = new EventEmitter<boolean>();
|
@Output() onChangeStatus = new EventEmitter<boolean>();
|
||||||
type = TwoFactorProviderType.Authenticator;
|
type = TwoFactorProviderType.Authenticator;
|
||||||
key: string;
|
key: string;
|
||||||
formPromise: Promise<TwoFactorAuthenticatorResponse>;
|
|
||||||
|
|
||||||
override componentName = "app-two-factor-authenticator";
|
override componentName = "app-two-factor-authenticator";
|
||||||
|
qrScriptError = false;
|
||||||
private qrScript: HTMLScriptElement;
|
private qrScript: HTMLScriptElement;
|
||||||
|
|
||||||
formGroup = this.formBuilder.group({
|
formGroup = this.formBuilder.group({
|
||||||
@@ -90,7 +90,7 @@ export class TwoFactorAuthenticatorComponent
|
|||||||
this.formGroup.controls.token.markAsTouched();
|
this.formGroup.controls.token.markAsTouched();
|
||||||
}
|
}
|
||||||
|
|
||||||
auth(authResponse: AuthResponse<TwoFactorAuthenticatorResponse>) {
|
async auth(authResponse: AuthResponse<TwoFactorAuthenticatorResponse>) {
|
||||||
super.auth(authResponse);
|
super.auth(authResponse);
|
||||||
return this.processResponse(authResponse.response);
|
return this.processResponse(authResponse.response);
|
||||||
}
|
}
|
||||||
@@ -100,56 +100,69 @@ export class TwoFactorAuthenticatorComponent
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.enabled) {
|
if (this.enabled) {
|
||||||
await this.disableAuthentication(this.formPromise);
|
await this.disableMethod();
|
||||||
this.onChangeStatus.emit(this.enabled);
|
this.dialogRef.close(this.enabled);
|
||||||
this.close();
|
|
||||||
} else {
|
} else {
|
||||||
await this.enable();
|
await this.enable();
|
||||||
this.onChangeStatus.emit(this.enabled);
|
|
||||||
}
|
}
|
||||||
|
this.onChangeStatus.emit(this.enabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
private async disableAuthentication(promise: Promise<unknown>) {
|
|
||||||
return super.disable(promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async enable() {
|
protected async enable() {
|
||||||
const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest);
|
const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest);
|
||||||
request.token = this.formGroup.value.token;
|
request.token = this.formGroup.value.token;
|
||||||
request.key = this.key;
|
request.key = this.key;
|
||||||
|
|
||||||
return super.enable(async () => {
|
const response = await this.apiService.putTwoFactorAuthenticator(request);
|
||||||
this.formPromise = this.apiService.putTwoFactorAuthenticator(request);
|
await this.processResponse(response);
|
||||||
const response = await this.formPromise;
|
this.onUpdated.emit(true);
|
||||||
await this.processResponse(response);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processResponse(response: TwoFactorAuthenticatorResponse) {
|
private async processResponse(response: TwoFactorAuthenticatorResponse) {
|
||||||
this.formGroup.get("token").setValue(null);
|
this.formGroup.get("token").setValue(null);
|
||||||
this.enabled = response.enabled;
|
this.enabled = response.enabled;
|
||||||
this.key = response.key;
|
this.key = response.key;
|
||||||
|
|
||||||
|
await this.waitForQRiousToLoadOrError().catch((error) => {
|
||||||
|
this.logService.error(error);
|
||||||
|
this.qrScriptError = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.createQRCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForQRiousToLoadOrError(): Promise<void> {
|
||||||
|
// Check if QRious is already loaded or if there was an error loading it either way don't wait for it to try and load again
|
||||||
|
if (typeof window.QRious !== "undefined" || this.qrScriptError) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.qrScript.onload = () => resolve();
|
||||||
|
this.qrScript.onerror = () =>
|
||||||
|
reject(new Error(this.i18nService.t("twoStepAuthenticatorQRCanvasError")));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createQRCode() {
|
||||||
|
if (this.qrScriptError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const email = await firstValueFrom(
|
const email = await firstValueFrom(
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||||
);
|
);
|
||||||
window.setTimeout(() => {
|
new window.QRious({
|
||||||
new window.QRious({
|
element: document.getElementById("qr"),
|
||||||
element: document.getElementById("qr"),
|
value:
|
||||||
value:
|
"otpauth://totp/Bitwarden:" +
|
||||||
"otpauth://totp/Bitwarden:" +
|
Utils.encodeRFC3986URIComponent(email) +
|
||||||
Utils.encodeRFC3986URIComponent(email) +
|
"?secret=" +
|
||||||
"?secret=" +
|
encodeURIComponent(this.key) +
|
||||||
encodeURIComponent(this.key) +
|
"&issuer=Bitwarden",
|
||||||
"&issuer=Bitwarden",
|
size: 160,
|
||||||
size: 160,
|
});
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close = () => {
|
|
||||||
this.dialogRef.close(this.enabled);
|
|
||||||
};
|
|
||||||
|
|
||||||
static open(
|
static open(
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
config: DialogConfig<AuthResponse<TwoFactorAuthenticatorResponse>>,
|
config: DialogConfig<AuthResponse<TwoFactorAuthenticatorResponse>>,
|
||||||
|
|||||||
107
apps/web/src/app/auth/two-factor-auth.component.ts
Normal file
107
apps/web/src/app/auth/two-factor-auth.component.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { DialogModule } from "@angular/cdk/dialog";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, Inject } from "@angular/core";
|
||||||
|
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { ActivatedRoute, Router, RouterLink } from "@angular/router";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
|
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { LinkModule, TypographyModule, CheckboxModule, DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { TwoFactorAuthAuthenticatorComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component";
|
||||||
|
import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component";
|
||||||
|
import { TwoFactorOptionsComponent } from "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component";
|
||||||
|
import {
|
||||||
|
LoginStrategyServiceAbstraction,
|
||||||
|
LoginEmailServiceAbstraction,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
} from "../../../../../libs/auth/src/common/abstractions";
|
||||||
|
import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions";
|
||||||
|
import { ButtonModule } from "../../../../../libs/components/src/button";
|
||||||
|
import { FormFieldModule } from "../../../../../libs/components/src/form-field";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
templateUrl:
|
||||||
|
"../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html",
|
||||||
|
selector: "app-two-factor-auth",
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
JslibModule,
|
||||||
|
DialogModule,
|
||||||
|
ButtonModule,
|
||||||
|
LinkModule,
|
||||||
|
TypographyModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormFieldModule,
|
||||||
|
AsyncActionsModule,
|
||||||
|
RouterLink,
|
||||||
|
CheckboxModule,
|
||||||
|
TwoFactorOptionsComponent,
|
||||||
|
TwoFactorAuthAuthenticatorComponent,
|
||||||
|
],
|
||||||
|
providers: [I18nPipe],
|
||||||
|
})
|
||||||
|
export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent {
|
||||||
|
constructor(
|
||||||
|
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||||
|
protected router: Router,
|
||||||
|
i18nService: I18nService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
|
dialogService: DialogService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
logService: LogService,
|
||||||
|
protected twoFactorService: TwoFactorService,
|
||||||
|
loginEmailService: LoginEmailServiceAbstraction,
|
||||||
|
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||||
|
protected configService: ConfigService,
|
||||||
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
|
accountService: AccountService,
|
||||||
|
formBuilder: FormBuilder,
|
||||||
|
@Inject(WINDOW) protected win: Window,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
loginStrategyService,
|
||||||
|
router,
|
||||||
|
i18nService,
|
||||||
|
platformUtilsService,
|
||||||
|
environmentService,
|
||||||
|
dialogService,
|
||||||
|
route,
|
||||||
|
logService,
|
||||||
|
twoFactorService,
|
||||||
|
loginEmailService,
|
||||||
|
userDecryptionOptionsService,
|
||||||
|
ssoLoginService,
|
||||||
|
configService,
|
||||||
|
masterPasswordService,
|
||||||
|
accountService,
|
||||||
|
formBuilder,
|
||||||
|
win,
|
||||||
|
);
|
||||||
|
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override handleMigrateEncryptionKey(result: AuthResult): boolean {
|
||||||
|
if (!result.requiresEncryptionKeyMigration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 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(["migrate-legacy-encryption"]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core";
|
|||||||
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||||
import {
|
import {
|
||||||
SECURE_STORAGE,
|
SECURE_STORAGE,
|
||||||
STATE_FACTORY,
|
|
||||||
LOCALES_DIRECTORY,
|
LOCALES_DIRECTORY,
|
||||||
SYSTEM_LANGUAGE,
|
SYSTEM_LANGUAGE,
|
||||||
MEMORY_STORAGE,
|
MEMORY_STORAGE,
|
||||||
@@ -30,10 +29,9 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil
|
|||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
||||||
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
|
||||||
@@ -64,7 +62,7 @@ import { EventService } from "./event.service";
|
|||||||
import { InitService } from "./init.service";
|
import { InitService } from "./init.service";
|
||||||
import { ModalService } from "./modal.service";
|
import { ModalService } from "./modal.service";
|
||||||
import { RouterService } from "./router.service";
|
import { RouterService } from "./router.service";
|
||||||
import { Account, GlobalState, StateService } from "./state";
|
import { StateService as WebStateService } from "./state";
|
||||||
import { WebFileDownloadService } from "./web-file-download.service";
|
import { WebFileDownloadService } from "./web-file-download.service";
|
||||||
import { WebPlatformUtilsService } from "./web-platform-utils.service";
|
import { WebPlatformUtilsService } from "./web-platform-utils.service";
|
||||||
|
|
||||||
@@ -90,10 +88,6 @@ const safeProviders: SafeProvider[] = [
|
|||||||
deps: [InitService],
|
deps: [InitService],
|
||||||
multi: true,
|
multi: true,
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
|
||||||
provide: STATE_FACTORY,
|
|
||||||
useValue: new StateFactory(GlobalState, Account),
|
|
||||||
}),
|
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: I18nServiceAbstraction,
|
provide: I18nServiceAbstraction,
|
||||||
useClass: I18nService,
|
useClass: I18nService,
|
||||||
@@ -132,10 +126,10 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: ModalService,
|
useClass: ModalService,
|
||||||
useAngularDecorators: true,
|
useAngularDecorators: true,
|
||||||
}),
|
}),
|
||||||
safeProvider(StateService),
|
safeProvider(WebStateService),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: BaseStateServiceAbstraction,
|
provide: StateService,
|
||||||
useExisting: StateService,
|
useExisting: WebStateService,
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: FileDownloadService,
|
provide: FileDownloadService,
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { Account as BaseAccount } from "@bitwarden/common/platform/models/domain/account";
|
|
||||||
|
|
||||||
// TODO: platform to clean up accounts in later PR
|
|
||||||
export class Account extends BaseAccount {
|
|
||||||
constructor(init: Partial<Account>) {
|
|
||||||
super(init);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { GlobalState as BaseGlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
|
||||||
|
|
||||||
export class GlobalState extends BaseGlobalState {
|
|
||||||
rememberEmail = true;
|
|
||||||
}
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
export * from "./account";
|
|
||||||
export * from "./global-state";
|
|
||||||
export * from "./state.service";
|
export * from "./state.service";
|
||||||
|
|||||||
@@ -11,13 +11,12 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
|
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||||
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||||
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
|
||||||
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
||||||
|
|
||||||
import { Account } from "./account";
|
|
||||||
import { GlobalState } from "./global-state";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StateService extends BaseStateService<GlobalState, Account> {
|
export class StateService extends BaseStateService<GlobalState, Account> {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -44,12 +43,6 @@ export class StateService extends BaseStateService<GlobalState, Account> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAccount(account: Account) {
|
|
||||||
// Apply web overrides to default account values
|
|
||||||
account = new Account(account);
|
|
||||||
await super.addAccount(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
override async getLastSync(options?: StorageOptions): Promise<string> {
|
override async getLastSync(options?: StorageOptions): Promise<string> {
|
||||||
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
options = this.reconcileOptions(options, await this.defaultInMemoryOptions());
|
||||||
return await super.getLastSync(options);
|
return await super.getLastSync(options);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
|
||||||
|
import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap";
|
||||||
import { flagEnabled, Flags } from "../utils/flags";
|
import { flagEnabled, Flags } from "../utils/flags";
|
||||||
|
|
||||||
import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/manage/verify-recover-delete-org.component";
|
import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/manage/verify-recover-delete-org.component";
|
||||||
@@ -46,6 +47,7 @@ import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/v
|
|||||||
import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module";
|
import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module";
|
||||||
import { SsoComponent } from "./auth/sso.component";
|
import { SsoComponent } from "./auth/sso.component";
|
||||||
import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component";
|
import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component";
|
||||||
|
import { TwoFactorAuthComponent } from "./auth/two-factor-auth.component";
|
||||||
import { TwoFactorComponent } from "./auth/two-factor.component";
|
import { TwoFactorComponent } from "./auth/two-factor.component";
|
||||||
import { UpdatePasswordComponent } from "./auth/update-password.component";
|
import { UpdatePasswordComponent } from "./auth/update-password.component";
|
||||||
import { UpdateTempPasswordComponent } from "./auth/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "./auth/update-temp-password.component";
|
||||||
@@ -248,10 +250,9 @@ const routes: Routes = [
|
|||||||
path: "2fa",
|
path: "2fa",
|
||||||
canActivate: [unauthGuardFn()],
|
canActivate: [unauthGuardFn()],
|
||||||
children: [
|
children: [
|
||||||
{
|
...twofactorRefactorSwap(TwoFactorComponent, TwoFactorAuthComponent, {
|
||||||
path: "",
|
path: "",
|
||||||
component: TwoFactorComponent,
|
}),
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: EnvironmentSelectorComponent,
|
component: EnvironmentSelectorComponent,
|
||||||
|
|||||||
@@ -1691,6 +1691,9 @@
|
|||||||
"twoStepAuthenticatorScanCodeV2": {
|
"twoStepAuthenticatorScanCodeV2": {
|
||||||
"message": "Scan the QR code below with your authenticator app or enter the key."
|
"message": "Scan the QR code below with your authenticator app or enter the key."
|
||||||
},
|
},
|
||||||
|
"twoStepAuthenticatorQRCanvasError": {
|
||||||
|
"message": "Could not load QR code. Try again or use the key below."
|
||||||
|
},
|
||||||
"key": {
|
"key": {
|
||||||
"message": "Key"
|
"message": "Key"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../../libs/admin-console/.eslintrc.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../../../libs/admin-console/.eslintrc.json"
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { firstValueFrom, from, map } from "rxjs";
|
import { firstValueFrom, from, map } from "rxjs";
|
||||||
import { switchMap, takeUntil } from "rxjs/operators";
|
import { switchMap, takeUntil } from "rxjs/operators";
|
||||||
@@ -33,7 +33,7 @@ const DisallowedPlanTypes = [
|
|||||||
@Component({
|
@Component({
|
||||||
templateUrl: "clients.component.html",
|
templateUrl: "clients.component.html",
|
||||||
})
|
})
|
||||||
export class ClientsComponent extends BaseClientsComponent {
|
export class ClientsComponent extends BaseClientsComponent implements OnInit, OnDestroy {
|
||||||
providerId: string;
|
providerId: string;
|
||||||
addableOrganizations: Organization[];
|
addableOrganizations: Organization[];
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { lastValueFrom } from "rxjs";
|
import { lastValueFrom } from "rxjs";
|
||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
@@ -34,7 +34,10 @@ import { UserAddEditComponent } from "./user-add-edit.component";
|
|||||||
templateUrl: "people.component.html",
|
templateUrl: "people.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetailsResponse> {
|
export class PeopleComponent
|
||||||
|
extends BasePeopleComponent<ProviderUserUserDetailsResponse>
|
||||||
|
implements OnInit
|
||||||
|
{
|
||||||
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||||
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
||||||
groupsModalRef: ViewContainerRef;
|
groupsModalRef: ViewContainerRef;
|
||||||
@@ -154,11 +157,11 @@ export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetails
|
|||||||
comp.name = this.userNamePipe.transform(user);
|
comp.name = this.userNamePipe.transform(user);
|
||||||
comp.providerId = this.providerId;
|
comp.providerId = this.providerId;
|
||||||
comp.providerUserId = user != null ? user.id : null;
|
comp.providerUserId = user != null ? user.id : null;
|
||||||
comp.onSavedUser.subscribe(() => {
|
comp.savedUser.subscribe(() => {
|
||||||
modal.close();
|
modal.close();
|
||||||
this.load();
|
this.load();
|
||||||
});
|
});
|
||||||
comp.onDeletedUser.subscribe(() => {
|
comp.deletedUser.subscribe(() => {
|
||||||
modal.close();
|
modal.close();
|
||||||
this.removeUser(user);
|
this.removeUser(user);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export class UserAddEditComponent implements OnInit {
|
|||||||
@Input() name: string;
|
@Input() name: string;
|
||||||
@Input() providerUserId: string;
|
@Input() providerUserId: string;
|
||||||
@Input() providerId: string;
|
@Input() providerId: string;
|
||||||
@Output() onSavedUser = new EventEmitter();
|
@Output() savedUser = new EventEmitter();
|
||||||
@Output() onDeletedUser = new EventEmitter();
|
@Output() deletedUser = new EventEmitter();
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
editMode = false;
|
editMode = false;
|
||||||
@@ -82,7 +82,7 @@ export class UserAddEditComponent implements OnInit {
|
|||||||
null,
|
null,
|
||||||
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name),
|
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name),
|
||||||
);
|
);
|
||||||
this.onSavedUser.emit();
|
this.savedUser.emit();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ export class UserAddEditComponent implements OnInit {
|
|||||||
null,
|
null,
|
||||||
this.i18nService.t("removedUserId", this.name),
|
this.i18nService.t("removedUserId", this.name),
|
||||||
);
|
);
|
||||||
this.onDeletedUser.emit();
|
this.deletedUser.emit();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
|
|||||||
22
libs/admin-console/.eslintrc.json
Normal file
22
libs/admin-console/.eslintrc.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts"],
|
||||||
|
"extends": ["plugin:@angular-eslint/recommended"],
|
||||||
|
"rules": {
|
||||||
|
"@angular-eslint/component-class-suffix": "error",
|
||||||
|
"@angular-eslint/contextual-lifecycle": "error",
|
||||||
|
"@angular-eslint/directive-class-suffix": "error",
|
||||||
|
"@angular-eslint/no-empty-lifecycle-method": "error",
|
||||||
|
"@angular-eslint/no-input-rename": "error",
|
||||||
|
"@angular-eslint/no-inputs-metadata-property": "error",
|
||||||
|
"@angular-eslint/no-output-native": "error",
|
||||||
|
"@angular-eslint/no-output-on-prefix": "error",
|
||||||
|
"@angular-eslint/no-output-rename": "error",
|
||||||
|
"@angular-eslint/no-outputs-metadata-property": "error",
|
||||||
|
"@angular-eslint/use-lifecycle-interface": "error",
|
||||||
|
"@angular-eslint/use-pipe-transform-interface": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<ng-container>
|
||||||
|
<p bitTypography="body1">
|
||||||
|
{{ "enterVerificationCodeApp" | i18n }}
|
||||||
|
</p>
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "verificationCode" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
type="text"
|
||||||
|
appAutofocus
|
||||||
|
appInputVerbatim
|
||||||
|
[(ngModel)]="tokenValue"
|
||||||
|
(input)="token.emit(tokenValue)"
|
||||||
|
/>
|
||||||
|
</bit-form-field>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { DialogModule } from "@angular/cdk/dialog";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, EventEmitter, Output } from "@angular/core";
|
||||||
|
import { ReactiveFormsModule, FormsModule } from "@angular/forms";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
|
import {
|
||||||
|
ButtonModule,
|
||||||
|
LinkModule,
|
||||||
|
TypographyModule,
|
||||||
|
FormFieldModule,
|
||||||
|
AsyncActionsModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "app-two-factor-auth-authenticator",
|
||||||
|
templateUrl: "two-factor-auth-authenticator.component.html",
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
JslibModule,
|
||||||
|
DialogModule,
|
||||||
|
ButtonModule,
|
||||||
|
LinkModule,
|
||||||
|
TypographyModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormFieldModule,
|
||||||
|
AsyncActionsModule,
|
||||||
|
FormsModule,
|
||||||
|
],
|
||||||
|
providers: [I18nPipe],
|
||||||
|
})
|
||||||
|
export class TwoFactorAuthAuthenticatorComponent {
|
||||||
|
tokenValue: string;
|
||||||
|
@Output() token = new EventEmitter<string>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<form [bitSubmit]="submitForm" [formGroup]="formGroup" autocomplete="off">
|
||||||
|
<div class="tw-min-w-96">
|
||||||
|
<app-two-factor-auth-authenticator
|
||||||
|
(token)="token = $event"
|
||||||
|
*ngIf="selectedProviderType === providerType.Authenticator"
|
||||||
|
/>
|
||||||
|
<bit-form-control *ngIf="selectedProviderType != null">
|
||||||
|
<bit-label>{{ "rememberMe" | i18n }}</bit-label>
|
||||||
|
<input type="checkbox" bitCheckbox formControlName="remember" />
|
||||||
|
</bit-form-control>
|
||||||
|
<ng-container *ngIf="selectedProviderType == null">
|
||||||
|
<p bitTypography="body1">{{ "noTwoStepProviders" | i18n }}</p>
|
||||||
|
<p bitTypography="body1">{{ "noTwoStepProviders2" | i18n }}</p>
|
||||||
|
</ng-container>
|
||||||
|
<hr />
|
||||||
|
<div [hidden]="!showCaptcha()">
|
||||||
|
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||||
|
</div>
|
||||||
|
<!-- <!-- Buttons -->
|
||||||
|
<div class="tw-flex tw-flex-col tw-space-y-2.5 tw-mb-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
buttonType="primary"
|
||||||
|
bitButton
|
||||||
|
bitFormButton
|
||||||
|
*ngIf="selectedProviderType != null"
|
||||||
|
>
|
||||||
|
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ actionButtonText }} </span>
|
||||||
|
</button>
|
||||||
|
<a routerLink="/login" bitButton buttonType="secondary">
|
||||||
|
{{ "cancel" | i18n }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<a bitLink href="#" appStopClick (click)="selectOtherTwofactorMethod()">{{
|
||||||
|
"useAnotherTwoStepMethod" | i18n
|
||||||
|
}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
|
import { ActivatedRoute, convertToParamMap, Router } from "@angular/router";
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||||
|
import {
|
||||||
|
LoginStrategyServiceAbstraction,
|
||||||
|
LoginEmailServiceAbstraction,
|
||||||
|
FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption,
|
||||||
|
FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption,
|
||||||
|
FakeUserDecryptionOptions as UserDecryptionOptions,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||||
|
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||||
|
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||||
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import {
|
||||||
|
Environment,
|
||||||
|
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";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { TwoFactorAuthComponent } from "./two-factor-auth.component";
|
||||||
|
|
||||||
|
// test component that extends the TwoFactorAuthComponent
|
||||||
|
@Component({})
|
||||||
|
class TestTwoFactorComponent extends TwoFactorAuthComponent {}
|
||||||
|
|
||||||
|
interface TwoFactorComponentProtected {
|
||||||
|
trustedDeviceEncRoute: string;
|
||||||
|
changePasswordRoute: string;
|
||||||
|
forcePasswordResetRoute: string;
|
||||||
|
successRoute: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("TwoFactorComponent", () => {
|
||||||
|
let component: TestTwoFactorComponent;
|
||||||
|
let _component: TwoFactorComponentProtected;
|
||||||
|
|
||||||
|
let fixture: ComponentFixture<TestTwoFactorComponent>;
|
||||||
|
const userId = "userId" as UserId;
|
||||||
|
|
||||||
|
// Mock Services
|
||||||
|
let mockLoginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
|
||||||
|
let mockRouter: MockProxy<Router>;
|
||||||
|
let mockI18nService: MockProxy<I18nService>;
|
||||||
|
let mockApiService: MockProxy<ApiService>;
|
||||||
|
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
|
||||||
|
let mockWin: MockProxy<Window>;
|
||||||
|
let mockEnvironmentService: MockProxy<EnvironmentService>;
|
||||||
|
let mockStateService: MockProxy<StateService>;
|
||||||
|
let mockLogService: MockProxy<LogService>;
|
||||||
|
let mockTwoFactorService: MockProxy<TwoFactorService>;
|
||||||
|
let mockAppIdService: MockProxy<AppIdService>;
|
||||||
|
let mockLoginEmailService: MockProxy<LoginEmailServiceAbstraction>;
|
||||||
|
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||||
|
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||||
|
let mockConfigService: MockProxy<ConfigService>;
|
||||||
|
let mockMasterPasswordService: FakeMasterPasswordService;
|
||||||
|
let mockAccountService: FakeAccountService;
|
||||||
|
let mockDialogService: MockProxy<DialogService>;
|
||||||
|
|
||||||
|
let mockUserDecryptionOpts: {
|
||||||
|
noMasterPassword: UserDecryptionOptions;
|
||||||
|
withMasterPassword: UserDecryptionOptions;
|
||||||
|
withMasterPasswordAndTrustedDevice: UserDecryptionOptions;
|
||||||
|
withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
|
||||||
|
withMasterPasswordAndKeyConnector: UserDecryptionOptions;
|
||||||
|
noMasterPasswordWithTrustedDevice: UserDecryptionOptions;
|
||||||
|
noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions;
|
||||||
|
noMasterPasswordWithKeyConnector: UserDecryptionOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
let selectedUserDecryptionOptions: BehaviorSubject<UserDecryptionOptions>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
|
||||||
|
mockRouter = mock<Router>();
|
||||||
|
mockI18nService = mock<I18nService>();
|
||||||
|
mockApiService = mock<ApiService>();
|
||||||
|
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
mockWin = mock<Window>();
|
||||||
|
const mockEnvironment = mock<Environment>();
|
||||||
|
mockEnvironment.getWebVaultUrl.mockReturnValue("http://example.com");
|
||||||
|
mockEnvironmentService = mock<EnvironmentService>();
|
||||||
|
mockEnvironmentService.environment$ = new BehaviorSubject(mockEnvironment);
|
||||||
|
|
||||||
|
mockStateService = mock<StateService>();
|
||||||
|
mockLogService = mock<LogService>();
|
||||||
|
mockTwoFactorService = mock<TwoFactorService>();
|
||||||
|
mockAppIdService = mock<AppIdService>();
|
||||||
|
mockLoginEmailService = mock<LoginEmailServiceAbstraction>();
|
||||||
|
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||||
|
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||||
|
mockConfigService = mock<ConfigService>();
|
||||||
|
mockAccountService = mockAccountServiceWith(userId);
|
||||||
|
mockMasterPasswordService = new FakeMasterPasswordService();
|
||||||
|
mockDialogService = mock<DialogService>();
|
||||||
|
|
||||||
|
mockUserDecryptionOpts = {
|
||||||
|
noMasterPassword: new UserDecryptionOptions({
|
||||||
|
hasMasterPassword: false,
|
||||||
|
trustedDeviceOption: undefined,
|
||||||
|
keyConnectorOption: undefined,
|
||||||
|
}),
|
||||||
|
withMasterPassword: new UserDecryptionOptions({
|
||||||
|
hasMasterPassword: true,
|
||||||
|
trustedDeviceOption: undefined,
|
||||||
|
keyConnectorOption: undefined,
|
||||||
|
}),
|
||||||
|
withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({
|
||||||
|
hasMasterPassword: true,
|
||||||
|
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||||
|
keyConnectorOption: undefined,
|
||||||
|
}),
|
||||||
|
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
|
||||||
|
hasMasterPassword: true,
|
||||||
|
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||||
|
keyConnectorOption: undefined,
|
||||||
|
}),
|
||||||
|
withMasterPasswordAndKeyConnector: new UserDecryptionOptions({
|
||||||
|
hasMasterPassword: true,
|
||||||
|
trustedDeviceOption: undefined,
|
||||||
|
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||||
|
}),
|
||||||
|
noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({
|
||||||
|
hasMasterPassword: false,
|
||||||
|
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||||
|
keyConnectorOption: undefined,
|
||||||
|
}),
|
||||||
|
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({
|
||||||
|
hasMasterPassword: false,
|
||||||
|
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||||
|
keyConnectorOption: undefined,
|
||||||
|
}),
|
||||||
|
noMasterPasswordWithKeyConnector: new UserDecryptionOptions({
|
||||||
|
hasMasterPassword: false,
|
||||||
|
trustedDeviceOption: undefined,
|
||||||
|
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
|
||||||
|
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [TestTwoFactorComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: LoginStrategyServiceAbstraction, useValue: mockLoginStrategyService },
|
||||||
|
{ provide: Router, useValue: mockRouter },
|
||||||
|
{ provide: I18nService, useValue: mockI18nService },
|
||||||
|
{ provide: ApiService, useValue: mockApiService },
|
||||||
|
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||||
|
{ provide: WINDOW, useValue: mockWin },
|
||||||
|
{ provide: EnvironmentService, useValue: mockEnvironmentService },
|
||||||
|
{ provide: StateService, useValue: mockStateService },
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: {
|
||||||
|
// Default to standard 2FA flow - not SSO + 2FA
|
||||||
|
queryParamMap: convertToParamMap({ sso: "false" }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ provide: LogService, useValue: mockLogService },
|
||||||
|
{ provide: TwoFactorService, useValue: mockTwoFactorService },
|
||||||
|
{ provide: AppIdService, useValue: mockAppIdService },
|
||||||
|
{ provide: LoginEmailServiceAbstraction, useValue: mockLoginEmailService },
|
||||||
|
{
|
||||||
|
provide: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
useValue: mockUserDecryptionOptionsService,
|
||||||
|
},
|
||||||
|
{ provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService },
|
||||||
|
{ provide: ConfigService, useValue: mockConfigService },
|
||||||
|
{ provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
|
||||||
|
{ provide: AccountService, useValue: mockAccountService },
|
||||||
|
{ provide: DialogService, useValue: mockDialogService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(TestTwoFactorComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
_component = component as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Reset all mocks after each test
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shared tests
|
||||||
|
const testChangePasswordOnSuccessfulLogin = () => {
|
||||||
|
it("navigates to the component's defined change password route when user doesn't have a MP and key connector isn't enabled", async () => {
|
||||||
|
// Act
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.changePasswordRoute], {
|
||||||
|
queryParams: {
|
||||||
|
identifier: component.orgIdentifier,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testForceResetOnSuccessfulLogin = (reasonString: string) => {
|
||||||
|
it(`navigates to the component's defined forcePasswordResetRoute route when response.forcePasswordReset is ${reasonString}`, async () => {
|
||||||
|
// Act
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
// expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.forcePasswordResetRoute], {
|
||||||
|
queryParams: {
|
||||||
|
identifier: component.orgIdentifier,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Standard 2FA scenarios", () => {
|
||||||
|
describe("submit", () => {
|
||||||
|
const token = "testToken";
|
||||||
|
const remember = false;
|
||||||
|
const captchaToken = "testCaptchaToken";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component.token = token;
|
||||||
|
component.remember = remember;
|
||||||
|
component.captchaToken = captchaToken;
|
||||||
|
|
||||||
|
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls authService.logInTwoFactor with correct parameters when form is submitted", async () => {
|
||||||
|
// Arrange
|
||||||
|
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockLoginStrategyService.logInTwoFactor).toHaveBeenCalledWith(
|
||||||
|
new TokenTwoFactorRequest(component.selectedProviderType, token, remember),
|
||||||
|
captchaToken,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return when handleCaptchaRequired returns true", async () => {
|
||||||
|
// Arrange
|
||||||
|
const captchaSiteKey = "testCaptchaSiteKey";
|
||||||
|
const authResult = new AuthResult();
|
||||||
|
authResult.captchaSiteKey = captchaSiteKey;
|
||||||
|
|
||||||
|
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||||
|
|
||||||
|
// Note: the any casts are required b/c typescript cant recognize that
|
||||||
|
// handleCaptureRequired is a method on TwoFactorComponent b/c it is inherited
|
||||||
|
// from the CaptchaProtectedComponent
|
||||||
|
const handleCaptchaRequiredSpy = jest
|
||||||
|
.spyOn<any, any>(component, "handleCaptchaRequired")
|
||||||
|
.mockReturnValue(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await component.submit();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(handleCaptchaRequiredSpy).toHaveBeenCalled();
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onSuccessfulLogin when defined", async () => {
|
||||||
|
// Arrange
|
||||||
|
component.onSuccessfulLogin = jest.fn().mockResolvedValue(undefined);
|
||||||
|
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.onSuccessfulLogin).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls loginEmailService.clearValues() when login is successful", async () => {
|
||||||
|
// Arrange
|
||||||
|
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||||
|
// spy on loginEmailService.clearValues
|
||||||
|
const clearValuesSpy = jest.spyOn(mockLoginEmailService, "clearValues");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(clearValuesSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Set Master Password scenarios", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const authResult = new AuthResult();
|
||||||
|
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Given user needs to set a master password", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Only need to test the case where the user has no master password to test the primary change mp flow here
|
||||||
|
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
|
||||||
|
});
|
||||||
|
|
||||||
|
testChangePasswordOnSuccessfulLogin();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
|
||||||
|
selectedUserDecryptionOptions.next(
|
||||||
|
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
|
||||||
|
);
|
||||||
|
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
expect(mockRouter.navigate).not.toHaveBeenCalledWith([_component.changePasswordRoute], {
|
||||||
|
queryParams: {
|
||||||
|
identifier: component.orgIdentifier,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Force Master Password Reset scenarios", () => {
|
||||||
|
[
|
||||||
|
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||||
|
ForceSetPasswordReason.WeakMasterPassword,
|
||||||
|
].forEach((forceResetPasswordReason) => {
|
||||||
|
const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// use standard user with MP because this test is not concerned with password reset.
|
||||||
|
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
||||||
|
|
||||||
|
const authResult = new AuthResult();
|
||||||
|
authResult.forcePasswordReset = forceResetPasswordReason;
|
||||||
|
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
testForceResetOnSuccessfulLogin(reasonString);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onSuccessfulLoginNavigate when the callback is defined", async () => {
|
||||||
|
// Arrange
|
||||||
|
component.onSuccessfulLoginNavigate = jest.fn().mockResolvedValue(undefined);
|
||||||
|
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.onSuccessfulLoginNavigate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("navigates to the component's defined success route when the login is successful and onSuccessfulLoginNavigate is undefined", async () => {
|
||||||
|
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(component.onSuccessfulLoginNavigate).not.toBeDefined();
|
||||||
|
|
||||||
|
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.successRoute], undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("SSO > 2FA scenarios", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockActivatedRoute = TestBed.inject(ActivatedRoute);
|
||||||
|
mockActivatedRoute.snapshot.queryParamMap.get = jest.fn().mockReturnValue("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("submit", () => {
|
||||||
|
const token = "testToken";
|
||||||
|
const remember = false;
|
||||||
|
const captchaToken = "testCaptchaToken";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component.token = token;
|
||||||
|
component.remember = remember;
|
||||||
|
component.captchaToken = captchaToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Trusted Device Encryption scenarios", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
selectedUserDecryptionOptions.next(
|
||||||
|
mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword,
|
||||||
|
);
|
||||||
|
|
||||||
|
const authResult = new AuthResult();
|
||||||
|
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("navigates to the component's defined trusted device encryption route and sets correct flag when user doesn't have a MP and key connector isn't enabled", async () => {
|
||||||
|
// Act
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||||
|
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockRouter.navigate).toHaveBeenCalledWith(
|
||||||
|
[_component.trustedDeviceEncRoute],
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is required", () => {
|
||||||
|
[
|
||||||
|
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||||
|
ForceSetPasswordReason.WeakMasterPassword,
|
||||||
|
].forEach((forceResetPasswordReason) => {
|
||||||
|
const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// use standard user with MP because this test is not concerned with password reset.
|
||||||
|
selectedUserDecryptionOptions.next(
|
||||||
|
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||||
|
);
|
||||||
|
|
||||||
|
const authResult = new AuthResult();
|
||||||
|
authResult.forcePasswordReset = forceResetPasswordReason;
|
||||||
|
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
testForceResetOnSuccessfulLogin(reasonString);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
|
||||||
|
let authResult;
|
||||||
|
beforeEach(() => {
|
||||||
|
selectedUserDecryptionOptions.next(
|
||||||
|
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||||
|
);
|
||||||
|
|
||||||
|
authResult = new AuthResult();
|
||||||
|
authResult.forcePasswordReset = ForceSetPasswordReason.None;
|
||||||
|
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("navigates to the component's defined trusted device encryption route when login is successful and onSuccessfulLoginTdeNavigate is undefined", async () => {
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockRouter.navigate).toHaveBeenCalledWith(
|
||||||
|
[_component.trustedDeviceEncRoute],
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onSuccessfulLoginTdeNavigate instead of router.navigate when the callback is defined", async () => {
|
||||||
|
component.onSuccessfulLoginTdeNavigate = jest.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await component.submit();
|
||||||
|
|
||||||
|
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||||
|
expect(component.onSuccessfulLoginTdeNavigate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, Inject, OnInit } from "@angular/core";
|
||||||
|
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||||
|
import { ActivatedRoute, NavigationExtras, Router, RouterLink } from "@angular/router";
|
||||||
|
import { Subject, takeUntil, lastValueFrom, first, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
|
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||||
|
import {
|
||||||
|
LoginStrategyServiceAbstraction,
|
||||||
|
LoginEmailServiceAbstraction,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
TrustedDeviceUserDecryptionOption,
|
||||||
|
UserDecryptionOptions,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
|
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||||
|
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||||
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||||
|
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||||
|
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import {
|
||||||
|
AsyncActionsModule,
|
||||||
|
ButtonModule,
|
||||||
|
DialogService,
|
||||||
|
FormFieldModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { CaptchaProtectedComponent } from "../captcha-protected.component";
|
||||||
|
|
||||||
|
import { TwoFactorAuthAuthenticatorComponent } from "./two-factor-auth-authenticator.component";
|
||||||
|
import {
|
||||||
|
TwoFactorOptionsDialogResult,
|
||||||
|
TwoFactorOptionsComponent,
|
||||||
|
TwoFactorOptionsDialogResultType,
|
||||||
|
} from "./two-factor-options.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "app-two-factor-auth",
|
||||||
|
templateUrl: "two-factor-auth.component.html",
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
JslibModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormFieldModule,
|
||||||
|
AsyncActionsModule,
|
||||||
|
RouterLink,
|
||||||
|
ButtonModule,
|
||||||
|
TwoFactorOptionsComponent,
|
||||||
|
TwoFactorAuthAuthenticatorComponent,
|
||||||
|
],
|
||||||
|
providers: [I18nPipe],
|
||||||
|
})
|
||||||
|
export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements OnInit {
|
||||||
|
token = "";
|
||||||
|
remember = false;
|
||||||
|
orgIdentifier: string = null;
|
||||||
|
|
||||||
|
providers = TwoFactorProviders;
|
||||||
|
providerType = TwoFactorProviderType;
|
||||||
|
selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator;
|
||||||
|
providerData: any;
|
||||||
|
|
||||||
|
formGroup = this.formBuilder.group({
|
||||||
|
token: [
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
validators: [Validators.required],
|
||||||
|
updateOn: "submit",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
remember: [false],
|
||||||
|
});
|
||||||
|
actionButtonText = "";
|
||||||
|
title = "";
|
||||||
|
formPromise: Promise<any>;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
onSuccessfulLogin: () => Promise<void>;
|
||||||
|
onSuccessfulLoginNavigate: () => Promise<void>;
|
||||||
|
|
||||||
|
onSuccessfulLoginTde: () => Promise<void>;
|
||||||
|
onSuccessfulLoginTdeNavigate: () => Promise<void>;
|
||||||
|
|
||||||
|
submitForm = async () => {
|
||||||
|
await this.submit();
|
||||||
|
};
|
||||||
|
goAfterLogIn = async () => {
|
||||||
|
this.loginEmailService.clearValues();
|
||||||
|
// 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([this.successRoute], {
|
||||||
|
queryParams: {
|
||||||
|
identifier: this.orgIdentifier,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
protected loginRoute = "login";
|
||||||
|
|
||||||
|
protected trustedDeviceEncRoute = "login-initiated";
|
||||||
|
protected changePasswordRoute = "set-password";
|
||||||
|
protected forcePasswordResetRoute = "update-temp-password";
|
||||||
|
protected successRoute = "vault";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||||
|
protected router: Router,
|
||||||
|
i18nService: I18nService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
private logService: LogService,
|
||||||
|
protected twoFactorService: TwoFactorService,
|
||||||
|
private loginEmailService: LoginEmailServiceAbstraction,
|
||||||
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||||
|
protected configService: ConfigService,
|
||||||
|
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
|
private accountService: AccountService,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
@Inject(WINDOW) protected win: Window,
|
||||||
|
) {
|
||||||
|
super(environmentService, i18nService, platformUtilsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
if (!(await this.authing()) || (await this.twoFactorService.getProviders()) == null) {
|
||||||
|
// 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([this.loginRoute]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
|
this.route.queryParams.pipe(first()).subscribe((qParams) => {
|
||||||
|
if (qParams.identifier != null) {
|
||||||
|
this.orgIdentifier = qParams.identifier;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (await this.needsLock()) {
|
||||||
|
this.successRoute = "lock";
|
||||||
|
}
|
||||||
|
|
||||||
|
const webAuthnSupported = this.platformUtilsService.supportsWebAuthn(this.win);
|
||||||
|
this.selectedProviderType = await this.twoFactorService.getDefaultProvider(webAuthnSupported);
|
||||||
|
const providerData = await this.twoFactorService.getProviders().then((providers) => {
|
||||||
|
return providers.get(this.selectedProviderType);
|
||||||
|
});
|
||||||
|
this.providerData = providerData;
|
||||||
|
await this.updateUIToProviderData();
|
||||||
|
|
||||||
|
this.actionButtonText = this.i18nService.t("continue");
|
||||||
|
this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||||
|
this.token = value.token;
|
||||||
|
this.remember = value.remember;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
await this.setupCaptcha();
|
||||||
|
|
||||||
|
if (this.token == null || this.token === "") {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("verificationCodeRequired"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.formPromise = this.loginStrategyService.logInTwoFactor(
|
||||||
|
new TokenTwoFactorRequest(this.selectedProviderType, this.token, this.remember),
|
||||||
|
this.captchaToken,
|
||||||
|
);
|
||||||
|
const authResult: AuthResult = await this.formPromise;
|
||||||
|
this.logService.info("Successfully submitted two factor token");
|
||||||
|
await this.handleLoginResponse(authResult);
|
||||||
|
} catch {
|
||||||
|
this.logService.error("Error submitting two factor token");
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("invalidVerificationCode"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectOtherTwofactorMethod() {
|
||||||
|
const dialogRef = TwoFactorOptionsComponent.open(this.dialogService);
|
||||||
|
const response: TwoFactorOptionsDialogResultType = await lastValueFrom(dialogRef.closed);
|
||||||
|
if (response.result === TwoFactorOptionsDialogResult.Provider) {
|
||||||
|
const providerData = await this.twoFactorService.getProviders().then((providers) => {
|
||||||
|
return providers.get(response.type);
|
||||||
|
});
|
||||||
|
this.providerData = providerData;
|
||||||
|
this.selectedProviderType = response.type;
|
||||||
|
await this.updateUIToProviderData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleMigrateEncryptionKey(result: AuthResult): boolean {
|
||||||
|
if (!result.requiresEncryptionKeyMigration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 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(["migrate-legacy-encryption"]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUIToProviderData() {
|
||||||
|
if (this.selectedProviderType == null) {
|
||||||
|
this.title = this.i18nService.t("loginUnavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleLoginResponse(authResult: AuthResult) {
|
||||||
|
if (this.handleCaptchaRequired(authResult)) {
|
||||||
|
return;
|
||||||
|
} else if (this.handleMigrateEncryptionKey(authResult)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save off the OrgSsoIdentifier for use in the TDE flows
|
||||||
|
// - TDE login decryption options component
|
||||||
|
// - Browser SSO on extension open
|
||||||
|
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier);
|
||||||
|
this.loginEmailService.clearValues();
|
||||||
|
|
||||||
|
// note: this flow affects both TDE & standard users
|
||||||
|
if (this.isForcePasswordResetRequired(authResult)) {
|
||||||
|
return await this.handleForcePasswordReset(this.orgIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDecryptionOpts = await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||||
|
);
|
||||||
|
|
||||||
|
const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption);
|
||||||
|
|
||||||
|
if (tdeEnabled) {
|
||||||
|
return await this.handleTrustedDeviceEncryptionEnabled(
|
||||||
|
authResult,
|
||||||
|
this.orgIdentifier,
|
||||||
|
userDecryptionOpts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User must set password if they don't have one and they aren't using either TDE or key connector.
|
||||||
|
const requireSetPassword =
|
||||||
|
!userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined;
|
||||||
|
|
||||||
|
if (requireSetPassword || authResult.resetMasterPassword) {
|
||||||
|
// Change implies going no password -> password in this case
|
||||||
|
return await this.handleChangePasswordRequired(this.orgIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.handleSuccessfulLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isTrustedDeviceEncEnabled(
|
||||||
|
trustedDeviceOption: TrustedDeviceUserDecryptionOption,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const ssoTo2faFlowActive = this.route.snapshot.queryParamMap.get("sso") === "true";
|
||||||
|
|
||||||
|
return ssoTo2faFlowActive && trustedDeviceOption !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleTrustedDeviceEncryptionEnabled(
|
||||||
|
authResult: AuthResult,
|
||||||
|
orgIdentifier: string,
|
||||||
|
userDecryptionOpts: UserDecryptionOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||||
|
if (
|
||||||
|
!userDecryptionOpts.hasMasterPassword &&
|
||||||
|
userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
|
||||||
|
) {
|
||||||
|
// Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device)
|
||||||
|
// Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and
|
||||||
|
// if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key.
|
||||||
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
|
await this.masterPasswordService.setForceSetPasswordReason(
|
||||||
|
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.onSuccessfulLoginTde != null) {
|
||||||
|
// Note: awaiting this will currently cause a hang on desktop & browser as they will wait for a full sync to complete
|
||||||
|
// before navigating to the success route.
|
||||||
|
// 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.onSuccessfulLoginTde();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.navigateViaCallbackOrRoute(
|
||||||
|
this.onSuccessfulLoginTdeNavigate,
|
||||||
|
// Navigate to TDE page (if user was on trusted device and TDE has decrypted
|
||||||
|
// their user key, the login-initiated guard will redirect them to the vault)
|
||||||
|
[this.trustedDeviceEncRoute],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleChangePasswordRequired(orgIdentifier: string) {
|
||||||
|
await this.router.navigate([this.changePasswordRoute], {
|
||||||
|
queryParams: {
|
||||||
|
identifier: orgIdentifier,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a user needs to reset their password based on certain conditions.
|
||||||
|
* Users can be forced to reset their password via an admin or org policy disallowing weak passwords.
|
||||||
|
* Note: this is different from the SSO component login flow as a user can
|
||||||
|
* login with MP and then have to pass 2FA to finish login and we can actually
|
||||||
|
* evaluate if they have a weak password at that time.
|
||||||
|
*
|
||||||
|
* @param {AuthResult} authResult - The authentication result.
|
||||||
|
* @returns {boolean} Returns true if a password reset is required, false otherwise.
|
||||||
|
*/
|
||||||
|
private isForcePasswordResetRequired(authResult: AuthResult): boolean {
|
||||||
|
const forceResetReasons = [
|
||||||
|
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||||
|
ForceSetPasswordReason.WeakMasterPassword,
|
||||||
|
];
|
||||||
|
|
||||||
|
return forceResetReasons.includes(authResult.forcePasswordReset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleForcePasswordReset(orgIdentifier: string) {
|
||||||
|
// 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([this.forcePasswordResetRoute], {
|
||||||
|
queryParams: {
|
||||||
|
identifier: orgIdentifier,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleSuccessfulLogin() {
|
||||||
|
if (this.onSuccessfulLogin != null) {
|
||||||
|
// Note: awaiting this will currently cause a hang on desktop & browser as they will wait for a full sync to complete
|
||||||
|
// before navigating to the success route.
|
||||||
|
// 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.onSuccessfulLogin();
|
||||||
|
}
|
||||||
|
await this.navigateViaCallbackOrRoute(this.onSuccessfulLoginNavigate, [this.successRoute]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async navigateViaCallbackOrRoute(
|
||||||
|
callback: () => Promise<unknown>,
|
||||||
|
commands: unknown[],
|
||||||
|
extras?: NavigationExtras,
|
||||||
|
): Promise<void> {
|
||||||
|
if (callback) {
|
||||||
|
await callback();
|
||||||
|
} else {
|
||||||
|
await this.router.navigate(commands, extras);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async authing(): Promise<boolean> {
|
||||||
|
return (await firstValueFrom(this.loginStrategyService.currentAuthType$)) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async needsLock(): Promise<boolean> {
|
||||||
|
const authType = await firstValueFrom(this.loginStrategyService.currentAuthType$);
|
||||||
|
return authType == AuthenticationType.Sso || authType == AuthenticationType.UserApiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -728,6 +728,10 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: SsoLoginService,
|
useClass: SsoLoginService,
|
||||||
deps: [StateProvider],
|
deps: [StateProvider],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: STATE_FACTORY,
|
||||||
|
useValue: new StateFactory(GlobalState, Account),
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: StateServiceAbstraction,
|
provide: StateServiceAbstraction,
|
||||||
useClass: StateService,
|
useClass: StateService,
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Type, inject } from "@angular/core";
|
||||||
|
import { Route, Routes } from "@angular/router";
|
||||||
|
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
|
||||||
|
import { componentRouteSwap } from "./component-route-swap";
|
||||||
|
/**
|
||||||
|
* Helper function to swap between two components based on the TwoFactorComponentRefactor feature flag.
|
||||||
|
* @param defaultComponent - The current non-refactored component to render.
|
||||||
|
* @param refreshedComponent - The new refactored component to render.
|
||||||
|
* @param defaultOptions - The options to apply to the default component and the refactored component, if alt options are not provided.
|
||||||
|
* @param altOptions - The options to apply to the refactored component.
|
||||||
|
*/
|
||||||
|
export function twofactorRefactorSwap(
|
||||||
|
defaultComponent: Type<any>,
|
||||||
|
refreshedComponent: Type<any>,
|
||||||
|
defaultOptions: Route,
|
||||||
|
altOptions?: Route,
|
||||||
|
): Routes {
|
||||||
|
return componentRouteSwap(
|
||||||
|
defaultComponent,
|
||||||
|
refreshedComponent,
|
||||||
|
async () => {
|
||||||
|
const configService = inject(ConfigService);
|
||||||
|
return configService.getFeatureFlag(FeatureFlag.TwoFactorComponentRefactor);
|
||||||
|
},
|
||||||
|
defaultOptions,
|
||||||
|
altOptions,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,11 +5,12 @@ import { firstValueFrom } from "rxjs";
|
|||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
|
|
||||||
import { IconModule, Icon } from "../../../../components/src/icon";
|
import { IconModule, Icon } from "../../../../components/src/icon";
|
||||||
import { SharedModule } from "../../../../components/src/shared";
|
import { SharedModule } from "../../../../components/src/shared";
|
||||||
import { TypographyModule } from "../../../../components/src/typography";
|
import { TypographyModule } from "../../../../components/src/typography";
|
||||||
import { BitwardenLogo } from "../icons/bitwarden-logo.icon";
|
import { BitwardenLogoPrimary, BitwardenLogoWhite } from "../icons/bitwarden-logo.icon";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -23,18 +24,20 @@ export class AnonLayoutComponent {
|
|||||||
@Input() icon: Icon;
|
@Input() icon: Icon;
|
||||||
@Input() showReadonlyHostname: boolean;
|
@Input() showReadonlyHostname: boolean;
|
||||||
|
|
||||||
protected logo = BitwardenLogo;
|
protected logo: Icon;
|
||||||
|
|
||||||
protected year = "2024";
|
protected year = "2024";
|
||||||
protected clientType: ClientType;
|
protected clientType: ClientType;
|
||||||
protected hostname: string;
|
protected hostname: string;
|
||||||
protected version: string;
|
protected version: string;
|
||||||
|
protected theme: string;
|
||||||
|
|
||||||
protected showYearAndVersion = true;
|
protected showYearAndVersion = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private themeStateService: ThemeStateService,
|
||||||
) {
|
) {
|
||||||
this.year = new Date().getFullYear().toString();
|
this.year = new Date().getFullYear().toString();
|
||||||
this.clientType = this.platformUtilsService.getClientType();
|
this.clientType = this.platformUtilsService.getClientType();
|
||||||
@@ -44,5 +47,12 @@ export class AnonLayoutComponent {
|
|||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname();
|
this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname();
|
||||||
this.version = await this.platformUtilsService.getApplicationVersion();
|
this.version = await this.platformUtilsService.getApplicationVersion();
|
||||||
|
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
|
||||||
|
|
||||||
|
if (this.theme === "dark") {
|
||||||
|
this.logo = BitwardenLogoWhite;
|
||||||
|
} else {
|
||||||
|
this.logo = BitwardenLogoPrimary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import * as stories from "./anon-layout.stories";
|
|||||||
|
|
||||||
# AnonLayout Component
|
# AnonLayout Component
|
||||||
|
|
||||||
The Auth-owned AnonLayoutComponent is to be used for unauthenticated pages, where we don't know who
|
The Auth-owned AnonLayoutComponent is to be used primarily for unauthenticated pages\*, where we
|
||||||
the user is (this includes viewing a Send).
|
don't know who the user is.
|
||||||
|
|
||||||
|
\*There will be a few exceptions to this—that is, AnonLayout will also be used for the Unlock
|
||||||
|
and View Send pages.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
|
|
||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
|
|
||||||
import { ButtonModule } from "../../../../components/src/button";
|
import { ButtonModule } from "../../../../components/src/button";
|
||||||
import { I18nMockService } from "../../../../components/src/utils/i18n-mock.service";
|
import { I18nMockService } from "../../../../components/src/utils/i18n-mock.service";
|
||||||
@@ -46,6 +47,12 @@ export default {
|
|||||||
}).asObservable(),
|
}).asObservable(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: ThemeStateService,
|
||||||
|
useValue: {
|
||||||
|
selectedTheme$: of("light"),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { svgIcon } from "@bitwarden/components";
|
import { svgIcon } from "@bitwarden/components";
|
||||||
|
|
||||||
export const BitwardenLogo = svgIcon`
|
export const BitwardenLogoPrimary = svgIcon`
|
||||||
<svg viewBox="0 0 290 45" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 290 45" fill="#175DDC" xmlns="http://www.w3.org/2000/svg">
|
||||||
<title>Bitwarden</title>
|
<title>Bitwarden</title>
|
||||||
<path class="tw-fill-primary-600" fill-rule="evenodd" clip-rule="evenodd" d="M69.799 10.713c3.325 0 5.911 1.248 7.811 3.848 1.9 2.549 2.85 6.033 2.85 10.453 0 4.576-.95 8.113-2.902 10.61-1.953 2.547-4.592 3.743-7.918 3.743-3.325 0-5.858-1.144-7.758-3.536h-.528l-1.003 2.444a.976.976 0 0 1-.897.572H55.23a.94.94 0 0 1-.95-.936V1.352a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v8.009c0 1.144-.105 2.964-.316 5.46h.317c1.741-2.704 4.433-4.108 7.917-4.108Zm-2.428 6.084c-1.847 0-3.273.572-4.17 1.717-.844 1.144-1.32 3.068-1.32 5.668v.832c0 2.964.423 5.097 1.32 6.345.897 1.248 2.322 1.924 4.275 1.924 1.531 0 2.85-.728 3.748-2.184.897-1.404 1.372-3.537 1.372-6.189 0-2.704-.475-4.732-1.372-6.084-.95-1.352-2.27-2.029-3.853-2.029ZM93.022 38.9h-5.7a.94.94 0 0 1-.95-.936V12.221a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v25.69c.053.468-.422.988-.95.988Zm20.849-5.564c1.108 0 2.428-.208 4.011-.624a.632.632 0 0 1 .792.624v4.316a.64.64 0 0 1-.37.572c-1.794.728-4.064 1.092-6.597 1.092-3.062 0-5.278-.728-6.651-2.288-1.372-1.508-2.111-3.796-2.111-6.812V16.953h-3.008c-.37 0-.634-.26-.634-.624v-2.444c0-.052.053-.104.053-.156l4.17-2.444 2.058-5.408c.106-.26.317-.417.581-.417h3.8c.369 0 .633.26.633.625v5.252h7.548c.158 0 .317.156.317.312v4.68c0 .364-.264.624-.634.624h-7.178v13.21c0 1.04.317 1.872.897 2.34.528.572 1.373.832 2.323.832Zm35.521 5.564c-.739 0-1.319-.468-1.636-1.144l-5.595-16.797c-.369-1.196-.844-3.016-1.478-5.357h-.158l-.528 1.873-1.108 3.536-5.753 16.797c-.211.676-.845 1.092-1.584 1.092a1.628 1.628 0 0 1-1.583-1.196l-7.02-24.182c-.211-.728.369-1.508 1.214-1.508h.158c.528 0 1.003.364 1.161.884l4.117 14.717c1.003 3.849 1.689 6.657 2.006 8.53h.158c.95-3.85 1.689-6.397 2.164-7.698l5.331-15.393c.211-.624.792-1.04 1.531-1.04.686 0 1.267.416 1.478 1.04l4.961 15.29c1.214 3.9 1.953 6.396 2.217 7.696h.158c.159-1.04.792-3.952 2.006-8.633l3.958-14.509c.159-.52.634-.884 1.162-.884.791 0 1.372.728 1.161 1.508l-6.651 24.182c-.211.728-.844 1.196-1.636 1.196h-.211Zm31.352 0a.962.962 0 0 1-.95-.832l-.475-3.432h-.264c-1.372 1.716-2.745 2.964-4.223 3.692-1.425.728-3.166 1.04-5.119 1.04-2.692 0-4.751-.676-6.228-2.028-1.32-1.196-2.059-2.808-2.164-4.836-.212-2.704.95-5.305 3.166-6.813 2.27-1.456 5.437-2.34 9.712-2.34l5.173-.156v-1.768c0-2.6-.528-4.473-1.637-5.773-1.108-1.3-2.744-1.924-5.067-1.924-2.216 0-4.433.52-6.756 1.612-.58.26-1.266 0-1.53-.572s0-1.248.58-1.456c2.639-1.04 5.226-1.612 7.865-1.612 3.008 0 5.225.78 6.756 2.34 1.478 1.508 2.216 3.953 2.216 7.125v16.901c-.052.312-.527.832-1.055.832Zm-10.926-1.768c2.956 0 5.226-.832 6.862-2.444 1.689-1.612 2.533-3.952 2.533-6.813v-2.6l-4.75.208c-3.853.156-6.545.78-8.234 1.768-1.636.988-2.481 2.6-2.481 4.68 0 1.665.528 3.017 1.531 3.953 1.161.78 2.639 1.248 4.539 1.248Zm31.246-25.638c.792 0 1.584.052 2.481.156a1.176 1.176 0 0 1 1.003 1.352c-.106.624-.739.988-1.372.884-.792-.104-1.584-.208-2.375-.208-2.323 0-4.223.988-5.701 2.912-1.478 1.925-2.217 4.42-2.217 7.333v13.625c0 .676-.527 1.196-1.214 1.196-.686 0-1.213-.52-1.213-1.196V13.105c0-.572.475-1.04 1.055-1.04.581 0 1.056.416 1.056.988l.211 3.848h.158c1.109-1.976 2.323-3.38 3.589-4.16 1.214-.832 2.745-1.248 4.539-1.248Zm18.579 0c1.953 0 3.695.364 5.12 1.04 1.478.676 2.745 1.924 3.853 3.64h.158a122.343 122.343 0 0 1-.158-6.084V1.612c0-.676.528-1.196 1.214-1.196.686 0 1.214.52 1.214 1.196v36.351c0 .468-.37.832-.845.832a.852.852 0 0 1-.844-.78l-.528-3.38h-.211c-2.058 3.068-5.067 4.576-8.92 4.576-3.8 0-6.598-1.144-8.656-3.484-1.953-2.34-3.008-5.668-3.008-10.089 0-4.628.95-8.165 2.955-10.66 2.006-2.237 4.856-3.485 8.656-3.485Zm0 2.236c-3.008 0-5.225 1.04-6.756 3.12-1.478 2.029-2.216 4.993-2.216 8.945 0 7.593 3.008 11.39 9.025 11.39 3.114 0 5.331-.885 6.756-2.653 1.478-1.768 2.164-4.68 2.164-8.737v-.416c0-4.16-.686-7.124-2.164-8.893-1.372-1.872-3.642-2.756-6.809-2.756Zm31.616 25.638c-3.959 0-7.02-1.196-9.289-3.64-2.217-2.392-3.326-5.772-3.326-10.089 0-4.316 1.056-7.748 3.22-10.297 2.164-2.6 5.014-3.9 8.656-3.9 3.167 0 5.753 1.092 7.548 3.276 1.9 2.184 2.797 5.2 2.797 8.997v1.976h-19.634c.052 3.692.897 6.5 2.639 8.477 1.741 1.976 4.169 2.86 7.389 2.86 1.531 0 2.956-.104 4.117-.312.844-.156 1.847-.416 3.061-.832.686-.26 1.425.26 1.425.988 0 .416-.264.832-.686.988-1.267.52-2.481.832-3.589 1.04-1.32.364-2.745.468-4.328.468Zm-.739-25.69c-2.639 0-4.75.832-6.334 2.548-1.583 1.665-2.48 4.16-2.797 7.333h16.89c0-3.068-.686-5.564-2.059-7.28-1.372-1.717-3.272-2.6-5.7-2.6ZM288.733 38.9c-.686 0-1.214-.52-1.214-1.196V21.426c0-2.704-.58-4.68-1.689-5.877-1.214-1.196-2.955-1.872-5.383-1.872-3.273 0-5.648.78-7.126 2.444-1.478 1.613-2.322 4.265-2.322 7.853V37.6c0 .676-.528 1.196-1.214 1.196-.686 0-1.214-.52-1.214-1.196V13.105c0-.624.475-1.092 1.108-1.092.581 0 1.003.416 1.109.936l.316 2.704h.159c1.794-2.808 4.908-4.212 9.448-4.212 6.175 0 9.289 3.276 9.289 9.829V37.6c-.053.727-.633 1.3-1.267 1.3ZM90.225 0c-2.48 0-4.486 1.872-4.486 4.212v.416c0 2.289 2.058 4.213 4.486 4.213s4.486-1.924 4.486-4.213v-.364C94.711 1.872 92.653 0 90.225 0Z" />
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M69.799 10.713c3.325 0 5.911 1.248 7.811 3.848 1.9 2.549 2.85 6.033 2.85 10.453 0 4.576-.95 8.113-2.902 10.61-1.953 2.547-4.592 3.743-7.918 3.743-3.325 0-5.858-1.144-7.758-3.536h-.528l-1.003 2.444a.976.976 0 0 1-.897.572H55.23a.94.94 0 0 1-.95-.936V1.352a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v8.009c0 1.144-.105 2.964-.316 5.46h.317c1.741-2.704 4.433-4.108 7.917-4.108Zm-2.428 6.084c-1.847 0-3.273.572-4.17 1.717-.844 1.144-1.32 3.068-1.32 5.668v.832c0 2.964.423 5.097 1.32 6.345.897 1.248 2.322 1.924 4.275 1.924 1.531 0 2.85-.728 3.748-2.184.897-1.404 1.372-3.537 1.372-6.189 0-2.704-.475-4.732-1.372-6.084-.95-1.352-2.27-2.029-3.853-2.029ZM93.022 38.9h-5.7a.94.94 0 0 1-.95-.936V12.221a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v25.69c.053.468-.422.988-.95.988Zm20.849-5.564c1.108 0 2.428-.208 4.011-.624a.632.632 0 0 1 .792.624v4.316a.64.64 0 0 1-.37.572c-1.794.728-4.064 1.092-6.597 1.092-3.062 0-5.278-.728-6.651-2.288-1.372-1.508-2.111-3.796-2.111-6.812V16.953h-3.008c-.37 0-.634-.26-.634-.624v-2.444c0-.052.053-.104.053-.156l4.17-2.444 2.058-5.408c.106-.26.317-.417.581-.417h3.8c.369 0 .633.26.633.625v5.252h7.548c.158 0 .317.156.317.312v4.68c0 .364-.264.624-.634.624h-7.178v13.21c0 1.04.317 1.872.897 2.34.528.572 1.373.832 2.323.832Zm35.521 5.564c-.739 0-1.319-.468-1.636-1.144l-5.595-16.797c-.369-1.196-.844-3.016-1.478-5.357h-.158l-.528 1.873-1.108 3.536-5.753 16.797c-.211.676-.845 1.092-1.584 1.092a1.628 1.628 0 0 1-1.583-1.196l-7.02-24.182c-.211-.728.369-1.508 1.214-1.508h.158c.528 0 1.003.364 1.161.884l4.117 14.717c1.003 3.849 1.689 6.657 2.006 8.53h.158c.95-3.85 1.689-6.397 2.164-7.698l5.331-15.393c.211-.624.792-1.04 1.531-1.04.686 0 1.267.416 1.478 1.04l4.961 15.29c1.214 3.9 1.953 6.396 2.217 7.696h.158c.159-1.04.792-3.952 2.006-8.633l3.958-14.509c.159-.52.634-.884 1.162-.884.791 0 1.372.728 1.161 1.508l-6.651 24.182c-.211.728-.844 1.196-1.636 1.196h-.211Zm31.352 0a.962.962 0 0 1-.95-.832l-.475-3.432h-.264c-1.372 1.716-2.745 2.964-4.223 3.692-1.425.728-3.166 1.04-5.119 1.04-2.692 0-4.751-.676-6.228-2.028-1.32-1.196-2.059-2.808-2.164-4.836-.212-2.704.95-5.305 3.166-6.813 2.27-1.456 5.437-2.34 9.712-2.34l5.173-.156v-1.768c0-2.6-.528-4.473-1.637-5.773-1.108-1.3-2.744-1.924-5.067-1.924-2.216 0-4.433.52-6.756 1.612-.58.26-1.266 0-1.53-.572s0-1.248.58-1.456c2.639-1.04 5.226-1.612 7.865-1.612 3.008 0 5.225.78 6.756 2.34 1.478 1.508 2.216 3.953 2.216 7.125v16.901c-.052.312-.527.832-1.055.832Zm-10.926-1.768c2.956 0 5.226-.832 6.862-2.444 1.689-1.612 2.533-3.952 2.533-6.813v-2.6l-4.75.208c-3.853.156-6.545.78-8.234 1.768-1.636.988-2.481 2.6-2.481 4.68 0 1.665.528 3.017 1.531 3.953 1.161.78 2.639 1.248 4.539 1.248Zm31.246-25.638c.792 0 1.584.052 2.481.156a1.176 1.176 0 0 1 1.003 1.352c-.106.624-.739.988-1.372.884-.792-.104-1.584-.208-2.375-.208-2.323 0-4.223.988-5.701 2.912-1.478 1.925-2.217 4.42-2.217 7.333v13.625c0 .676-.527 1.196-1.214 1.196-.686 0-1.213-.52-1.213-1.196V13.105c0-.572.475-1.04 1.055-1.04.581 0 1.056.416 1.056.988l.211 3.848h.158c1.109-1.976 2.323-3.38 3.589-4.16 1.214-.832 2.745-1.248 4.539-1.248Zm18.579 0c1.953 0 3.695.364 5.12 1.04 1.478.676 2.745 1.924 3.853 3.64h.158a122.343 122.343 0 0 1-.158-6.084V1.612c0-.676.528-1.196 1.214-1.196.686 0 1.214.52 1.214 1.196v36.351c0 .468-.37.832-.845.832a.852.852 0 0 1-.844-.78l-.528-3.38h-.211c-2.058 3.068-5.067 4.576-8.92 4.576-3.8 0-6.598-1.144-8.656-3.484-1.953-2.34-3.008-5.668-3.008-10.089 0-4.628.95-8.165 2.955-10.66 2.006-2.237 4.856-3.485 8.656-3.485Zm0 2.236c-3.008 0-5.225 1.04-6.756 3.12-1.478 2.029-2.216 4.993-2.216 8.945 0 7.593 3.008 11.39 9.025 11.39 3.114 0 5.331-.885 6.756-2.653 1.478-1.768 2.164-4.68 2.164-8.737v-.416c0-4.16-.686-7.124-2.164-8.893-1.372-1.872-3.642-2.756-6.809-2.756Zm31.616 25.638c-3.959 0-7.02-1.196-9.289-3.64-2.217-2.392-3.326-5.772-3.326-10.089 0-4.316 1.056-7.748 3.22-10.297 2.164-2.6 5.014-3.9 8.656-3.9 3.167 0 5.753 1.092 7.548 3.276 1.9 2.184 2.797 5.2 2.797 8.997v1.976h-19.634c.052 3.692.897 6.5 2.639 8.477 1.741 1.976 4.169 2.86 7.389 2.86 1.531 0 2.956-.104 4.117-.312.844-.156 1.847-.416 3.061-.832.686-.26 1.425.26 1.425.988 0 .416-.264.832-.686.988-1.267.52-2.481.832-3.589 1.04-1.32.364-2.745.468-4.328.468Zm-.739-25.69c-2.639 0-4.75.832-6.334 2.548-1.583 1.665-2.48 4.16-2.797 7.333h16.89c0-3.068-.686-5.564-2.059-7.28-1.372-1.717-3.272-2.6-5.7-2.6ZM288.733 38.9c-.686 0-1.214-.52-1.214-1.196V21.426c0-2.704-.58-4.68-1.689-5.877-1.214-1.196-2.955-1.872-5.383-1.872-3.273 0-5.648.78-7.126 2.444-1.478 1.613-2.322 4.265-2.322 7.853V37.6c0 .676-.528 1.196-1.214 1.196-.686 0-1.214-.52-1.214-1.196V13.105c0-.624.475-1.092 1.108-1.092.581 0 1.003.416 1.109.936l.316 2.704h.159c1.794-2.808 4.908-4.212 9.448-4.212 6.175 0 9.289 3.276 9.289 9.829V37.6c-.053.727-.633 1.3-1.267 1.3ZM90.225 0c-2.48 0-4.486 1.872-4.486 4.212v.416c0 2.289 2.058 4.213 4.486 4.213s4.486-1.924 4.486-4.213v-.364C94.711 1.872 92.653 0 90.225 0Z" />
|
||||||
<path class="tw-fill-primary-600" d="M32.041 24.546V5.95H18.848v33.035c2.336-1.22 4.427-2.547 6.272-3.98 4.614-3.565 6.921-7.051 6.921-10.46Zm5.654-22.314v22.314c0 1.665-.329 3.317-.986 4.953-.658 1.637-1.473 3.09-2.445 4.359-.971 1.268-2.13 2.503-3.475 3.704-1.345 1.2-2.586 2.199-3.725 2.993a46.963 46.963 0 0 1-3.563 2.251c-1.237.707-2.116 1.187-2.636 1.439-.52.251-.938.445-1.252.58-.235.117-.49.175-.765.175s-.53-.058-.766-.174c-.314-.136-.731-.33-1.252-.581-.52-.252-1.398-.732-2.635-1.439a47.003 47.003 0 0 1-3.564-2.251c-1.138-.794-2.38-1.792-3.725-2.993-1.345-1.2-2.503-2.436-3.475-3.704-.972-1.27-1.787-2.722-2.444-4.359C.329 27.863 0 26.211 0 24.546V2.232c0-.504.187-.94.56-1.308A1.823 1.823 0 0 1 1.885.372H35.81c.511 0 .953.184 1.326.552.373.368.56.804.56 1.308Z" />
|
<path d="M32.041 24.546V5.95H18.848v33.035c2.336-1.22 4.427-2.547 6.272-3.98 4.614-3.565 6.921-7.051 6.921-10.46Zm5.654-22.314v22.314c0 1.665-.329 3.317-.986 4.953-.658 1.637-1.473 3.09-2.445 4.359-.971 1.268-2.13 2.503-3.475 3.704-1.345 1.2-2.586 2.199-3.725 2.993a46.963 46.963 0 0 1-3.563 2.251c-1.237.707-2.116 1.187-2.636 1.439-.52.251-.938.445-1.252.58-.235.117-.49.175-.765.175s-.53-.058-.766-.174c-.314-.136-.731-.33-1.252-.581-.52-.252-1.398-.732-2.635-1.439a47.003 47.003 0 0 1-3.564-2.251c-1.138-.794-2.38-1.792-3.725-2.993-1.345-1.2-2.503-2.436-3.475-3.704-.972-1.27-1.787-2.722-2.444-4.359C.329 27.863 0 26.211 0 24.546V2.232c0-.504.187-.94.56-1.308A1.823 1.823 0 0 1 1.885.372H35.81c.511 0 .953.184 1.326.552.373.368.56.804.56 1.308Z" />
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const BitwardenLogoWhite = svgIcon`
|
||||||
|
<svg viewBox="0 0 290 45" fill="#FFF" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>Bitwarden</title>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M69.799 10.713c3.325 0 5.911 1.248 7.811 3.848 1.9 2.549 2.85 6.033 2.85 10.453 0 4.576-.95 8.113-2.902 10.61-1.953 2.547-4.592 3.743-7.918 3.743-3.325 0-5.858-1.144-7.758-3.536h-.528l-1.003 2.444a.976.976 0 0 1-.897.572H55.23a.94.94 0 0 1-.95-.936V1.352a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v8.009c0 1.144-.105 2.964-.316 5.46h.317c1.741-2.704 4.433-4.108 7.917-4.108Zm-2.428 6.084c-1.847 0-3.273.572-4.17 1.717-.844 1.144-1.32 3.068-1.32 5.668v.832c0 2.964.423 5.097 1.32 6.345.897 1.248 2.322 1.924 4.275 1.924 1.531 0 2.85-.728 3.748-2.184.897-1.404 1.372-3.537 1.372-6.189 0-2.704-.475-4.732-1.372-6.084-.95-1.352-2.27-2.029-3.853-2.029ZM93.022 38.9h-5.7a.94.94 0 0 1-.95-.936V12.221a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v25.69c.053.468-.422.988-.95.988Zm20.849-5.564c1.108 0 2.428-.208 4.011-.624a.632.632 0 0 1 .792.624v4.316a.64.64 0 0 1-.37.572c-1.794.728-4.064 1.092-6.597 1.092-3.062 0-5.278-.728-6.651-2.288-1.372-1.508-2.111-3.796-2.111-6.812V16.953h-3.008c-.37 0-.634-.26-.634-.624v-2.444c0-.052.053-.104.053-.156l4.17-2.444 2.058-5.408c.106-.26.317-.417.581-.417h3.8c.369 0 .633.26.633.625v5.252h7.548c.158 0 .317.156.317.312v4.68c0 .364-.264.624-.634.624h-7.178v13.21c0 1.04.317 1.872.897 2.34.528.572 1.373.832 2.323.832Zm35.521 5.564c-.739 0-1.319-.468-1.636-1.144l-5.595-16.797c-.369-1.196-.844-3.016-1.478-5.357h-.158l-.528 1.873-1.108 3.536-5.753 16.797c-.211.676-.845 1.092-1.584 1.092a1.628 1.628 0 0 1-1.583-1.196l-7.02-24.182c-.211-.728.369-1.508 1.214-1.508h.158c.528 0 1.003.364 1.161.884l4.117 14.717c1.003 3.849 1.689 6.657 2.006 8.53h.158c.95-3.85 1.689-6.397 2.164-7.698l5.331-15.393c.211-.624.792-1.04 1.531-1.04.686 0 1.267.416 1.478 1.04l4.961 15.29c1.214 3.9 1.953 6.396 2.217 7.696h.158c.159-1.04.792-3.952 2.006-8.633l3.958-14.509c.159-.52.634-.884 1.162-.884.791 0 1.372.728 1.161 1.508l-6.651 24.182c-.211.728-.844 1.196-1.636 1.196h-.211Zm31.352 0a.962.962 0 0 1-.95-.832l-.475-3.432h-.264c-1.372 1.716-2.745 2.964-4.223 3.692-1.425.728-3.166 1.04-5.119 1.04-2.692 0-4.751-.676-6.228-2.028-1.32-1.196-2.059-2.808-2.164-4.836-.212-2.704.95-5.305 3.166-6.813 2.27-1.456 5.437-2.34 9.712-2.34l5.173-.156v-1.768c0-2.6-.528-4.473-1.637-5.773-1.108-1.3-2.744-1.924-5.067-1.924-2.216 0-4.433.52-6.756 1.612-.58.26-1.266 0-1.53-.572s0-1.248.58-1.456c2.639-1.04 5.226-1.612 7.865-1.612 3.008 0 5.225.78 6.756 2.34 1.478 1.508 2.216 3.953 2.216 7.125v16.901c-.052.312-.527.832-1.055.832Zm-10.926-1.768c2.956 0 5.226-.832 6.862-2.444 1.689-1.612 2.533-3.952 2.533-6.813v-2.6l-4.75.208c-3.853.156-6.545.78-8.234 1.768-1.636.988-2.481 2.6-2.481 4.68 0 1.665.528 3.017 1.531 3.953 1.161.78 2.639 1.248 4.539 1.248Zm31.246-25.638c.792 0 1.584.052 2.481.156a1.176 1.176 0 0 1 1.003 1.352c-.106.624-.739.988-1.372.884-.792-.104-1.584-.208-2.375-.208-2.323 0-4.223.988-5.701 2.912-1.478 1.925-2.217 4.42-2.217 7.333v13.625c0 .676-.527 1.196-1.214 1.196-.686 0-1.213-.52-1.213-1.196V13.105c0-.572.475-1.04 1.055-1.04.581 0 1.056.416 1.056.988l.211 3.848h.158c1.109-1.976 2.323-3.38 3.589-4.16 1.214-.832 2.745-1.248 4.539-1.248Zm18.579 0c1.953 0 3.695.364 5.12 1.04 1.478.676 2.745 1.924 3.853 3.64h.158a122.343 122.343 0 0 1-.158-6.084V1.612c0-.676.528-1.196 1.214-1.196.686 0 1.214.52 1.214 1.196v36.351c0 .468-.37.832-.845.832a.852.852 0 0 1-.844-.78l-.528-3.38h-.211c-2.058 3.068-5.067 4.576-8.92 4.576-3.8 0-6.598-1.144-8.656-3.484-1.953-2.34-3.008-5.668-3.008-10.089 0-4.628.95-8.165 2.955-10.66 2.006-2.237 4.856-3.485 8.656-3.485Zm0 2.236c-3.008 0-5.225 1.04-6.756 3.12-1.478 2.029-2.216 4.993-2.216 8.945 0 7.593 3.008 11.39 9.025 11.39 3.114 0 5.331-.885 6.756-2.653 1.478-1.768 2.164-4.68 2.164-8.737v-.416c0-4.16-.686-7.124-2.164-8.893-1.372-1.872-3.642-2.756-6.809-2.756Zm31.616 25.638c-3.959 0-7.02-1.196-9.289-3.64-2.217-2.392-3.326-5.772-3.326-10.089 0-4.316 1.056-7.748 3.22-10.297 2.164-2.6 5.014-3.9 8.656-3.9 3.167 0 5.753 1.092 7.548 3.276 1.9 2.184 2.797 5.2 2.797 8.997v1.976h-19.634c.052 3.692.897 6.5 2.639 8.477 1.741 1.976 4.169 2.86 7.389 2.86 1.531 0 2.956-.104 4.117-.312.844-.156 1.847-.416 3.061-.832.686-.26 1.425.26 1.425.988 0 .416-.264.832-.686.988-1.267.52-2.481.832-3.589 1.04-1.32.364-2.745.468-4.328.468Zm-.739-25.69c-2.639 0-4.75.832-6.334 2.548-1.583 1.665-2.48 4.16-2.797 7.333h16.89c0-3.068-.686-5.564-2.059-7.28-1.372-1.717-3.272-2.6-5.7-2.6ZM288.733 38.9c-.686 0-1.214-.52-1.214-1.196V21.426c0-2.704-.58-4.68-1.689-5.877-1.214-1.196-2.955-1.872-5.383-1.872-3.273 0-5.648.78-7.126 2.444-1.478 1.613-2.322 4.265-2.322 7.853V37.6c0 .676-.528 1.196-1.214 1.196-.686 0-1.214-.52-1.214-1.196V13.105c0-.624.475-1.092 1.108-1.092.581 0 1.003.416 1.109.936l.316 2.704h.159c1.794-2.808 4.908-4.212 9.448-4.212 6.175 0 9.289 3.276 9.289 9.829V37.6c-.053.727-.633 1.3-1.267 1.3ZM90.225 0c-2.48 0-4.486 1.872-4.486 4.212v.416c0 2.289 2.058 4.213 4.486 4.213s4.486-1.924 4.486-4.213v-.364C94.711 1.872 92.653 0 90.225 0Z" />
|
||||||
|
<path d="M32.041 24.546V5.95H18.848v33.035c2.336-1.22 4.427-2.547 6.272-3.98 4.614-3.565 6.921-7.051 6.921-10.46Zm5.654-22.314v22.314c0 1.665-.329 3.317-.986 4.953-.658 1.637-1.473 3.09-2.445 4.359-.971 1.268-2.13 2.503-3.475 3.704-1.345 1.2-2.586 2.199-3.725 2.993a46.963 46.963 0 0 1-3.563 2.251c-1.237.707-2.116 1.187-2.636 1.439-.52.251-.938.445-1.252.58-.235.117-.49.175-.765.175s-.53-.058-.766-.174c-.314-.136-.731-.33-1.252-.581-.52-.252-1.398-.732-2.635-1.439a47.003 47.003 0 0 1-3.564-2.251c-1.138-.794-2.38-1.792-3.725-2.993-1.345-1.2-2.503-2.436-3.475-3.704-.972-1.27-1.787-2.722-2.444-4.359C.329 27.863 0 26.211 0 24.546V2.232c0-.504.187-.94.56-1.308A1.823 1.823 0 0 1 1.885.372H35.81c.511 0 .953.184 1.326.552.373.368.56.804.56 1.308Z" />
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export enum FeatureFlag {
|
|||||||
EmailVerification = "email-verification",
|
EmailVerification = "email-verification",
|
||||||
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
||||||
MemberAccessReport = "ac-2059-member-access-report",
|
MemberAccessReport = "ac-2059-member-access-report",
|
||||||
|
TwoFactorComponentRefactor = "two-factor-component-refactor",
|
||||||
EnableTimeThreshold = "PM-5864-dollar-threshold",
|
EnableTimeThreshold = "PM-5864-dollar-threshold",
|
||||||
GroupsComponentRefactor = "groups-component-refactor",
|
GroupsComponentRefactor = "groups-component-refactor",
|
||||||
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
|
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
|
||||||
@@ -53,6 +54,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.EmailVerification]: FALSE,
|
[FeatureFlag.EmailVerification]: FALSE,
|
||||||
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
||||||
[FeatureFlag.MemberAccessReport]: FALSE,
|
[FeatureFlag.MemberAccessReport]: FALSE,
|
||||||
|
[FeatureFlag.TwoFactorComponentRefactor]: FALSE,
|
||||||
[FeatureFlag.EnableTimeThreshold]: FALSE,
|
[FeatureFlag.EnableTimeThreshold]: FALSE,
|
||||||
[FeatureFlag.GroupsComponentRefactor]: FALSE,
|
[FeatureFlag.GroupsComponentRefactor]: FALSE,
|
||||||
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||||
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
|
||||||
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
|
||||||
import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
|
||||||
import { UserId } from "../../types/guid";
|
|
||||||
import { Account } from "../models/domain/account";
|
import { Account } from "../models/domain/account";
|
||||||
import { StorageOptions } from "../models/domain/storage-options";
|
import { StorageOptions } from "../models/domain/storage-options";
|
||||||
|
|
||||||
@@ -22,7 +18,6 @@ export type InitOptions = {
|
|||||||
|
|
||||||
export abstract class StateService<T extends Account = Account> {
|
export abstract class StateService<T extends Account = Account> {
|
||||||
addAccount: (account: T) => Promise<void>;
|
addAccount: (account: T) => Promise<void>;
|
||||||
clearDecryptedData: (userId: UserId) => Promise<void>;
|
|
||||||
clean: (options?: StorageOptions) => Promise<void>;
|
clean: (options?: StorageOptions) => Promise<void>;
|
||||||
init: (initOptions?: InitOptions) => Promise<void>;
|
init: (initOptions?: InitOptions) => Promise<void>;
|
||||||
|
|
||||||
@@ -73,36 +68,10 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
* @deprecated For migration purposes only, use setUserKeyBiometric instead
|
* @deprecated For migration purposes only, use setUserKeyBiometric instead
|
||||||
*/
|
*/
|
||||||
setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
|
setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
|
||||||
getDecryptedPasswordGenerationHistory: (
|
|
||||||
options?: StorageOptions,
|
|
||||||
) => Promise<GeneratedPasswordHistory[]>;
|
|
||||||
setDecryptedPasswordGenerationHistory: (
|
|
||||||
value: GeneratedPasswordHistory[],
|
|
||||||
options?: StorageOptions,
|
|
||||||
) => Promise<void>;
|
|
||||||
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
||||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getEncryptedPasswordGenerationHistory: (
|
|
||||||
options?: StorageOptions,
|
|
||||||
) => Promise<GeneratedPasswordHistory[]>;
|
|
||||||
setEncryptedPasswordGenerationHistory: (
|
|
||||||
value: GeneratedPasswordHistory[],
|
|
||||||
options?: StorageOptions,
|
|
||||||
) => Promise<void>;
|
|
||||||
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
|
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
|
||||||
getLastSync: (options?: StorageOptions) => Promise<string>;
|
getLastSync: (options?: StorageOptions) => Promise<string>;
|
||||||
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
|
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getPasswordGenerationOptions: (options?: StorageOptions) => Promise<PasswordGeneratorOptions>;
|
|
||||||
setPasswordGenerationOptions: (
|
|
||||||
value: PasswordGeneratorOptions,
|
|
||||||
options?: StorageOptions,
|
|
||||||
) => Promise<void>;
|
|
||||||
getUsernameGenerationOptions: (options?: StorageOptions) => Promise<UsernameGeneratorOptions>;
|
|
||||||
setUsernameGenerationOptions: (
|
|
||||||
value: UsernameGeneratorOptions,
|
|
||||||
options?: StorageOptions,
|
|
||||||
) => Promise<void>;
|
|
||||||
getGeneratorOptions: (options?: StorageOptions) => Promise<GeneratorOptions>;
|
|
||||||
setGeneratorOptions: (value: GeneratorOptions, options?: StorageOptions) => Promise<void>;
|
|
||||||
getUserId: (options?: StorageOptions) => Promise<string>;
|
getUserId: (options?: StorageOptions) => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { AccountSettings } from "./account";
|
|
||||||
|
|
||||||
describe("AccountSettings", () => {
|
|
||||||
describe("fromJSON", () => {
|
|
||||||
it("should deserialize to an instance of itself", () => {
|
|
||||||
expect(AccountSettings.fromJSON(JSON.parse("{}"))).toBeInstanceOf(AccountSettings);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Account, AccountKeys, AccountProfile, AccountSettings } from "./account";
|
import { Account, AccountKeys, AccountProfile } from "./account";
|
||||||
|
|
||||||
describe("Account", () => {
|
describe("Account", () => {
|
||||||
describe("fromJSON", () => {
|
describe("fromJSON", () => {
|
||||||
@@ -9,13 +9,11 @@ describe("Account", () => {
|
|||||||
it("should call all the sub-fromJSONs", () => {
|
it("should call all the sub-fromJSONs", () => {
|
||||||
const keysSpy = jest.spyOn(AccountKeys, "fromJSON");
|
const keysSpy = jest.spyOn(AccountKeys, "fromJSON");
|
||||||
const profileSpy = jest.spyOn(AccountProfile, "fromJSON");
|
const profileSpy = jest.spyOn(AccountProfile, "fromJSON");
|
||||||
const settingsSpy = jest.spyOn(AccountSettings, "fromJSON");
|
|
||||||
|
|
||||||
Account.fromJSON({});
|
Account.fromJSON({});
|
||||||
|
|
||||||
expect(keysSpy).toHaveBeenCalled();
|
expect(keysSpy).toHaveBeenCalled();
|
||||||
expect(profileSpy).toHaveBeenCalled();
|
expect(profileSpy).toHaveBeenCalled();
|
||||||
expect(settingsSpy).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
|
||||||
import { GeneratorOptions } from "../../../tools/generator/generator-options";
|
|
||||||
import {
|
|
||||||
GeneratedPasswordHistory,
|
|
||||||
PasswordGeneratorOptions,
|
|
||||||
} from "../../../tools/generator/password";
|
|
||||||
import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options";
|
|
||||||
import { DeepJsonify } from "../../../types/deep-jsonify";
|
import { DeepJsonify } from "../../../types/deep-jsonify";
|
||||||
import { KdfType } from "../../enums";
|
|
||||||
import { Utils } from "../../misc/utils";
|
import { Utils } from "../../misc/utils";
|
||||||
|
|
||||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||||
@@ -51,26 +43,6 @@ export class EncryptionPair<TEncrypted, TDecrypted> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataEncryptionPair<TEncrypted, TDecrypted> {
|
|
||||||
encrypted?: Record<string, TEncrypted>;
|
|
||||||
decrypted?: TDecrypted[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AccountData {
|
|
||||||
passwordGenerationHistory?: EncryptionPair<
|
|
||||||
GeneratedPasswordHistory[],
|
|
||||||
GeneratedPasswordHistory[]
|
|
||||||
> = new EncryptionPair<GeneratedPasswordHistory[], GeneratedPasswordHistory[]>();
|
|
||||||
|
|
||||||
static fromJSON(obj: DeepJsonify<AccountData>): AccountData {
|
|
||||||
if (obj == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.assign(new AccountData(), obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AccountKeys {
|
export class AccountKeys {
|
||||||
publicKey?: Uint8Array;
|
publicKey?: Uint8Array;
|
||||||
|
|
||||||
@@ -127,10 +99,6 @@ export class AccountProfile {
|
|||||||
emailVerified?: boolean;
|
emailVerified?: boolean;
|
||||||
lastSync?: string;
|
lastSync?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
kdfIterations?: number;
|
|
||||||
kdfMemory?: number;
|
|
||||||
kdfParallelism?: number;
|
|
||||||
kdfType?: KdfType;
|
|
||||||
|
|
||||||
static fromJSON(obj: Jsonify<AccountProfile>): AccountProfile {
|
static fromJSON(obj: Jsonify<AccountProfile>): AccountProfile {
|
||||||
if (obj == null) {
|
if (obj == null) {
|
||||||
@@ -141,33 +109,12 @@ export class AccountProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AccountSettings {
|
|
||||||
defaultUriMatch?: UriMatchStrategySetting;
|
|
||||||
passwordGenerationOptions?: PasswordGeneratorOptions;
|
|
||||||
usernameGenerationOptions?: UsernameGeneratorOptions;
|
|
||||||
generatorOptions?: GeneratorOptions;
|
|
||||||
|
|
||||||
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
|
||||||
if (obj == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.assign(new AccountSettings(), obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Account {
|
export class Account {
|
||||||
data?: AccountData = new AccountData();
|
|
||||||
keys?: AccountKeys = new AccountKeys();
|
keys?: AccountKeys = new AccountKeys();
|
||||||
profile?: AccountProfile = new AccountProfile();
|
profile?: AccountProfile = new AccountProfile();
|
||||||
settings?: AccountSettings = new AccountSettings();
|
|
||||||
|
|
||||||
constructor(init: Partial<Account>) {
|
constructor(init: Partial<Account>) {
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
data: {
|
|
||||||
...new AccountData(),
|
|
||||||
...init?.data,
|
|
||||||
},
|
|
||||||
keys: {
|
keys: {
|
||||||
...new AccountKeys(),
|
...new AccountKeys(),
|
||||||
...init?.keys,
|
...init?.keys,
|
||||||
@@ -176,10 +123,6 @@ export class Account {
|
|||||||
...new AccountProfile(),
|
...new AccountProfile(),
|
||||||
...init?.profile,
|
...init?.profile,
|
||||||
},
|
},
|
||||||
settings: {
|
|
||||||
...new AccountSettings(),
|
|
||||||
...init?.settings,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,9 +133,7 @@ export class Account {
|
|||||||
|
|
||||||
return Object.assign(new Account({}), json, {
|
return Object.assign(new Account({}), json, {
|
||||||
keys: AccountKeys.fromJSON(json?.keys),
|
keys: AccountKeys.fromJSON(json?.keys),
|
||||||
data: AccountData.fromJSON(json?.data),
|
|
||||||
profile: AccountProfile.fromJSON(json?.profile),
|
profile: AccountProfile.fromJSON(json?.profile),
|
||||||
settings: AccountSettings.fromJSON(json?.settings),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import { Jsonify, JsonValue } from "type-fest";
|
|||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
import { TokenService } from "../../auth/abstractions/token.service";
|
import { TokenService } from "../../auth/abstractions/token.service";
|
||||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||||
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
|
||||||
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
|
||||||
import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { EnvironmentService } from "../abstractions/environment.service";
|
import { EnvironmentService } from "../abstractions/environment.service";
|
||||||
import { LogService } from "../abstractions/log.service";
|
import { LogService } from "../abstractions/log.service";
|
||||||
@@ -17,8 +14,7 @@ import {
|
|||||||
import { AbstractStorageService } from "../abstractions/storage.service";
|
import { AbstractStorageService } from "../abstractions/storage.service";
|
||||||
import { HtmlStorageLocation, StorageLocation } from "../enums";
|
import { HtmlStorageLocation, StorageLocation } from "../enums";
|
||||||
import { StateFactory } from "../factories/state-factory";
|
import { StateFactory } from "../factories/state-factory";
|
||||||
import { Utils } from "../misc/utils";
|
import { Account } from "../models/domain/account";
|
||||||
import { Account, AccountData, AccountSettings } from "../models/domain/account";
|
|
||||||
import { GlobalState } from "../models/domain/global-state";
|
import { GlobalState } from "../models/domain/global-state";
|
||||||
import { State } from "../models/domain/state";
|
import { State } from "../models/domain/state";
|
||||||
import { StorageOptions } from "../models/domain/storage-options";
|
import { StorageOptions } from "../models/domain/storage-options";
|
||||||
@@ -306,29 +302,6 @@ export class StateService<
|
|||||||
await this.saveSecureStorageKey(partialKeys.biometricKey, value, options);
|
await this.saveSecureStorageKey(partialKeys.biometricKey, value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@withPrototypeForArrayMembers(GeneratedPasswordHistory)
|
|
||||||
async getDecryptedPasswordGenerationHistory(
|
|
||||||
options?: StorageOptions,
|
|
||||||
): Promise<GeneratedPasswordHistory[]> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
|
||||||
)?.data?.passwordGenerationHistory?.decrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setDecryptedPasswordGenerationHistory(
|
|
||||||
value: GeneratedPasswordHistory[],
|
|
||||||
options?: StorageOptions,
|
|
||||||
): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
account.data.passwordGenerationHistory.decrypted = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
|
async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
|
||||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
if (options?.userId == null) {
|
if (options?.userId == null) {
|
||||||
@@ -370,29 +343,6 @@ export class StateService<
|
|||||||
)?.keys.cryptoSymmetricKey.encrypted;
|
)?.keys.cryptoSymmetricKey.encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@withPrototypeForArrayMembers(GeneratedPasswordHistory)
|
|
||||||
async getEncryptedPasswordGenerationHistory(
|
|
||||||
options?: StorageOptions,
|
|
||||||
): Promise<GeneratedPasswordHistory[]> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
|
||||||
)?.data?.passwordGenerationHistory?.encrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setEncryptedPasswordGenerationHistory(
|
|
||||||
value: GeneratedPasswordHistory[],
|
|
||||||
options?: StorageOptions,
|
|
||||||
): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.data.passwordGenerationHistory.encrypted = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getIsAuthenticated(options?: StorageOptions): Promise<boolean> {
|
async getIsAuthenticated(options?: StorageOptions): Promise<boolean> {
|
||||||
return (
|
return (
|
||||||
(await this.tokenService.getAccessToken(options?.userId as UserId)) != null &&
|
(await this.tokenService.getAccessToken(options?.userId as UserId)) != null &&
|
||||||
@@ -417,63 +367,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPasswordGenerationOptions(options?: StorageOptions): Promise<PasswordGeneratorOptions> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
|
||||||
)?.settings?.passwordGenerationOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setPasswordGenerationOptions(
|
|
||||||
value: PasswordGeneratorOptions,
|
|
||||||
options?: StorageOptions,
|
|
||||||
): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
|
||||||
);
|
|
||||||
account.settings.passwordGenerationOptions = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUsernameGenerationOptions(options?: StorageOptions): Promise<UsernameGeneratorOptions> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
|
||||||
)?.settings?.usernameGenerationOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setUsernameGenerationOptions(
|
|
||||||
value: UsernameGeneratorOptions,
|
|
||||||
options?: StorageOptions,
|
|
||||||
): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
|
||||||
);
|
|
||||||
account.settings.usernameGenerationOptions = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getGeneratorOptions(options?: StorageOptions): Promise<GeneratorOptions> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
|
||||||
)?.settings?.generatorOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setGeneratorOptions(value: GeneratorOptions, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
|
||||||
);
|
|
||||||
account.settings.generatorOptions = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserId(options?: StorageOptions): Promise<string> {
|
async getUserId(options?: StorageOptions): Promise<string> {
|
||||||
return (
|
return (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
@@ -629,19 +522,6 @@ export class StateService<
|
|||||||
// TODO: There is a tech debt item for splitting up these methods - only Web uses multiple storage locations in its storageService.
|
// TODO: There is a tech debt item for splitting up these methods - only Web uses multiple storage locations in its storageService.
|
||||||
// For now these methods exist with some redundancy to facilitate this special web requirement.
|
// For now these methods exist with some redundancy to facilitate this special web requirement.
|
||||||
protected async scaffoldNewAccountLocalStorage(account: TAccount): Promise<void> {
|
protected async scaffoldNewAccountLocalStorage(account: TAccount): Promise<void> {
|
||||||
const storedAccount = await this.getAccount(
|
|
||||||
this.reconcileOptions(
|
|
||||||
{ userId: account.profile.userId },
|
|
||||||
await this.defaultOnDiskLocalOptions(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (storedAccount?.settings != null) {
|
|
||||||
account.settings = storedAccount.settings;
|
|
||||||
} else if (await this.storageService.has(keys.tempAccountSettings)) {
|
|
||||||
account.settings = await this.storageService.get<AccountSettings>(keys.tempAccountSettings);
|
|
||||||
await this.storageService.remove(keys.tempAccountSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.saveAccount(
|
await this.saveAccount(
|
||||||
account,
|
account,
|
||||||
this.reconcileOptions(
|
this.reconcileOptions(
|
||||||
@@ -652,15 +532,6 @@ export class StateService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async scaffoldNewAccountMemoryStorage(account: TAccount): Promise<void> {
|
protected async scaffoldNewAccountMemoryStorage(account: TAccount): Promise<void> {
|
||||||
const storedAccount = await this.getAccount(
|
|
||||||
this.reconcileOptions(
|
|
||||||
{ userId: account.profile.userId },
|
|
||||||
await this.defaultOnDiskMemoryOptions(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (storedAccount?.settings != null) {
|
|
||||||
account.settings = storedAccount.settings;
|
|
||||||
}
|
|
||||||
await this.storageService.save(
|
await this.storageService.save(
|
||||||
account.profile.userId,
|
account.profile.userId,
|
||||||
account,
|
account,
|
||||||
@@ -676,12 +547,6 @@ export class StateService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async scaffoldNewAccountSessionStorage(account: TAccount): Promise<void> {
|
protected async scaffoldNewAccountSessionStorage(account: TAccount): Promise<void> {
|
||||||
const storedAccount = await this.getAccount(
|
|
||||||
this.reconcileOptions({ userId: account.profile.userId }, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
if (storedAccount?.settings != null) {
|
|
||||||
account.settings = storedAccount.settings;
|
|
||||||
}
|
|
||||||
await this.storageService.save(
|
await this.storageService.save(
|
||||||
account.profile.userId,
|
account.profile.userId,
|
||||||
account,
|
account,
|
||||||
@@ -830,20 +695,8 @@ export class StateService<
|
|||||||
|
|
||||||
// settings persist even on reset, and are not affected by this method
|
// settings persist even on reset, and are not affected by this method
|
||||||
protected resetAccount(account: TAccount) {
|
protected resetAccount(account: TAccount) {
|
||||||
const persistentAccountInformation = {
|
// All settings have been moved to StateProviders
|
||||||
settings: account.settings,
|
return this.createAccount();
|
||||||
};
|
|
||||||
return Object.assign(this.createAccount(), persistentAccountInformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearDecryptedData(userId: UserId): Promise<void> {
|
|
||||||
await this.updateState(async (state) => {
|
|
||||||
if (userId != null && state?.accounts[userId]?.data != null) {
|
|
||||||
state.accounts[userId].data = new AccountData();
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createAccount(init: Partial<TAccount> = null): TAccount {
|
protected createAccount(init: Partial<TAccount> = null): TAccount {
|
||||||
@@ -904,51 +757,3 @@ export class StateService<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function withPrototypeForArrayMembers<T>(
|
|
||||||
memberConstructor: new (...args: any[]) => T,
|
|
||||||
memberConverter: (input: any) => T = (i) => i,
|
|
||||||
): (
|
|
||||||
target: any,
|
|
||||||
propertyKey: string | symbol,
|
|
||||||
descriptor: PropertyDescriptor,
|
|
||||||
) => { value: (...args: any[]) => Promise<T[]> } {
|
|
||||||
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
|
|
||||||
const originalMethod = descriptor.value;
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: function (...args: any[]) {
|
|
||||||
const originalResult: Promise<any[]> = originalMethod.apply(this, args);
|
|
||||||
|
|
||||||
if (!Utils.isPromise(originalResult)) {
|
|
||||||
throw new Error(
|
|
||||||
`Error applying prototype to stored value -- result is not a promise for method ${String(
|
|
||||||
propertyKey,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalResult.then((result) => {
|
|
||||||
if (result == null) {
|
|
||||||
return null;
|
|
||||||
} else if (!(result instanceof Array)) {
|
|
||||||
throw new Error(
|
|
||||||
`Attempted to retrieve non array type from state as an array for method ${String(
|
|
||||||
propertyKey,
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return result.map((r) => {
|
|
||||||
return r == null ||
|
|
||||||
r.constructor.name === memberConstructor.prototype.constructor.name
|
|
||||||
? r
|
|
||||||
: memberConverter(
|
|
||||||
Object.create(memberConstructor.prototype, Object.getOwnPropertyDescriptors(r)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
|||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { MessagingService } from "../abstractions/messaging.service";
|
import { MessagingService } from "../abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
|
||||||
import { StateService } from "../abstractions/state.service";
|
|
||||||
import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service";
|
import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service";
|
||||||
import { BiometricStateService } from "../biometrics/biometric-state.service";
|
import { BiometricStateService } from "../biometrics/biometric-state.service";
|
||||||
import { Utils } from "../misc/utils";
|
import { Utils } from "../misc/utils";
|
||||||
@@ -25,7 +24,6 @@ export class SystemService implements SystemServiceAbstraction {
|
|||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private reloadCallback: () => Promise<void> = null,
|
private reloadCallback: () => Promise<void> = null,
|
||||||
private stateService: StateService,
|
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
@@ -90,8 +88,6 @@ export class SystemService implements SystemServiceAbstraction {
|
|||||||
const nextUser = await firstValueFrom(
|
const nextUser = await firstValueFrom(
|
||||||
this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)),
|
this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)),
|
||||||
);
|
);
|
||||||
// Can be removed once we migrate password generation history to state providers
|
|
||||||
await this.stateService.clearDecryptedData(activeUserId);
|
|
||||||
await this.accountService.switchAccount(nextUser);
|
await this.accountService.switchAccount(nextUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
86
libs/common/src/tools/integration/README.md
Normal file
86
libs/common/src/tools/integration/README.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
This module defines interfaces and helpers for creating vendor integration sites.
|
||||||
|
|
||||||
|
## RPC
|
||||||
|
|
||||||
|
> ⚠️ **Only use for extension points!**
|
||||||
|
> This logic is not suitable for general use. Making calls to the Bitwarden server api
|
||||||
|
> using `@bitwarden/common/tools/integration/rpc` is prohibited.
|
||||||
|
|
||||||
|
Interfaces and helpers defining a remote procedure call to a vendor's service. These
|
||||||
|
types provide extension points to produce and process the call without exposing a
|
||||||
|
generalized fetch API.
|
||||||
|
|
||||||
|
## Sample usage
|
||||||
|
|
||||||
|
An email forwarder configuration:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// define RPC shapes;
|
||||||
|
// * the request format, `RequestOptions` is common to all calls
|
||||||
|
// * the context operates on forwarder-specific settings provided by `state`.
|
||||||
|
type CreateForwardingEmailConfig<Settings> = RpcConfiguration<
|
||||||
|
RequestOptions,
|
||||||
|
ForwarderContext<Settings>
|
||||||
|
>;
|
||||||
|
|
||||||
|
// how a forwarder integration point might represent its configuration
|
||||||
|
type ForwarderConfiguration<Settings> = IntegrationConfiguration & {
|
||||||
|
forwarder: {
|
||||||
|
defaultState: Settings;
|
||||||
|
createForwardingEmail: CreateForwardingEmailConfig<Settings>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// how an importer integration point might represent its configuration
|
||||||
|
type ImporterConfiguration = IntegrationConfiguration & {
|
||||||
|
importer: {
|
||||||
|
fileless: false | { selector: string };
|
||||||
|
formats: ContentType[];
|
||||||
|
crep:
|
||||||
|
| false
|
||||||
|
| {
|
||||||
|
/* credential exchange protocol configuration */
|
||||||
|
};
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// how a plugin might be structured
|
||||||
|
export type JustTrustUsSettings = ApiSettings & EmailDomainSettings;
|
||||||
|
export type JustTrustUsConfiguration = ForwarderConfiguration<JustTrustUsSettings> &
|
||||||
|
ImporterConfiguration;
|
||||||
|
|
||||||
|
export const JustTrustUs = {
|
||||||
|
// common metadata
|
||||||
|
id: "justrustus",
|
||||||
|
name: "Just Trust Us, LLC",
|
||||||
|
extends: ["forwarder"],
|
||||||
|
|
||||||
|
// API conventions
|
||||||
|
selfHost: "never",
|
||||||
|
baseUrl: "https://api.just-trust.us/v1",
|
||||||
|
authenticate(settings: ApiSettings, context: IntegrationContext) {
|
||||||
|
return { Authorization: "Bearer " + context.authenticationToken(settings) };
|
||||||
|
},
|
||||||
|
|
||||||
|
// forwarder specific config
|
||||||
|
forwarder: {
|
||||||
|
defaultState: { domain: "just-trust.us" },
|
||||||
|
|
||||||
|
// specific RPC call
|
||||||
|
createForwardingEmail: {
|
||||||
|
url: () => context.baseUrl() + "/fowarder",
|
||||||
|
body: (request: RequestOptions) => ({ description: context.generatedBy(request) }),
|
||||||
|
hasJsonPayload: (response) => response.status === 200,
|
||||||
|
processJson: (json) => json.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// importer specific config
|
||||||
|
importer: {
|
||||||
|
fileless: false,
|
||||||
|
crep: false,
|
||||||
|
formats: ["text/csv", "application/json"],
|
||||||
|
},
|
||||||
|
} as JustTrustUsConfiguration;
|
||||||
|
```
|
||||||
4
libs/common/src/tools/integration/extension-point-id.ts
Normal file
4
libs/common/src/tools/integration/extension-point-id.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/** well-known name for a feature extensible through an integration. */
|
||||||
|
// The forwarder extension point is presently hard-coded in `@bitwarden/generator-legacy/`.
|
||||||
|
// v2 will load forwarders using an extension provider.
|
||||||
|
export type ExtensionPointId = "forwarder";
|
||||||
5
libs/common/src/tools/integration/index.ts
Normal file
5
libs/common/src/tools/integration/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "./extension-point-id";
|
||||||
|
export * from "./integration-configuration";
|
||||||
|
export * from "./integration-context";
|
||||||
|
export * from "./integration-id";
|
||||||
|
export * from "./integration-metadata";
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { IntegrationContext } from "./integration-context";
|
||||||
|
import { IntegrationMetadata } from "./integration-metadata";
|
||||||
|
import { ApiSettings, TokenHeader } from "./rpc";
|
||||||
|
|
||||||
|
/** Configures integration-wide settings */
|
||||||
|
export type IntegrationConfiguration = IntegrationMetadata & {
|
||||||
|
/** Creates the authentication header for all integration remote procedure calls */
|
||||||
|
authenticate: (settings: ApiSettings, context: IntegrationContext) => TokenHeader;
|
||||||
|
};
|
||||||
195
libs/common/src/tools/integration/integration-context.spec.ts
Normal file
195
libs/common/src/tools/integration/integration-context.spec.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { IntegrationContext } from "./integration-context";
|
||||||
|
import { IntegrationId } from "./integration-id";
|
||||||
|
import { IntegrationMetadata } from "./integration-metadata";
|
||||||
|
|
||||||
|
const EXAMPLE_META = Object.freeze({
|
||||||
|
// arbitrary
|
||||||
|
id: "simplelogin" as IntegrationId,
|
||||||
|
name: "Example",
|
||||||
|
// arbitrary
|
||||||
|
extends: ["forwarder"],
|
||||||
|
baseUrl: "https://api.example.com",
|
||||||
|
selfHost: "maybe",
|
||||||
|
} as IntegrationMetadata);
|
||||||
|
|
||||||
|
describe("IntegrationContext", () => {
|
||||||
|
const i18n = mock<I18nService>();
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("baseUrl", () => {
|
||||||
|
it("outputs the base url from metadata", () => {
|
||||||
|
const context = new IntegrationContext(EXAMPLE_META, i18n);
|
||||||
|
|
||||||
|
const result = context.baseUrl();
|
||||||
|
|
||||||
|
expect(result).toBe("https://api.example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when the baseurl isn't defined in metadata", () => {
|
||||||
|
const noBaseUrl: IntegrationMetadata = {
|
||||||
|
id: "simplelogin" as IntegrationId, // arbitrary
|
||||||
|
name: "Example",
|
||||||
|
extends: ["forwarder"], // arbitrary
|
||||||
|
selfHost: "maybe",
|
||||||
|
};
|
||||||
|
i18n.t.mockReturnValue("error");
|
||||||
|
|
||||||
|
const context = new IntegrationContext(noBaseUrl, i18n);
|
||||||
|
|
||||||
|
expect(() => context.baseUrl()).toThrow("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads from the settings", () => {
|
||||||
|
const context = new IntegrationContext(EXAMPLE_META, i18n);
|
||||||
|
|
||||||
|
const result = context.baseUrl({ baseUrl: "httpbin.org" });
|
||||||
|
|
||||||
|
expect(result).toBe("httpbin.org");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores settings when selfhost is 'never'", () => {
|
||||||
|
const selfHostNever: IntegrationMetadata = {
|
||||||
|
id: "simplelogin" as IntegrationId, // arbitrary
|
||||||
|
name: "Example",
|
||||||
|
extends: ["forwarder"], // arbitrary
|
||||||
|
baseUrl: "example.com",
|
||||||
|
selfHost: "never",
|
||||||
|
};
|
||||||
|
const context = new IntegrationContext(selfHostNever, i18n);
|
||||||
|
|
||||||
|
const result = context.baseUrl({ baseUrl: "httpbin.org" });
|
||||||
|
|
||||||
|
expect(result).toBe("example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("always reads the settings when selfhost is 'always'", () => {
|
||||||
|
const selfHostAlways: IntegrationMetadata = {
|
||||||
|
id: "simplelogin" as IntegrationId, // arbitrary
|
||||||
|
name: "Example",
|
||||||
|
extends: ["forwarder"], // arbitrary
|
||||||
|
baseUrl: "example.com",
|
||||||
|
selfHost: "always",
|
||||||
|
};
|
||||||
|
const context = new IntegrationContext(selfHostAlways, i18n);
|
||||||
|
|
||||||
|
// expect success
|
||||||
|
const result = context.baseUrl({ baseUrl: "http.bin" });
|
||||||
|
expect(result).toBe("http.bin");
|
||||||
|
|
||||||
|
// expect error
|
||||||
|
i18n.t.mockReturnValue("error");
|
||||||
|
expect(() => context.baseUrl()).toThrow("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads from the metadata by default when selfhost is 'maybe'", () => {
|
||||||
|
const selfHostMaybe: IntegrationMetadata = {
|
||||||
|
id: "simplelogin" as IntegrationId, // arbitrary
|
||||||
|
name: "Example",
|
||||||
|
extends: ["forwarder"], // arbitrary
|
||||||
|
baseUrl: "example.com",
|
||||||
|
selfHost: "maybe",
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = new IntegrationContext(selfHostMaybe, i18n);
|
||||||
|
|
||||||
|
const result = context.baseUrl();
|
||||||
|
|
||||||
|
expect(result).toBe("example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overrides the metadata when selfhost is 'maybe'", () => {
|
||||||
|
const selfHostMaybe: IntegrationMetadata = {
|
||||||
|
id: "simplelogin" as IntegrationId, // arbitrary
|
||||||
|
name: "Example",
|
||||||
|
extends: ["forwarder"], // arbitrary
|
||||||
|
baseUrl: "example.com",
|
||||||
|
selfHost: "maybe",
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = new IntegrationContext(selfHostMaybe, i18n);
|
||||||
|
|
||||||
|
const result = context.baseUrl({ baseUrl: "httpbin.org" });
|
||||||
|
|
||||||
|
expect(result).toBe("httpbin.org");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("authenticationToken", () => {
|
||||||
|
it("reads from the settings", () => {
|
||||||
|
const context = new IntegrationContext(EXAMPLE_META, i18n);
|
||||||
|
|
||||||
|
const result = context.authenticationToken({ token: "example" });
|
||||||
|
|
||||||
|
expect(result).toBe("example");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("base64 encodes the read value", () => {
|
||||||
|
const context = new IntegrationContext(EXAMPLE_META, i18n);
|
||||||
|
|
||||||
|
const result = context.authenticationToken({ token: "example" }, { base64: true });
|
||||||
|
|
||||||
|
expect(result).toBe("ZXhhbXBsZQ==");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error when the value is missing", () => {
|
||||||
|
const context = new IntegrationContext(EXAMPLE_META, i18n);
|
||||||
|
i18n.t.mockReturnValue("error");
|
||||||
|
|
||||||
|
expect(() => context.authenticationToken({})).toThrow("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error when the value is empty", () => {
|
||||||
|
const context = new IntegrationContext(EXAMPLE_META, i18n);
|
||||||
|
i18n.t.mockReturnValue("error");
|
||||||
|
|
||||||
|
expect(() => context.authenticationToken({ token: "" })).toThrow("error");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("website", () => {
|
||||||
|
it("returns the website", () => {
|
||||||
|
const context = new IntegrationContext(EXAMPLE_META, i18n);
|
||||||
|
|
||||||
|
const result = context.website({ website: "www.example.com" });
|
||||||
|
|
||||||
|
expect(result).toBe("www.example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty string when the website is not specified", () => {
|
||||||
|
const context = new IntegrationContext(EXAMPLE_META, i18n);
|
||||||
|
|
||||||
|
const result = context.website({ website: undefined });
|
||||||
|
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generatedBy", () => {
|
||||||
|
it("creates generated by text", () => {
|
||||||
|
const context = new IntegrationContext(EXAMPLE_META, i18n);
|
||||||
|
i18n.t.mockReturnValue("result");
|
||||||
|
|
||||||
|
const result = context.generatedBy({ website: null });
|
||||||
|
|
||||||
|
expect(result).toBe("result");
|
||||||
|
expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedBy", "");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates generated by text including the website", () => {
|
||||||
|
const context = new IntegrationContext(EXAMPLE_META, i18n);
|
||||||
|
i18n.t.mockReturnValue("result");
|
||||||
|
|
||||||
|
const result = context.generatedBy({ website: "www.example.com" });
|
||||||
|
|
||||||
|
expect(result).toBe("result");
|
||||||
|
expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedByWithWebsite", "www.example.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
91
libs/common/src/tools/integration/integration-context.ts
Normal file
91
libs/common/src/tools/integration/integration-context.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
|
import { IntegrationMetadata } from "./integration-metadata";
|
||||||
|
import { ApiSettings, SelfHostedApiSettings, IntegrationRequest } from "./rpc";
|
||||||
|
|
||||||
|
/** Utilities for processing integration settings */
|
||||||
|
export class IntegrationContext {
|
||||||
|
/** Instantiates an integration context
|
||||||
|
* @param metadata - defines integration capabilities
|
||||||
|
* @param i18n - localizes error messages
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly metadata: IntegrationMetadata,
|
||||||
|
protected i18n: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Lookup the integration's baseUrl
|
||||||
|
* @param settings settings that override the baseUrl.
|
||||||
|
* @returns the baseUrl for the API's integration point.
|
||||||
|
* - By default this is defined by the metadata
|
||||||
|
* - When a service allows self-hosting, this can be supplied by `settings`.
|
||||||
|
* @throws a localized error message when a base URL is neither defined by the metadata or
|
||||||
|
* supplied by an argument.
|
||||||
|
*/
|
||||||
|
baseUrl(settings?: SelfHostedApiSettings) {
|
||||||
|
// normalize baseUrl
|
||||||
|
const setting = settings && "baseUrl" in settings ? settings.baseUrl : "";
|
||||||
|
let result = "";
|
||||||
|
|
||||||
|
// look up definition
|
||||||
|
if (this.metadata.selfHost === "always") {
|
||||||
|
result = setting;
|
||||||
|
} else if (this.metadata.selfHost === "never" || setting.length <= 0) {
|
||||||
|
result = this.metadata.baseUrl ?? "";
|
||||||
|
} else {
|
||||||
|
result = setting;
|
||||||
|
}
|
||||||
|
|
||||||
|
// postconditions
|
||||||
|
if (result === "") {
|
||||||
|
const error = this.i18n.t("forwarderNoUrl", this.metadata.name);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** look up a service API's authentication token
|
||||||
|
* @param settings store the API token
|
||||||
|
* @param options.base64 when `true`, base64 encodes the result. Defaults to `false`.
|
||||||
|
* @returns the user's authentication token
|
||||||
|
* @throws a localized error message when the token is invalid.
|
||||||
|
*/
|
||||||
|
authenticationToken(settings: ApiSettings, options: { base64?: boolean } = null) {
|
||||||
|
if (!settings.token || settings.token === "") {
|
||||||
|
const error = this.i18n.t("forwaderInvalidToken", this.metadata.name);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = settings.token;
|
||||||
|
if (options?.base64) {
|
||||||
|
token = Utils.fromUtf8ToB64(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** look up the website the integration is working with.
|
||||||
|
* @param request supplies information about the state of the extension site
|
||||||
|
* @returns The website or an empty string if a website isn't available
|
||||||
|
* @remarks `website` is usually supplied when generating a credential from the vault
|
||||||
|
*/
|
||||||
|
website(request: IntegrationRequest) {
|
||||||
|
return request.website ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** look up localized text indicating Bitwarden requested the forwarding address.
|
||||||
|
* @param request supplies information about the state of the extension site
|
||||||
|
* @returns localized text describing a generated forwarding address
|
||||||
|
*/
|
||||||
|
generatedBy(request: IntegrationRequest) {
|
||||||
|
const website = this.website(request);
|
||||||
|
|
||||||
|
const descriptionId =
|
||||||
|
website === "" ? "forwarderGeneratedBy" : "forwarderGeneratedByWithWebsite";
|
||||||
|
const description = this.i18n.t(descriptionId, website);
|
||||||
|
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
libs/common/src/tools/integration/integration-id.ts
Normal file
7
libs/common/src/tools/integration/integration-id.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Opaque } from "type-fest";
|
||||||
|
|
||||||
|
/** Identifies a vendor integrated into bitwarden */
|
||||||
|
export type IntegrationId = Opaque<
|
||||||
|
"anonaddy" | "duckduckgo" | "fastmail" | "firefoxrelay" | "forwardemail" | "simplelogin",
|
||||||
|
"IntegrationId"
|
||||||
|
>;
|
||||||
23
libs/common/src/tools/integration/integration-metadata.ts
Normal file
23
libs/common/src/tools/integration/integration-metadata.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ExtensionPointId } from "./extension-point-id";
|
||||||
|
import { IntegrationId } from "./integration-id";
|
||||||
|
|
||||||
|
/** The capabilities and descriptive content for an integration */
|
||||||
|
export type IntegrationMetadata = {
|
||||||
|
/** Uniquely identifies the integrator. */
|
||||||
|
id: IntegrationId;
|
||||||
|
|
||||||
|
/** Brand name of the integrator. */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Features extended by the integration. */
|
||||||
|
extends: Array<ExtensionPointId>;
|
||||||
|
|
||||||
|
/** Common URL for the service; this should only be undefined when selfHost is "always" */
|
||||||
|
baseUrl?: string;
|
||||||
|
|
||||||
|
/** Determines whether the integration supports self-hosting;
|
||||||
|
* "maybe" allows a service's base URLs to vary from the metadata URL
|
||||||
|
* "never" always sets a service's baseURL from the metadata URL
|
||||||
|
*/
|
||||||
|
selfHost: "always" | "maybe" | "never";
|
||||||
|
};
|
||||||
15
libs/common/src/tools/integration/rpc/api-settings.ts
Normal file
15
libs/common/src/tools/integration/rpc/api-settings.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/** Options common to all forwarder APIs */
|
||||||
|
export type ApiSettings = {
|
||||||
|
/** bearer token that authenticates bitwarden to the forwarder.
|
||||||
|
* This is required to issue an API request.
|
||||||
|
*/
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Api configuration for forwarders that support self-hosted installations. */
|
||||||
|
export type SelfHostedApiSettings = ApiSettings & {
|
||||||
|
/** The base URL of the forwarder's API.
|
||||||
|
* When this is empty, the forwarder's default production API is used.
|
||||||
|
*/
|
||||||
|
baseUrl: string;
|
||||||
|
};
|
||||||
6
libs/common/src/tools/integration/rpc/index.ts
Normal file
6
libs/common/src/tools/integration/rpc/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from "./api-settings";
|
||||||
|
export * from "./integration-request";
|
||||||
|
export * from "./rest-client";
|
||||||
|
export * from "./rpc-definition";
|
||||||
|
export * from "./rpc";
|
||||||
|
export * from "./token-header";
|
||||||
11
libs/common/src/tools/integration/rpc/integration-request.ts
Normal file
11
libs/common/src/tools/integration/rpc/integration-request.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/** Options that provide contextual information about the application state
|
||||||
|
* when an integration is invoked.
|
||||||
|
*/
|
||||||
|
export type IntegrationRequest = {
|
||||||
|
/** @param website The domain of the website the requested integration is used
|
||||||
|
* within. This should be set to `null` when the request is not specific
|
||||||
|
* to any website.
|
||||||
|
* @remarks this field contains sensitive data
|
||||||
|
*/
|
||||||
|
website: string | null;
|
||||||
|
};
|
||||||
164
libs/common/src/tools/integration/rpc/rest-client.spec.ts
Normal file
164
libs/common/src/tools/integration/rpc/rest-client.spec.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { IntegrationRequest } from "./integration-request";
|
||||||
|
import { RestClient } from "./rest-client";
|
||||||
|
import { JsonRpc } from "./rpc";
|
||||||
|
|
||||||
|
describe("RestClient", () => {
|
||||||
|
const expectedRpc = {
|
||||||
|
fetchRequest: {} as any,
|
||||||
|
json: {},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const i18n = mock<I18nService>();
|
||||||
|
const nativeFetchResponse = mock<Response>({ status: 200 });
|
||||||
|
const api = mock<ApiService>();
|
||||||
|
const rpc = mock<JsonRpc<IntegrationRequest, object>>({ requestor: { name: "mock" } });
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
i18n.t.mockImplementation((a) => a);
|
||||||
|
|
||||||
|
api.nativeFetch.mockResolvedValue(nativeFetchResponse);
|
||||||
|
|
||||||
|
rpc.toRequest.mockReturnValue(expectedRpc.fetchRequest);
|
||||||
|
rpc.hasJsonPayload.mockReturnValue(true);
|
||||||
|
rpc.processJson.mockImplementation((json: any) => [expectedRpc.json]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchJson", () => {
|
||||||
|
it("issues a request", async () => {
|
||||||
|
const client = new RestClient(api, i18n);
|
||||||
|
const request: IntegrationRequest = { website: null };
|
||||||
|
|
||||||
|
const result = await client.fetchJson(rpc, request);
|
||||||
|
|
||||||
|
expect(result).toBe(expectedRpc.json);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invokes the constructed request", async () => {
|
||||||
|
const client = new RestClient(api, i18n);
|
||||||
|
const request: IntegrationRequest = { website: null };
|
||||||
|
|
||||||
|
await client.fetchJson(rpc, request);
|
||||||
|
|
||||||
|
expect(api.nativeFetch).toHaveBeenCalledWith(expectedRpc.fetchRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([[401], [403]])(
|
||||||
|
"throws an invalid token error when HTTP status is %i",
|
||||||
|
async (status) => {
|
||||||
|
const client = new RestClient(api, i18n);
|
||||||
|
const request: IntegrationRequest = { website: null };
|
||||||
|
const response = mock<Response>({ status });
|
||||||
|
api.nativeFetch.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = client.fetchJson(rpc, request);
|
||||||
|
|
||||||
|
await expect(result).rejects.toEqual("forwarderInvalidToken");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[401, "message"],
|
||||||
|
[403, "message"],
|
||||||
|
[401, "error"],
|
||||||
|
[403, "error"],
|
||||||
|
])(
|
||||||
|
"throws an invalid token detailed error when HTTP status is %i and the payload has a %s",
|
||||||
|
async (status, property) => {
|
||||||
|
const client = new RestClient(api, i18n);
|
||||||
|
const request: IntegrationRequest = { website: null };
|
||||||
|
const response = mock<Response>({
|
||||||
|
status,
|
||||||
|
text: () => Promise.resolve(`{ "${property}": "expected message" }`),
|
||||||
|
});
|
||||||
|
api.nativeFetch.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = client.fetchJson(rpc, request);
|
||||||
|
|
||||||
|
await expect(result).rejects.toEqual("forwarderInvalidTokenWithMessage");
|
||||||
|
expect(i18n.t).toHaveBeenCalledWith(
|
||||||
|
"forwarderInvalidTokenWithMessage",
|
||||||
|
"mock",
|
||||||
|
"expected message",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([[500], [501]])(
|
||||||
|
"throws a forwarder error with the status text when HTTP status is %i",
|
||||||
|
async (status) => {
|
||||||
|
const client = new RestClient(api, i18n);
|
||||||
|
const request: IntegrationRequest = { website: null };
|
||||||
|
const response = mock<Response>({ status, statusText: "expectedResult" });
|
||||||
|
api.nativeFetch.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = client.fetchJson(rpc, request);
|
||||||
|
|
||||||
|
await expect(result).rejects.toEqual("forwarderError");
|
||||||
|
expect(i18n.t).toHaveBeenCalledWith("forwarderError", "mock", "expectedResult");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[500, "message"],
|
||||||
|
[500, "message"],
|
||||||
|
[501, "error"],
|
||||||
|
[501, "error"],
|
||||||
|
])(
|
||||||
|
"throws a detailed forwarder error when HTTP status is %i and the payload has a %s",
|
||||||
|
async (status, property) => {
|
||||||
|
const client = new RestClient(api, i18n);
|
||||||
|
const request: IntegrationRequest = { website: null };
|
||||||
|
const response = mock<Response>({
|
||||||
|
status,
|
||||||
|
text: () => Promise.resolve(`{ "${property}": "expected message" }`),
|
||||||
|
});
|
||||||
|
api.nativeFetch.mockResolvedValue(response);
|
||||||
|
|
||||||
|
const result = client.fetchJson(rpc, request);
|
||||||
|
|
||||||
|
await expect(result).rejects.toEqual("forwarderError");
|
||||||
|
expect(i18n.t).toHaveBeenCalledWith("forwarderError", "mock", "expected message");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("outputs an error if there's no json payload", async () => {
|
||||||
|
const client = new RestClient(api, i18n);
|
||||||
|
rpc.hasJsonPayload.mockReturnValue(false);
|
||||||
|
const request: IntegrationRequest = { website: null };
|
||||||
|
|
||||||
|
const result = client.fetchJson(rpc, request);
|
||||||
|
|
||||||
|
await expect(result).rejects.toEqual("forwarderUnknownError");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("processes an ok JSON payload", async () => {
|
||||||
|
const client = new RestClient(api, i18n);
|
||||||
|
rpc.processJson.mockReturnValue([{ foo: true }]);
|
||||||
|
const request: IntegrationRequest = { website: null };
|
||||||
|
|
||||||
|
const result = client.fetchJson(rpc, request);
|
||||||
|
|
||||||
|
await expect(result).resolves.toEqual({ foo: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("processes an erroneous JSON payload", async () => {
|
||||||
|
const client = new RestClient(api, i18n);
|
||||||
|
rpc.processJson.mockReturnValue([undefined, "expected message"]);
|
||||||
|
const request: IntegrationRequest = { website: null };
|
||||||
|
|
||||||
|
const result = client.fetchJson(rpc, request);
|
||||||
|
|
||||||
|
await expect(result).rejects.toEqual("forwarderError");
|
||||||
|
expect(i18n.t).toHaveBeenCalledWith("forwarderError", "mock", "expected message");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
68
libs/common/src/tools/integration/rpc/rest-client.ts
Normal file
68
libs/common/src/tools/integration/rpc/rest-client.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { IntegrationRequest } from "./integration-request";
|
||||||
|
import { JsonRpc } from "./rpc";
|
||||||
|
|
||||||
|
/** Makes remote procedure calls using a RESTful interface. */
|
||||||
|
export class RestClient {
|
||||||
|
constructor(
|
||||||
|
private api: ApiService,
|
||||||
|
private i18n: I18nService,
|
||||||
|
) {}
|
||||||
|
/** uses the fetch API to request a JSON payload. */
|
||||||
|
async fetchJson<Parameters extends IntegrationRequest, Response>(
|
||||||
|
rpc: JsonRpc<Parameters, Response>,
|
||||||
|
params: Parameters,
|
||||||
|
): Promise<Response> {
|
||||||
|
const request = rpc.toRequest(params);
|
||||||
|
const response = await this.api.nativeFetch(request);
|
||||||
|
|
||||||
|
// FIXME: once legacy password generator is removed, replace forwarder-specific error
|
||||||
|
// messages with RPC-generalized ones.
|
||||||
|
let error: string = undefined;
|
||||||
|
let cause: string = undefined;
|
||||||
|
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
cause = await this.tryGetErrorMessage(response);
|
||||||
|
error = cause ? "forwarderInvalidTokenWithMessage" : "forwarderInvalidToken";
|
||||||
|
} else if (response.status >= 500) {
|
||||||
|
cause = await this.tryGetErrorMessage(response);
|
||||||
|
cause = cause ?? response.statusText;
|
||||||
|
error = "forwarderError";
|
||||||
|
}
|
||||||
|
|
||||||
|
let ok: Response = undefined;
|
||||||
|
if (!error && rpc.hasJsonPayload(response)) {
|
||||||
|
[ok, cause] = rpc.processJson(await response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
// success
|
||||||
|
if (ok) {
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// failure
|
||||||
|
if (!error) {
|
||||||
|
error = cause ? "forwarderError" : "forwarderUnknownError";
|
||||||
|
}
|
||||||
|
throw this.i18n.t(error, rpc.requestor.name, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async tryGetErrorMessage(response: Response) {
|
||||||
|
const body = (await response.text()) ?? "";
|
||||||
|
|
||||||
|
if (!body.startsWith("{")) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = JSON.parse(body);
|
||||||
|
if ("error" in json) {
|
||||||
|
return json.error;
|
||||||
|
} else if ("message" in json) {
|
||||||
|
return json.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
libs/common/src/tools/integration/rpc/rpc-definition.ts
Normal file
40
libs/common/src/tools/integration/rpc/rpc-definition.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { IntegrationRequest } from "./integration-request";
|
||||||
|
|
||||||
|
/** Defines how an integration processes an RPC call.
|
||||||
|
* @remarks This interface should not be used directly. Your integration should specialize
|
||||||
|
* it to fill a specific use-case. For example, the forwarder provides two specializations as follows:
|
||||||
|
*
|
||||||
|
* // optional; supplements the `IntegrationRequest` with an integrator-supplied account Id
|
||||||
|
* type GetAccountId = RpcConfiguration<IntegrationRequest, ForwarderContext<Settings>, ForwarderRequest>
|
||||||
|
*
|
||||||
|
* // generates a forwarding address
|
||||||
|
* type CreateForwardingEmail = RpcConfiguration<ForwarderRequest, ForwarderContext<Settings>, string>
|
||||||
|
*/
|
||||||
|
export interface RpcConfiguration<Request extends IntegrationRequest, Helper, Result> {
|
||||||
|
/** determine the URL of the lookup
|
||||||
|
* @param request describes the state of the integration site
|
||||||
|
* @param helper supplies logic from bitwarden specific to the integration site
|
||||||
|
*/
|
||||||
|
url(request: Request, helper: Helper): string;
|
||||||
|
|
||||||
|
/** format the body of the rpc call; when this method is not supplied, the request omits the body
|
||||||
|
* @param request describes the state of the integration site
|
||||||
|
* @param helper supplies logic from bitwarden specific to the integration site
|
||||||
|
* @returns a JSON object supplied as the body of the request
|
||||||
|
*/
|
||||||
|
body?(request: Request, helper: Helper): any;
|
||||||
|
|
||||||
|
/** returns true when there's a JSON payload to process
|
||||||
|
* @param response the fetch API response returned by the RPC call
|
||||||
|
* @param helper supplies logic from bitwarden specific to the integration site
|
||||||
|
*/
|
||||||
|
hasJsonPayload(response: Response, helper: Helper): boolean;
|
||||||
|
|
||||||
|
/** map body parsed as json payload of the rpc call.
|
||||||
|
* @param json the object to map
|
||||||
|
* @param helper supplies logic from bitwarden specific to the integration site
|
||||||
|
* @returns When the JSON is processed successfully, a 1-tuple whose value is the processed result.
|
||||||
|
* Otherwise, a 2-tuple whose first value is undefined, and whose second value is an error message.
|
||||||
|
*/
|
||||||
|
processJson(json: any, helper: Helper): [Result?, string?];
|
||||||
|
}
|
||||||
26
libs/common/src/tools/integration/rpc/rpc.ts
Normal file
26
libs/common/src/tools/integration/rpc/rpc.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { IntegrationMetadata } from "../integration-metadata";
|
||||||
|
|
||||||
|
import { IntegrationRequest } from "./integration-request";
|
||||||
|
|
||||||
|
/** A runtime RPC request that returns a JSON-encoded payload.
|
||||||
|
*/
|
||||||
|
export interface JsonRpc<Parameters extends IntegrationRequest, Result> {
|
||||||
|
/** information about the integration requesting RPC */
|
||||||
|
requestor: Readonly<IntegrationMetadata>;
|
||||||
|
|
||||||
|
/** creates a fetch request for the RPC
|
||||||
|
* @param request describes the state of the integration site
|
||||||
|
*/
|
||||||
|
toRequest(request: Parameters): Request;
|
||||||
|
|
||||||
|
/** returns true when there should be a JSON payload to process
|
||||||
|
* @param response the fetch API response returned by the RPC call
|
||||||
|
*/
|
||||||
|
hasJsonPayload(response: Response): boolean;
|
||||||
|
|
||||||
|
/** processes the json payload
|
||||||
|
* @param json the object to map
|
||||||
|
* @returns on success returns [Result], on failure returns [undefined, string]
|
||||||
|
*/
|
||||||
|
processJson(json: any): [Result?, string?];
|
||||||
|
}
|
||||||
2
libs/common/src/tools/integration/rpc/token-header.ts
Normal file
2
libs/common/src/tools/integration/rpc/token-header.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/** Token header patterns created by extensions */
|
||||||
|
export type TokenHeader = { Authorization: string } | { Authentication: string };
|
||||||
@@ -31,9 +31,12 @@ describe("Protonpass Json Importer", () => {
|
|||||||
expect(uriView.uri).toEqual("https://example.com/");
|
expect(uriView.uri).toEqual("https://example.com/");
|
||||||
expect(cipher.notes).toEqual("My login secure note.");
|
expect(cipher.notes).toEqual("My login secure note.");
|
||||||
|
|
||||||
expect(cipher.fields.at(2).name).toEqual("second 2fa secret");
|
expect(cipher.fields.at(0).name).toEqual("itemUsername");
|
||||||
expect(cipher.fields.at(2).value).toEqual("TOTPCODE");
|
expect(cipher.fields.at(0).value).toEqual("someOtherUsername");
|
||||||
expect(cipher.fields.at(2).type).toEqual(FieldType.Hidden);
|
|
||||||
|
expect(cipher.fields.at(3).name).toEqual("second 2fa secret");
|
||||||
|
expect(cipher.fields.at(3).value).toEqual("TOTPCODE");
|
||||||
|
expect(cipher.fields.at(3).type).toEqual(FieldType.Hidden);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse note data", async () => {
|
it("should parse note data", async () => {
|
||||||
|
|||||||
@@ -49,11 +49,12 @@ export const testData: ProtonPassJsonFile = {
|
|||||||
],
|
],
|
||||||
type: "login",
|
type: "login",
|
||||||
content: {
|
content: {
|
||||||
username: "Username",
|
itemEmail: "Username",
|
||||||
password: "Password",
|
password: "Password",
|
||||||
urls: ["https://example.com/", "https://example2.com/"],
|
urls: ["https://example.com/", "https://example2.com/"],
|
||||||
totpUri:
|
totpUri:
|
||||||
"otpauth://totp/Test%20Login%20-%20Personal%20Vault:Username?issuer=Test%20Login%20-%20Personal%20Vault&secret=TOTPCODE&algorithm=SHA1&digits=6&period=30",
|
"otpauth://totp/Test%20Login%20-%20Personal%20Vault:Username?issuer=Test%20Login%20-%20Personal%20Vault&secret=TOTPCODE&algorithm=SHA1&digits=6&period=30",
|
||||||
|
itemUsername: "someOtherUsername",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
state: 1,
|
state: 1,
|
||||||
@@ -156,10 +157,11 @@ export const testData: ProtonPassJsonFile = {
|
|||||||
extraFields: [],
|
extraFields: [],
|
||||||
type: "login",
|
type: "login",
|
||||||
content: {
|
content: {
|
||||||
username: "other vault username",
|
itemEmail: "other vault username",
|
||||||
password: "other vault password",
|
password: "other vault password",
|
||||||
urls: [],
|
urls: [],
|
||||||
totpUri: "JBSWY3DPEHPK3PXP",
|
totpUri: "JBSWY3DPEHPK3PXP",
|
||||||
|
itemUsername: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
state: 1,
|
state: 1,
|
||||||
|
|||||||
@@ -48,9 +48,10 @@ export class ProtonPassJsonImporter extends BaseImporter implements Importer {
|
|||||||
case "login": {
|
case "login": {
|
||||||
const loginContent = item.data.content as ProtonPassLoginItemContent;
|
const loginContent = item.data.content as ProtonPassLoginItemContent;
|
||||||
cipher.login.uris = this.makeUriArray(loginContent.urls);
|
cipher.login.uris = this.makeUriArray(loginContent.urls);
|
||||||
cipher.login.username = this.getValueOrDefault(loginContent.username);
|
cipher.login.username = this.getValueOrDefault(loginContent.itemEmail);
|
||||||
cipher.login.password = this.getValueOrDefault(loginContent.password);
|
cipher.login.password = this.getValueOrDefault(loginContent.password);
|
||||||
cipher.login.totp = this.getValueOrDefault(loginContent.totpUri);
|
cipher.login.totp = this.getValueOrDefault(loginContent.totpUri);
|
||||||
|
this.processKvp(cipher, "itemUsername", loginContent.itemUsername);
|
||||||
for (const extraField of item.data.extraFields) {
|
for (const extraField of item.data.extraFields) {
|
||||||
this.processKvp(
|
this.processKvp(
|
||||||
cipher,
|
cipher,
|
||||||
|
|||||||
@@ -56,10 +56,11 @@ export type ProtonPassItemExtraFieldData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ProtonPassLoginItemContent = {
|
export type ProtonPassLoginItemContent = {
|
||||||
username?: string;
|
itemEmail?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
urls?: string[];
|
urls?: string[];
|
||||||
totpUri?: string;
|
totpUri?: string;
|
||||||
|
itemUsername?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProtonPassCreditCardItemContent = {
|
export type ProtonPassCreditCardItemContent = {
|
||||||
|
|||||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -169,7 +169,7 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"regedit": "^3.0.3",
|
"regedit": "^3.0.3",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"rimraf": "5.0.7",
|
"rimraf": "5.0.8",
|
||||||
"sass": "1.74.1",
|
"sass": "1.74.1",
|
||||||
"sass-loader": "14.2.1",
|
"sass-loader": "14.2.1",
|
||||||
"storybook": "7.6.19",
|
"storybook": "7.6.19",
|
||||||
@@ -34530,10 +34530,11 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/rimraf": {
|
"node_modules/rimraf": {
|
||||||
"version": "5.0.7",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.8.tgz",
|
||||||
"integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
|
"integrity": "sha512-XSh0V2/yNhDEi8HwdIefD8MLgs4LQXPag/nEJWs3YUc3Upn+UHa1GyIkEg9xSSNt7HnkO5FjTvmcRzgf+8UZuw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^10.3.7"
|
"glob": "^10.3.7"
|
||||||
},
|
},
|
||||||
@@ -34541,7 +34542,7 @@
|
|||||||
"rimraf": "dist/esm/bin.mjs"
|
"rimraf": "dist/esm/bin.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"regedit": "^3.0.3",
|
"regedit": "^3.0.3",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"rimraf": "5.0.7",
|
"rimraf": "5.0.8",
|
||||||
"sass": "1.74.1",
|
"sass": "1.74.1",
|
||||||
"sass-loader": "14.2.1",
|
"sass-loader": "14.2.1",
|
||||||
"storybook": "7.6.19",
|
"storybook": "7.6.19",
|
||||||
|
|||||||
Reference in New Issue
Block a user