1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 23:03:32 +00:00

Merge branch 'master' into feature/org-admin-refresh

This commit is contained in:
Shane Melton
2022-10-11 13:28:19 -07:00
140 changed files with 3451 additions and 683 deletions

View File

@@ -64,7 +64,7 @@ export default class CommandsBackground {
}
private async generatePasswordToClipboard() {
const options = (await this.passwordGenerationService.getOptions())[0];
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
const password = await this.passwordGenerationService.generatePassword(options);
this.platformUtilsService.copyToClipboard(password, { window: window });
this.passwordGenerationService.addHistory(password);

View File

@@ -66,7 +66,7 @@ export default class ContextMenusBackground {
}
private async generatePasswordToClipboard() {
const options = (await this.passwordGenerationService.getOptions())[0];
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
const password = await this.passwordGenerationService.generatePassword(options);
this.platformUtilsService.copyToClipboard(password, { window: window });
this.passwordGenerationService.addHistory(password);

View File

@@ -247,7 +247,8 @@ export default class MainBackground {
return promise.then((result) => result.response === "unlocked");
}
}
},
window
);
this.i18nService = new I18nService(BrowserApi.getUILanguage(window));
this.encryptService = new EncryptService(this.cryptoFunctionService, this.logService, true);

View File

@@ -446,6 +446,8 @@ export default class NotificationBackground {
}
private async allowPersonalOwnership(): Promise<boolean> {
return !(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership));
return !(await firstValueFrom(
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
));
}
}

View File

@@ -0,0 +1,37 @@
import { AutofillService as AbstractAutoFillService } from "../../services/abstractions/autofill.service";
import AutofillService from "../../services/autofill.service";
import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory";
import { EventServiceInitOptions, eventServiceFactory } from "./event-service.factory";
import { CachedServices, factory, FactoryOptions } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
import { totpServiceFactory, TotpServiceInitOptions } from "./totp-service.factory";
type AutoFillServiceOptions = FactoryOptions;
export type AutoFillServiceInitOptions = AutoFillServiceOptions &
CipherServiceInitOptions &
StateServiceInitOptions &
TotpServiceInitOptions &
EventServiceInitOptions &
LogServiceInitOptions;
export function autofillServiceFactory(
cache: { autofillService?: AbstractAutoFillService } & CachedServices,
opts: AutoFillServiceInitOptions
): Promise<AbstractAutoFillService> {
return factory(
cache,
"autofillService",
opts,
async () =>
new AutofillService(
await cipherServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await totpServiceFactory(cache, opts),
await eventServiceFactory(cache, opts),
await logServiceFactory(cache, opts)
)
);
}

View File

@@ -44,7 +44,7 @@ export function cipherServiceFactory(
await apiServiceFactory(cache, opts),
await fileUploadServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts),
opts.cipherServiceOptions.searchServiceFactory === undefined
opts.cipherServiceOptions?.searchServiceFactory === undefined
? () => cache.searchService
: opts.cipherServiceOptions.searchServiceFactory,
await logServiceFactory(cache, opts),

View File

@@ -0,0 +1,40 @@
import { EventService as AbstractEventService } from "@bitwarden/common/abstractions/event.service";
import { EventService } from "@bitwarden/common/services/event.service";
import { apiServiceFactory, ApiServiceInitOptions } from "./api-service.factory";
import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory";
import { FactoryOptions, CachedServices, factory } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
import {
organizationServiceFactory,
OrganizationServiceInitOptions,
} from "./organization-service.factory";
import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory";
type EventServiceOptions = FactoryOptions;
export type EventServiceInitOptions = EventServiceOptions &
ApiServiceInitOptions &
CipherServiceInitOptions &
StateServiceInitOptions &
LogServiceInitOptions &
OrganizationServiceInitOptions;
export function eventServiceFactory(
cache: { eventService?: AbstractEventService } & CachedServices,
opts: EventServiceInitOptions
): Promise<AbstractEventService> {
return factory(
cache,
"eventService",
opts,
async () =>
new EventService(
await apiServiceFactory(cache, opts),
await cipherServiceFactory(cache, opts),
await stateServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await organizationServiceFactory(cache, opts)
)
);
}

View File

@@ -28,7 +28,8 @@ export function platformUtilsServiceFactory(
new BrowserPlatformUtilsService(
await messagingServiceFactory(cache, opts),
opts.platformUtilsServiceOptions.clipboardWriteCallback,
opts.platformUtilsServiceOptions.biometricCallback
opts.platformUtilsServiceOptions.biometricCallback,
opts.platformUtilsServiceOptions.win
)
);
}

View File

@@ -0,0 +1,31 @@
import { TotpService as AbstractTotpService } from "@bitwarden/common/abstractions/totp.service";
import { TotpService } from "@bitwarden/common/services/totp.service";
import {
cryptoFunctionServiceFactory,
CryptoFunctionServiceInitOptions,
} from "./crypto-function-service.factory";
import { CachedServices, factory, FactoryOptions } from "./factory-options";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
type TotpServiceOptions = FactoryOptions;
export type TotpServiceInitOptions = TotpServiceOptions &
CryptoFunctionServiceInitOptions &
LogServiceInitOptions;
export function totpServiceFactory(
cache: { totpService?: AbstractTotpService } & CachedServices,
opts: TotpServiceInitOptions
): Promise<AbstractTotpService> {
return factory(
cache,
"totpService",
opts,
async () =>
new TotpService(
await cryptoFunctionServiceFactory(cache, opts),
await logServiceFactory(cache, opts)
)
);
}

View File

@@ -1,27 +1,15 @@
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/globalState";
import { AuthService } from "@bitwarden/common/services/auth.service";
import { CipherService } from "@bitwarden/common/services/cipher.service";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
import { EncryptService } from "@bitwarden/common/services/encrypt.service";
import { NoopEventService } from "@bitwarden/common/services/noopEvent.service";
import { SearchService } from "@bitwarden/common/services/search.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service";
import { WebCryptoFunctionService } from "@bitwarden/common/services/webCryptoFunction.service";
import { authServiceFactory } from "../background/service_factories/auth-service.factory";
import { autofillServiceFactory } from "../background/service_factories/autofill-service.factory";
import { CachedServices } from "../background/service_factories/factory-options";
import { logServiceFactory } from "../background/service_factories/log-service.factory";
import { BrowserApi } from "../browser/browserApi";
import { AutoFillActiveTabCommand } from "../commands/autoFillActiveTabCommand";
import { Account } from "../models/account";
import { StateService as AbstractStateService } from "../services/abstractions/state.service";
import AutofillService from "../services/autofill.service";
import { BrowserCryptoService } from "../services/browserCrypto.service";
import BrowserLocalStorageService from "../services/browserLocalStorage.service";
import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
import I18nService from "../services/i18n.service";
import { KeyGenerationService } from "../services/keyGeneration.service";
import { LocalBackedSessionStorageService } from "../services/localBackedSessionStorage.service";
import { StateService } from "../services/state.service";
export const onCommandListener = async (command: string, tab: chrome.tabs.Tab) => {
switch (command) {
@@ -32,100 +20,44 @@ export const onCommandListener = async (command: string, tab: chrome.tabs.Tab) =
};
const doAutoFillLogin = async (tab: chrome.tabs.Tab): Promise<void> => {
const logService = new ConsoleLogService(false);
const cryptoFunctionService = new WebCryptoFunctionService(self);
const storageService = new BrowserLocalStorageService();
const secureStorageService = new BrowserLocalStorageService();
const memoryStorageService = new LocalBackedSessionStorageService(
new EncryptService(cryptoFunctionService, logService, false),
new KeyGenerationService(cryptoFunctionService)
);
const stateFactory = new StateFactory(GlobalState, Account);
const stateMigrationService = new StateMigrationService(
storageService,
secureStorageService,
stateFactory
);
const stateService: AbstractStateService = new StateService(
storageService,
secureStorageService,
memoryStorageService, // AbstractStorageService
logService,
stateMigrationService,
stateFactory
);
await stateService.init();
const platformUtils = new BrowserPlatformUtilsService(
null, // MessagingService
null, // clipboardWriteCallback
null // biometricCallback
);
const cryptoService = new BrowserCryptoService(
cryptoFunctionService,
null, // AbstractEncryptService
platformUtils,
logService,
stateService
);
const settingsService = new SettingsService(stateService);
const i18nService = new I18nService(chrome.i18n.getUILanguage());
await i18nService.init();
// Don't love this pt.1
let searchService: SearchService = null;
const cipherService = new CipherService(
cryptoService,
settingsService,
null, // ApiService
null, // FileUploadService,
i18nService,
() => searchService, // Don't love this pt.2
logService,
stateService
);
// Don't love this pt.3
searchService = new SearchService(cipherService, logService, i18nService);
// TODO: Remove this before we encourage anyone to start using this
const eventService = new NoopEventService();
const autofillService = new AutofillService(
cipherService,
stateService,
null, // TotpService
eventService,
logService
);
const authService = new AuthService(
cryptoService, // CryptoService
null, // ApiService
null, // TokenService
null, // AppIdService
platformUtils,
null, // MessagingService
logService,
null, // KeyConnectorService
null, // EnvironmentService
stateService,
null, // TwoFactorService
i18nService
);
const cachedServices: CachedServices = {};
const opts = {
cryptoFunctionServiceOptions: {
win: self,
},
encryptServiceOptions: {
logMacFailures: true,
},
logServiceOptions: {
isDev: false,
},
platformUtilsServiceOptions: {
clipboardWriteCallback: () => Promise.resolve(),
biometricCallback: () => Promise.resolve(false),
win: self,
},
stateServiceOptions: {
stateFactory: new StateFactory(GlobalState, Account),
},
stateMigrationServiceOptions: {
stateFactory: new StateFactory(GlobalState, Account),
},
apiServiceOptions: {
logoutCallback: () => Promise.resolve(),
},
keyConnectorServiceOptions: {
logoutCallback: () => Promise.resolve(),
},
i18nServiceOptions: {
systemLanguage: BrowserApi.getUILanguage(self),
},
cipherServiceOptions: {
searchServiceFactory: null as () => SearchService, // No dependence on search service
},
};
const logService = await logServiceFactory(cachedServices, opts);
const authService = await authServiceFactory(cachedServices, opts);
const autofillService = await autofillServiceFactory(cachedServices, opts);
const authStatus = await authService.getAuthStatus();
if (authStatus < AuthenticationStatus.Unlocked) {

View File

@@ -12,7 +12,6 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
@@ -28,8 +27,6 @@ export class LockComponent extends BaseLockComponent {
biometricError: string;
pendingBiometric = false;
authenicatedUrl = "/tabs/current";
unAuthenicatedUrl = "/update-temp-password";
constructor(
router: Router,
@@ -45,8 +42,7 @@ export class LockComponent extends BaseLockComponent {
logService: LogService,
keyConnectorService: KeyConnectorService,
ngZone: NgZone,
private authService: AuthService,
private syncService: SyncService
private authService: AuthService
) {
super(
router,
@@ -63,17 +59,12 @@ export class LockComponent extends BaseLockComponent {
keyConnectorService,
ngZone
);
this.successRoute = "/tabs/current";
this.isInitialLockScreen = (window as any).previousPopupUrl == null;
}
async ngOnInit() {
await super.ngOnInit();
await this.syncService.fullSync(true);
const forcePasswordReset = await this.stateService.getForcePasswordReset();
this.successRoute = forcePasswordReset === true ? this.unAuthenicatedUrl : this.authenicatedUrl;
const disableAutoBiometricsPrompt =
(await this.stateService.getDisableAutoBiometricsPrompt()) ?? true;

View File

@@ -172,14 +172,10 @@ export default class AutofillService implements AutofillServiceInterface {
} else {
cipher = await this.cipherService.getLastUsedForUrl(tab.url, true);
}
if (cipher == null) {
return null;
}
}
if (cipher.reprompt !== CipherRepromptType.None) {
return;
if (cipher == null || cipher.reprompt !== CipherRepromptType.None) {
return null;
}
const totpCode = await this.doAutoFill({

View File

@@ -16,7 +16,7 @@ describe("Browser Utils Service", () => {
let browserPlatformUtilsService: BrowserPlatformUtilsService;
beforeEach(() => {
(window as any).matchMedia = jest.fn().mockReturnValueOnce({});
browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null);
browserPlatformUtilsService = new BrowserPlatformUtilsService(null, null, null, self);
});
afterEach(() => {

View File

@@ -19,7 +19,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
constructor(
private messagingService: MessagingService,
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
private biometricCallback: () => Promise<boolean>
private biometricCallback: () => Promise<boolean>,
private win: Window & typeof globalThis
) {}
getDevice(): DeviceType {
@@ -33,8 +34,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
) {
this.deviceCache = DeviceType.FirefoxExtension;
} else if (
(self.opr && self.opr.addons) ||
self.opera ||
(!!this.win.opr && !!opr.addons) ||
!!this.win.opera ||
navigator.userAgent.indexOf(" OPR/") >= 0
) {
this.deviceCache = DeviceType.OperaExtension;
@@ -42,7 +43,7 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
this.deviceCache = DeviceType.EdgeExtension;
} else if (navigator.userAgent.indexOf(" Vivaldi/") !== -1) {
this.deviceCache = DeviceType.VivaldiExtension;
} else if (window.chrome && navigator.userAgent.indexOf(" Chrome/") !== -1) {
} else if (this.win.chrome && navigator.userAgent.indexOf(" Chrome/") !== -1) {
this.deviceCache = DeviceType.ChromeExtension;
} else if (navigator.userAgent.indexOf(" Safari/") !== -1) {
this.deviceCache = DeviceType.SafariExtension;
@@ -178,8 +179,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
}
copyToClipboard(text: string, options?: any): void {
let win = window;
let doc = window.document;
let win = this.win;
let doc = this.win.document;
if (options && (options.window || options.win)) {
win = options.window || options.win;
doc = win.document;
@@ -238,8 +239,8 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
}
async readFromClipboard(options?: any): Promise<string> {
let win = window;
let doc = window.document;
let win = this.win;
let doc = this.win.document;
if (options && (options.window || options.win)) {
win = options.window || options.win;
doc = win.document;
@@ -335,7 +336,7 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
}
sidebarViewName(): string {
if (window.chrome.sidebarAction && this.isFirefox()) {
if (this.win.chrome.sidebarAction && this.isFirefox()) {
return "sidebar";
} else if (this.isOpera() && typeof opr !== "undefined" && opr.sidebarAction) {
return "sidebar_panel";

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { Utils } from "@bitwarden/common/misc/utils";

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { LogService } from "@bitwarden/common/abstractions/log.service";

View File

@@ -1,5 +1,6 @@
import * as program from "commander";
import * as inquirer from "inquirer";
import { firstValueFrom } from "rxjs";
import { ExportFormat, ExportService } from "@bitwarden/common/abstractions/export.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
@@ -15,7 +16,9 @@ export class ExportCommand {
async run(options: program.OptionValues): Promise<Response> {
if (
options.organizationid == null &&
(await this.policyService.policyAppliesToUser(PolicyType.DisablePersonalVaultExport))
(await firstValueFrom(
this.policyService.policyAppliesToActiveUser$(PolicyType.DisablePersonalVaultExport)
))
) {
return Response.badRequest(
"One or more organization policies prevents you from exporting your personal vault."

View File

@@ -13,7 +13,6 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
@@ -25,8 +24,6 @@ const BroadcasterSubscriptionId = "LockComponent";
})
export class LockComponent extends BaseLockComponent {
private deferFocus: boolean = null;
authenicatedUrl = "vault";
unAuthenicatedUrl = "update-temp-password";
constructor(
router: Router,
@@ -43,8 +40,7 @@ export class LockComponent extends BaseLockComponent {
private broadcasterService: BroadcasterService,
ngZone: NgZone,
logService: LogService,
keyConnectorService: KeyConnectorService,
private syncService: SyncService
keyConnectorService: KeyConnectorService
) {
super(
router,
@@ -67,11 +63,6 @@ export class LockComponent extends BaseLockComponent {
await super.ngOnInit();
const autoPromptBiometric = !(await this.stateService.getNoAutoPromptBiometrics());
await this.syncService.fullSync(true);
const forcePasswordReset = await this.stateService.getForcePasswordReset();
this.successRoute = forcePasswordReset === true ? this.unAuthenicatedUrl : this.authenicatedUrl;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.route.queryParams.subscribe((params) => {
if (this.supportsBiometric && params.promptBiometric && autoPromptBiometric) {

View File

@@ -1,6 +1,7 @@
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router";
// eslint-disable-next-line no-restricted-imports
import { Substitute } from "@fluffy-spoon/substitute";
import { mock, MockProxy } from "jest-mock-extended";

View File

@@ -182,12 +182,25 @@ export class NativeMessageHandlerService {
this.ddgSharedSecret = SymmetricCryptoKey.fromJSON({ keyB64: storedKey });
}
return JSON.parse(
await this.cryptoService.decryptToUtf8(
try {
let decryptedResult = await this.cryptoService.decryptToUtf8(
message.encryptedCommand as EncString,
this.ddgSharedSecret
)
);
);
decryptedResult = this.trimNullCharsFromMessage(decryptedResult);
return JSON.parse(decryptedResult);
} catch {
this.sendResponse({
messageId: message.messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "cannot-decrypt",
},
});
return;
}
}
private async sendEncryptedResponse(
@@ -218,4 +231,23 @@ export class NativeMessageHandlerService {
private sendResponse(response: EncryptedMessageResponse | UnencryptedMessageResponse) {
ipcRenderer.send("nativeMessagingReply", response);
}
// Trim all null bytes padded at the end of messages. This happens with C encryption libraries.
private trimNullCharsFromMessage(message: string): string {
const charNull = 0;
const charRightCurlyBrace = 125;
const charRightBracket = 93;
for (let i = message.length - 1; i >= 0; i--) {
if (message.charCodeAt(i) === charNull) {
message = message.substring(0, message.length - 1);
} else if (
message.charCodeAt(i) === charRightCurlyBrace ||
message.charCodeAt(i) === charRightBracket
) {
break;
}
}
return message;
}
}

View File

@@ -14,7 +14,8 @@
"**/organizations/policies/*",
"@bitwarden/web-vault/*",
"src/**/*"
]
],
"paths": ["@fluffy-spoon/substitute"]
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
"version": "2022.9.2",
"version": "2022.10.0",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

View File

@@ -37,9 +37,11 @@ export class LoginWithDeviceComponent
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
onSuccessfulLogin: () => Promise<any>;
onSuccessfulLoginNavigate: () => Promise<any>;
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
protected twoFactorRoute = "2fa";
protected successRoute = "vault";
protected forcePasswordResetRoute = "update-temp-password";
private authRequestKeyPair: [publicKey: ArrayBuffer, privateKey: ArrayBuffer];
constructor(
@@ -119,14 +121,29 @@ export class LoginWithDeviceComponent
}
const credentials = await this.buildLoginCredntials(requestId, response);
await this.authService.logIn(credentials);
if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin();
}
if (this.onSuccessfulLoginNavigate != null) {
this.onSuccessfulLoginNavigate();
const loginResponse = await this.authService.logIn(credentials);
if (loginResponse.requiresTwoFactor) {
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
this.onSuccessfulLoginTwoFactorNavigate();
} else {
this.router.navigate([this.twoFactorRoute]);
}
} else if (loginResponse.forcePasswordReset) {
if (this.onSuccessfulLoginForceResetNavigate != null) {
this.onSuccessfulLoginForceResetNavigate();
} else {
this.router.navigate([this.forcePasswordResetRoute]);
}
} else {
this.router.navigate([this.successRoute]);
if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin();
}
if (this.onSuccessfulLoginNavigate != null) {
this.onSuccessfulLoginNavigate();
} else {
this.router.navigate([this.successRoute]);
}
}
} catch (error) {
this.logService.error(error);

View File

@@ -1,6 +1,7 @@
import { Component, NgZone } from "@angular/core";
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { first } from "rxjs/operators";
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component";
@@ -29,13 +30,14 @@ import { RouterService, StateService } from "../../core";
selector: "app-login",
templateUrl: "login.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class LoginComponent extends BaseLoginComponent {
export class LoginComponent extends BaseLoginComponent implements OnInit, OnDestroy {
showResetPasswordAutoEnrollWarning = false;
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
policies: ListResponse<PolicyResponse>;
showPasswordless = false;
private destroy$ = new Subject<void>();
constructor(
authService: AuthService,
router: Router,
@@ -128,14 +130,23 @@ export class LoginComponent extends BaseLoginComponent {
this.showResetPasswordAutoEnrollWarning =
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
this.enforcedPasswordPolicyOptions =
await this.policyService.getMasterPasswordPolicyOptions(policyList);
this.policyService
.masterPasswordPolicyOptions$(policyList)
.pipe(takeUntil(this.destroy$))
.subscribe((enforcedPasswordPolicyOptions) => {
this.enforcedPasswordPolicyOptions = enforcedPasswordPolicyOptions;
});
}
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async goAfterLogIn() {
const masterPassword = this.formGroup.get("masterPassword")?.value;
const masterPassword = this.formGroup.value.masterPassword;
// Check master password against policy
if (this.enforcedPasswordPolicyOptions != null) {
@@ -170,7 +181,7 @@ export class LoginComponent extends BaseLoginComponent {
}
async submit() {
const rememberEmail = this.formGroup.get("rememberEmail")?.value;
const rememberEmail = this.formGroup.value.rememberEmail;
await this.stateService.setRememberEmail(rememberEmail);
if (!rememberEmail) {
@@ -192,7 +203,7 @@ export class LoginComponent extends BaseLoginComponent {
}
private getPasswordStrengthUserInput() {
const email = this.formGroup.get("email")?.value;
const email = this.formGroup.value.email;
let userInput: string[] = [];
const atPosition = email.indexOf("@");
if (atPosition > -1) {

View File

@@ -73,7 +73,7 @@ export class RegisterFormComponent extends BaseRegisterComponent {
this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
this.passwordStrengthResult.score,
this.formGroup.get("masterPassword")?.value,
this.formGroup.value.masterPassword,
this.enforcedPolicyOptions
)
) {

View File

@@ -1,6 +1,7 @@
import { Component } from "@angular/core";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { first } from "rxjs/operators";
import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/components/register.component";
@@ -27,14 +28,14 @@ import { RouterService } from "../core";
selector: "app-register",
templateUrl: "register.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class RegisterComponent extends BaseRegisterComponent {
export class RegisterComponent extends BaseRegisterComponent implements OnInit, OnDestroy {
email = "";
showCreateOrgMessage = false;
layout = "";
enforcedPolicyOptions: MasterPasswordPolicyOptions;
private policies: Policy[];
private destroy$ = new Subject<void>();
constructor(
formValidationErrorService: FormValidationErrorsService,
@@ -130,11 +131,19 @@ export class RegisterComponent extends BaseRegisterComponent {
}
if (this.policies != null) {
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(
this.policies
);
this.policyService
.masterPasswordPolicyOptions$(this.policies)
.pipe(takeUntil(this.destroy$))
.subscribe((enforcedPasswordPolicyOptions) => {
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
});
}
await super.ngOnInit();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@@ -57,8 +57,8 @@ export class BillingComponent extends OrganizationPlansComponent {
async ngOnInit() {
const additionalSeats = this.product == ProductType.Families ? 0 : 1;
this.formGroup.patchValue({
name: this.orgInfoForm.get("name")?.value,
billingEmail: this.orgInfoForm.get("email")?.value,
name: this.orgInfoForm.value.name,
billingEmail: this.orgInfoForm.value.email,
additionalSeats: additionalSeats,
plan: this.plan,
product: this.product,

View File

@@ -5,8 +5,9 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testin
import { FormBuilder, UntypedFormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
// eslint-disable-next-line no-restricted-imports
import { Substitute } from "@fluffy-spoon/substitute";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, of } from "rxjs";
import { I18nPipe } from "@bitwarden/angular/pipes/i18n.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -46,7 +47,7 @@ describe("TrialInitiationComponent", () => {
};
policyServiceMock = {
getMasterPasswordPolicyOptions: jest.fn(),
masterPasswordPolicyOptions$: jest.fn(),
};
TestBed.configureTestingModule({
@@ -144,14 +145,16 @@ describe("TrialInitiationComponent", () => {
},
],
});
policyServiceMock.getMasterPasswordPolicyOptions.mockReturnValueOnce({
minComplexity: 4,
minLength: 10,
requireLower: null,
requireNumbers: null,
requireSpecial: null,
requireUpper: null,
} as MasterPasswordPolicyOptions);
policyServiceMock.masterPasswordPolicyOptions$.mockReturnValue(
of({
minComplexity: 4,
minLength: 10,
requireLower: null,
requireNumbers: null,
requireSpecial: null,
requireUpper: null,
} as MasterPasswordPolicyOptions)
);
// Need to recreate component with new service mocks
fixture = TestBed.createComponent(TrialInitiationComponent);

View File

@@ -1,9 +1,9 @@
import { StepperSelectionEvent } from "@angular/cdk/stepper";
import { TitleCasePipe } from "@angular/common";
import { Component, OnInit, ViewChild } from "@angular/core";
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { UntypedFormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs";
import { first, Subject, takeUntil } from "rxjs";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
@@ -24,8 +24,7 @@ import { VerticalStepperComponent } from "./vertical-stepper/vertical-stepper.co
selector: "app-trial",
templateUrl: "trial-initiation.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class TrialInitiationComponent implements OnInit {
export class TrialInitiationComponent implements OnInit, OnDestroy {
email = "";
org = "";
orgInfoSubLabel = "";
@@ -63,6 +62,8 @@ export class TrialInitiationComponent implements OnInit {
}
}
private destroy$ = new Subject<void>();
constructor(
private route: ActivatedRoute,
protected router: Router,
@@ -140,12 +141,20 @@ export class TrialInitiationComponent implements OnInit {
}
if (this.policies != null) {
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(
this.policies
);
this.policyService
.masterPasswordPolicyOptions$(this.policies)
.pipe(takeUntil(this.destroy$))
.subscribe((enforcedPasswordPolicyOptions) => {
this.enforcedPolicyOptions = enforcedPasswordPolicyOptions;
});
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
stepSelectionChange(event: StepperSelectionEvent) {
// Set org info sub label
if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") {

View File

@@ -3,7 +3,6 @@ import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ValidationService } from "@bitwarden/angular/services/validation.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@@ -11,6 +10,7 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
import { ProviderUserStatusType } from "@bitwarden/common/enums/providerUserStatusType";

View File

@@ -1,16 +1,32 @@
import { Injectable } from "@angular/core";
import { Injectable, OnDestroy, OnInit } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { DeviceType } from "@bitwarden/common/enums/deviceType";
import { EventType } from "@bitwarden/common/enums/eventType";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { Policy } from "@bitwarden/common/models/domain/policy";
import { EventResponse } from "@bitwarden/common/models/response/eventResponse";
@Injectable()
export class EventService {
export class EventService implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
private policies: Policy[];
constructor(private i18nService: I18nService, private policyService: PolicyService) {}
ngOnInit(): void {
this.policyService.policies$.pipe(takeUntil(this.destroy$)).subscribe((policies) => {
this.policies = policies;
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
getDefaultDateFilters() {
const d = new Date();
const end = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59);
@@ -326,8 +342,7 @@ export class EventService {
case EventType.Policy_Updated: {
msg = this.i18nService.t("modifiedPolicyId", this.formatPolicyId(ev));
const policies = await this.policyService.getAll();
const policy = policies.filter((p) => p.id === ev.policyId)[0];
const policy = this.policies.filter((p) => p.id === ev.policyId)[0];
let p1 = this.getShortId(ev.policyId);
if (policy != null) {
p1 = PolicyType[policy.type];

View File

@@ -1,11 +1,10 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { combineLatest, concatMap, Subject, takeUntil } from "rxjs";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ValidationService } from "@bitwarden/angular/services/validation.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@@ -13,11 +12,11 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
import { PolicyType } from "@bitwarden/common/enums/policyType";
@@ -43,10 +42,9 @@ import { UserGroupsComponent } from "./user-groups.component";
selector: "app-org-people",
templateUrl: "people.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class PeopleComponent
extends BasePeopleComponent<OrganizationUserUserDetailsResponse>
implements OnInit
implements OnInit, OnDestroy
{
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
@@ -77,6 +75,8 @@ export class PeopleComponent
orgResetPasswordPolicyEnabled = false;
callingUserType: OrganizationUserType = null;
private destroy$ = new Subject<void>();
constructor(
apiService: ApiService,
private route: ActivatedRoute,
@@ -84,10 +84,8 @@ export class PeopleComponent
modalService: ModalService,
platformUtilsService: PlatformUtilsService,
cryptoService: CryptoService,
private router: Router,
searchService: SearchService,
validationService: ValidationService,
private policyApiService: PolicyApiServiceAbstraction,
private policyService: PolicyService,
logService: LogService,
searchPipe: SearchPipe,
@@ -113,53 +111,63 @@ export class PeopleComponent
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
const organization = await this.organizationService.get(this.organizationId);
this.accessEvents = organization.useEvents;
this.accessGroups = organization.useGroups;
this.canResetPassword = organization.canManageUsersPassword;
this.orgUseResetPassword = organization.useResetPassword;
this.callingUserType = organization.type;
this.orgHasKeys = organization.hasPublicAndPrivateKeys;
combineLatest([this.route.params, this.route.queryParams, this.policyService.policies$])
.pipe(
concatMap(async ([params, qParams, policies]) => {
this.organizationId = params.organizationId;
const organization = await this.organizationService.get(this.organizationId);
this.accessEvents = organization.useEvents;
this.accessGroups = organization.useGroups;
this.canResetPassword = organization.canManageUsersPassword;
this.orgUseResetPassword = organization.useResetPassword;
this.callingUserType = organization.type;
this.orgHasKeys = organization.hasPublicAndPrivateKeys;
// Backfill pub/priv key if necessary
if (this.canResetPassword && !this.orgHasKeys) {
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId);
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
const response = await this.organizationApiService.updateKeys(this.organizationId, request);
if (response != null) {
this.orgHasKeys = response.publicKey != null && response.privateKey != null;
await this.syncService.fullSync(true); // Replace oganizations with new data
} else {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
}
await this.load();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
if (qParams.viewEvents != null) {
const user = this.users.filter((u) => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) {
this.events(user[0]);
// Backfill pub/priv key if necessary
if (this.canResetPassword && !this.orgHasKeys) {
const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId);
const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey);
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
const response = await this.organizationApiService.updateKeys(
this.organizationId,
request
);
if (response != null) {
this.orgHasKeys = response.publicKey != null && response.privateKey != null;
await this.syncService.fullSync(true); // Replace oganizations with new data
} else {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
}
}
});
});
const resetPasswordPolicy = policies
.filter((policy) => policy.type === PolicyType.ResetPassword)
.find((p) => p.organizationId === this.organizationId);
this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled;
await this.load();
this.searchText = qParams.search;
if (qParams.viewEvents != null) {
const user = this.users.filter((u) => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) {
this.events(user[0]);
}
}
}),
takeUntil(this.destroy$)
)
.subscribe();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async load() {
const resetPasswordPolicy = await this.policyApiService.getPolicyForOrganization(
PolicyType.ResetPassword,
this.organizationId
);
this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled;
super.load();
await super.load();
}
getUsers(): Promise<ListResponse<OrganizationUserUserDetailsResponse>> {

View File

@@ -1,4 +1,13 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core";
import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import zxcvbn from "zxcvbn";
import { PasswordStrengthComponent } from "@bitwarden/angular/shared/components/password-strength/password-strength.component";
@@ -18,7 +27,7 @@ import { OrganizationUserResetPasswordRequest } from "@bitwarden/common/models/r
selector: "app-reset-password",
templateUrl: "reset-password.component.html",
})
export class ResetPasswordComponent implements OnInit {
export class ResetPasswordComponent implements OnInit, OnDestroy {
@Input() name: string;
@Input() email: string;
@Input() id: string;
@@ -32,6 +41,8 @@ export class ResetPasswordComponent implements OnInit {
passwordStrengthResult: zxcvbn.ZXCVBNResult;
formPromise: Promise<any>;
private destroy$ = new Subject<void>();
constructor(
private apiService: ApiService,
private i18nService: I18nService,
@@ -43,8 +54,18 @@ export class ResetPasswordComponent implements OnInit {
) {}
async ngOnInit() {
// Get Enforced Policy Options
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
this.policyService
.masterPasswordPolicyOptions$()
.pipe(takeUntil(this.destroy$))
.subscribe(
(enforcedPasswordPolicyOptions) =>
(this.enforcedPolicyOptions = enforcedPasswordPolicyOptions)
);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get loggedOutWarningName() {
@@ -52,7 +73,7 @@ export class ResetPasswordComponent implements OnInit {
}
async generatePassword() {
const options = (await this.passwordGenerationService.getOptions())[0];
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
this.newPassword = await this.passwordGenerationService.generatePassword(options);
this.passwordStrengthComponent.updatePasswordStrength(this.newPassword);
}

View File

@@ -4,12 +4,12 @@ import { Observable, Subject } from "rxjs";
import { first, map, takeUntil } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ValidationService } from "@bitwarden/angular/services/validation.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
import { PlanSponsorshipType } from "@bitwarden/common/enums/planSponsorshipType";
import { PlanType } from "@bitwarden/common/enums/planType";
import { ProductType } from "@bitwarden/common/enums/productType";

View File

@@ -47,7 +47,6 @@ export class OrganizationImportComponent extends ImportComponent {
this.organizationId = params.organizationId;
this.successNavigate = ["organizations", this.organizationId, "vault"];
await super.ngOnInit();
this.importBlockedByPolicy = false;
});
const organization = await this.organizationService.get(this.organizationId);
this.organizationName = organization.name;

View File

@@ -1,4 +1,5 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { takeUntil } from "rxjs";
import { ChangePasswordComponent } from "@bitwarden/angular/components/change-password.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -21,7 +22,11 @@ import { PolicyResponse } from "@bitwarden/common/models/response/policyResponse
selector: "emergency-access-takeover",
templateUrl: "emergency-access-takeover.component.html",
})
export class EmergencyAccessTakeoverComponent extends ChangePasswordComponent implements OnInit {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class EmergencyAccessTakeoverComponent
extends ChangePasswordComponent
implements OnInit, OnDestroy
{
@Output() onDone = new EventEmitter();
@Input() emergencyAccessId: string;
@Input() name: string;
@@ -59,12 +64,19 @@ export class EmergencyAccessTakeoverComponent extends ChangePasswordComponent im
const policies = response.data.map(
(policyResponse: PolicyResponse) => new Policy(new PolicyData(policyResponse))
);
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(
policies
);
this.policyService
.masterPasswordPolicyOptions$(policies)
.pipe(takeUntil(this.destroy$))
.subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions));
}
}
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
ngOnDestroy(): void {
super.ngOnDestroy();
}
async submit() {
if (!(await this.strongPassword())) {
return;

View File

@@ -1,6 +1,15 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core";
import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from "@angular/core";
import { UntypedFormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
@@ -35,7 +44,7 @@ interface OnSuccessArgs {
selector: "app-organization-plans",
templateUrl: "organization-plans.component.html",
})
export class OrganizationPlansComponent implements OnInit {
export class OrganizationPlansComponent implements OnInit, OnDestroy {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
@@ -73,6 +82,8 @@ export class OrganizationPlansComponent implements OnInit {
plans: PlanResponse[];
private destroy$ = new Subject<void>();
constructor(
private apiService: ApiService,
private i18nService: I18nService,
@@ -114,9 +125,21 @@ export class OrganizationPlansComponent implements OnInit {
this.formGroup.controls.billingEmail.addValidators(Validators.required);
}
this.policyService
.policyAppliesToActiveUser$(PolicyType.SingleOrg)
.pipe(takeUntil(this.destroy$))
.subscribe((policyAppliesToActiveUser) => {
this.singleOrgPolicyBlock = policyAppliesToActiveUser;
});
this.loading = false;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get createOrganization() {
return this.organizationId == null;
}
@@ -288,8 +311,6 @@ export class OrganizationPlansComponent implements OnInit {
}
async submit() {
this.singleOrgPolicyBlock = await this.userHasBlockingSingleOrgPolicy();
if (this.singleOrgPolicyBlock) {
return;
}
@@ -353,10 +374,6 @@ export class OrganizationPlansComponent implements OnInit {
}
}
private async userHasBlockingSingleOrgPolicy() {
return this.policyService.policyAppliesToUser(PolicyType.SingleOrg);
}
private async updateOrganization(orgId: string) {
const request = new OrganizationUpgradeRequest();
request.businessName = this.formGroup.controls.businessOwned.value

View File

@@ -1,4 +1,5 @@
import { Component, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core";
import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@@ -25,8 +26,7 @@ import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component";
selector: "app-two-factor-setup",
templateUrl: "two-factor-setup.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class TwoFactorSetupComponent implements OnInit {
export class TwoFactorSetupComponent implements OnInit, OnDestroy {
@ViewChild("recoveryTemplate", { read: ViewContainerRef, static: true })
recoveryModalRef: ViewContainerRef;
@ViewChild("authenticatorTemplate", { read: ViewContainerRef, static: true })
@@ -49,6 +49,9 @@ export class TwoFactorSetupComponent implements OnInit {
modal: ModalRef;
formPromise: Promise<any>;
private destroy$ = new Subject<void>();
private twoFactorAuthPolicyAppliesToActiveUser: boolean;
constructor(
protected apiService: ApiService,
protected modalService: ModalService,
@@ -93,9 +96,22 @@ export class TwoFactorSetupComponent implements OnInit {
}
this.providers.sort((a: any, b: any) => a.sort - b.sort);
this.policyService
.policyAppliesToActiveUser$(PolicyType.TwoFactorAuthentication)
.pipe(takeUntil(this.destroy$))
.subscribe((policyAppliesToActiveUser) => {
this.twoFactorAuthPolicyAppliesToActiveUser = policyAppliesToActiveUser;
});
await this.load();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async load() {
this.loading = true;
const providerList = await this.getTwoFactorProviders();
@@ -203,9 +219,7 @@ export class TwoFactorSetupComponent implements OnInit {
private async evaluatePolicies() {
if (this.organizationId == null && this.providers.filter((p) => p.enabled).length === 1) {
this.showPolicyWarning = await this.policyService.policyAppliesToUser(
PolicyType.TwoFactorAuthentication
);
this.showPolicyWarning = this.twoFactorAuthPolicyAppliesToActiveUser;
} else {
this.showPolicyWarning = false;
}

View File

@@ -13,6 +13,7 @@ import {
CalloutModule,
FormFieldModule,
IconModule,
AsyncActionsModule,
MenuModule,
TableModule,
TabsModule,
@@ -51,6 +52,7 @@ import "./locales";
],
exports: [
CommonModule,
AsyncActionsModule,
DragDropModule,
FormsModule,
InfiniteScrollModule,

View File

@@ -1,7 +1,7 @@
<div class="page-header">
<h1>{{ "importData" | i18n }}</h1>
</div>
<app-callout type="info" *ngIf="importBlockedByPolicy">
<app-callout type="info" *ngIf="importBlockedByPolicy$ | async">
{{ "personalOwnershipPolicyInEffectImports" | i18n }}
</app-callout>
<form #form (ngSubmit)="submit()" ngNativeValidate>
@@ -14,7 +14,7 @@
name="Format"
[(ngModel)]="format"
class="form-control"
[disabled]="importBlockedByPolicy"
[disabled]="importBlockedByPolicy$ | async"
required
>
<option *ngFor="let o of featuredImportOptions" [ngValue]="o.id">{{ o.name }}</option>
@@ -296,7 +296,7 @@
id="file"
class="form-control-file"
name="file"
[disabled]="importBlockedByPolicy"
[disabled]="importBlockedByPolicy$ | async"
/>
</div>
</div>
@@ -308,14 +308,14 @@
class="form-control"
name="FileContents"
[(ngModel)]="fileContents"
[disabled]="importBlockedByPolicy"
[disabled]="importBlockedByPolicy$ | async"
></textarea>
</div>
<button
type="submit"
class="btn btn-primary btn-submit"
[disabled]="loading || importBlockedByPolicy"
[ngClass]="{ manual: importBlockedByPolicy }"
[disabled]="loading || importBlockedByPolicy$ | async"
[ngClass]="{ manual: importBlockedByPolicy$ | async }"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "importData" | i18n }}</span>

View File

@@ -1,6 +1,7 @@
import { Component, OnInit } from "@angular/core";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import * as JSZip from "jszip";
import { firstValueFrom, Subject } from "rxjs";
import Swal, { SweetAlertIcon } from "sweetalert2";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@@ -19,18 +20,22 @@ import { FilePasswordPromptComponent } from "./file-password-prompt.component";
selector: "app-import",
templateUrl: "import.component.html",
})
export class ImportComponent implements OnInit {
export class ImportComponent implements OnInit, OnDestroy {
featuredImportOptions: ImportOption[];
importOptions: ImportOption[];
format: ImportType = null;
fileContents: string;
formPromise: Promise<ImportError>;
loading = false;
importBlockedByPolicy = false;
importBlockedByPolicy$ = this.policyService.policyAppliesToActiveUser$(
PolicyType.PersonalOwnership
);
protected organizationId: string = null;
protected successNavigate: any[] = ["vault"];
private destroy$ = new Subject<void>();
constructor(
protected i18nService: I18nService,
protected importService: ImportService,
@@ -43,14 +48,15 @@ export class ImportComponent implements OnInit {
async ngOnInit() {
this.setImportOptions();
}
this.importBlockedByPolicy = await this.policyService.policyAppliesToUser(
PolicyType.PersonalOwnership
);
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
async submit() {
if (this.importBlockedByPolicy) {
if (await firstValueFrom(this.importBlockedByPolicy$)) {
this.platformUtilsService.showToast(
"error",
null,

View File

@@ -1,4 +1,4 @@
import { Component } from "@angular/core";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/components/add-edit.component";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
@@ -24,7 +24,7 @@ import { LoginUriView } from "@bitwarden/common/models/view/loginUriView";
selector: "app-vault-add-edit",
templateUrl: "add-edit.component.html",
})
export class AddEditComponent extends BaseAddEditComponent {
export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnDestroy {
canAccessPremium: boolean;
totpCode: string;
totpCodeFormatted: string;
@@ -95,6 +95,10 @@ export class AddEditComponent extends BaseAddEditComponent {
}
}
ngOnDestroy() {
super.ngOnDestroy();
}
toggleFavorite() {
this.cipher.favorite = !this.cipher.favorite;
}
@@ -133,7 +137,7 @@ export class AddEditComponent extends BaseAddEditComponent {
async generatePassword(): Promise<boolean> {
const confirmed = await super.generatePassword();
if (confirmed) {
const options = (await this.passwordGenerationService.getOptions())[0];
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
this.cipher.login.password = await this.passwordGenerationService.generatePassword(options);
}
return confirmed;

View File

@@ -1,4 +1,5 @@
import { Component, Inject } from "@angular/core";
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { map, Subject, takeUntil } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -21,11 +22,13 @@ import { OrganizationFilter } from "../shared/models/vault-filter.type";
selector: "app-organization-options",
templateUrl: "organization-options.component.html",
})
export class OrganizationOptionsComponent {
export class OrganizationOptionsComponent implements OnInit, OnDestroy {
actionPromise: Promise<void | boolean>;
policies: Policy[];
loaded = false;
private destroy$ = new Subject<void>();
constructor(
@Inject(OptionsInput) private organization: OrganizationFilter,
private platformUtilsService: PlatformUtilsService,
@@ -39,12 +42,20 @@ export class OrganizationOptionsComponent {
) {}
async ngOnInit() {
await this.load();
this.policyService.policies$
.pipe(
map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)),
takeUntil(this.destroy$)
)
.subscribe((policies) => {
this.policies = policies;
this.loaded = true;
});
}
async load() {
this.policies = await this.policyService.getAll(PolicyType.ResetPassword);
this.loaded = true;
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
allowEnrollmentChanges(org: Organization): boolean {
@@ -84,7 +95,6 @@ export class OrganizationOptionsComponent {
});
await this.actionPromise;
this.platformUtilsService.showToast("success", null, "Unlinked SSO");
await this.load();
} catch (e) {
this.logService.error(e);
}
@@ -106,7 +116,6 @@ export class OrganizationOptionsComponent {
this.actionPromise = this.organizationApiService.leave(org.id);
await this.actionPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
await this.load();
} catch (e) {
this.logService.error(e);
}

View File

@@ -5454,6 +5454,18 @@
"numberOfUsers": {
"message": "Number of users"
},
"multiSelectPlaceholder": {
"message": "-- Type to Filter --"
},
"multiSelectLoading": {
"message": "Retrieving options..."
},
"multiSelectNotFound": {
"message": "No items found"
},
"multiSelectClearAll": {
"message": "Clear all"
},
"from": {
"message": "From"
},