mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
Merge branch 'main' into autofill/pm-8027-inline-menu-appears-within-input-fields-that-do-not-relate-to-user-login
This commit is contained in:
@@ -153,7 +153,7 @@ describe("AccountSwitcherService", () => {
|
||||
|
||||
await selectAccountPromise;
|
||||
|
||||
expect(accountService.switchAccount).toBeCalledWith(null);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("switchAccount", { userId: null });
|
||||
|
||||
expect(removeListenerSpy).toBeCalledTimes(1);
|
||||
});
|
||||
@@ -176,7 +176,7 @@ describe("AccountSwitcherService", () => {
|
||||
|
||||
await selectAccountPromise;
|
||||
|
||||
expect(accountService.switchAccount).toBeCalledWith("1");
|
||||
expect(messagingService.send).toHaveBeenCalledWith("switchAccount", { userId: "1" });
|
||||
expect(messagingService.send).toBeCalledWith(
|
||||
"switchAccount",
|
||||
matches((payload) => {
|
||||
|
||||
@@ -134,7 +134,6 @@ export class AccountSwitcherService {
|
||||
const switchAccountFinishedPromise = this.listenForSwitchAccountFinish(userId);
|
||||
|
||||
// Initiate the actions required to make account switching happen
|
||||
await this.accountService.switchAccount(userId);
|
||||
this.messagingService.send("switchAccount", { userId }); // This message should cause switchAccountFinish to be sent
|
||||
|
||||
// Wait until we receive the switchAccountFinished message
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mock, mockReset } from "jest-mock-extended";
|
||||
import { mock, MockProxy, mockReset } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
@@ -62,7 +62,8 @@ describe("OverlayBackground", () => {
|
||||
let overlayBackground: OverlayBackground;
|
||||
const cipherService = mock<CipherService>();
|
||||
const autofillService = mock<AutofillService>();
|
||||
const authService = mock<AuthService>();
|
||||
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
|
||||
const environmentService = mock<EnvironmentService>();
|
||||
environmentService.environment$ = new BehaviorSubject(
|
||||
@@ -94,6 +95,9 @@ describe("OverlayBackground", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
||||
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
|
||||
authService = mock<AuthService>();
|
||||
authService.activeAccountStatus$ = activeAccountStatusMock$;
|
||||
overlayBackground = new OverlayBackground(
|
||||
cipherService,
|
||||
autofillService,
|
||||
@@ -166,11 +170,11 @@ describe("OverlayBackground", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
});
|
||||
|
||||
it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => {
|
||||
overlayBackground["userAuthStatus"] = AuthenticationStatus.Locked;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
jest.spyOn(BrowserApi, "getTabFromCurrentWindowId");
|
||||
jest.spyOn(cipherService, "getAllDecryptedForUrl");
|
||||
|
||||
|
||||
@@ -136,7 +136,8 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
* list of ciphers if the extension is not unlocked.
|
||||
*/
|
||||
async updateOverlayCiphers() {
|
||||
if (this.userAuthStatus !== AuthenticationStatus.Unlocked) {
|
||||
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -167,7 +168,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
private async getOverlayCipherData(): Promise<OverlayCipherData[]> {
|
||||
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
||||
const overlayCiphersArray = Array.from(this.overlayLoginCiphers);
|
||||
const overlayCipherData = [];
|
||||
const overlayCipherData: OverlayCipherData[] = [];
|
||||
let loginCipherIcon: WebsiteIconData;
|
||||
|
||||
for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
||||
@@ -109,9 +110,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
// Autofill user settings loaded from state can await the active account state indefinitely
|
||||
// if not guarded by an active account check (e.g. the user is logged in)
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
|
||||
let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off;
|
||||
let autoFillOnPageLoadIsEnabled = false;
|
||||
const overlayVisibility = await this.getOverlayVisibility();
|
||||
|
||||
if (activeAccount) {
|
||||
overlayVisibility = await this.getOverlayVisibility();
|
||||
}
|
||||
|
||||
const mainAutofillScript = overlayVisibility
|
||||
? "bootstrap-autofill-overlay.js"
|
||||
|
||||
@@ -144,13 +144,11 @@ import { NotificationsService } from "@bitwarden/common/services/notifications.s
|
||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||
import {
|
||||
PasswordGenerationService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/password";
|
||||
import {
|
||||
UsernameGenerationService,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/username";
|
||||
legacyPasswordGenerationServiceFactory,
|
||||
legacyUsernameGenerationServiceFactory,
|
||||
} from "@bitwarden/common/tools/generator";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
@@ -649,10 +647,12 @@ export default class MainBackground {
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
|
||||
this.passwordGenerationService = new PasswordGenerationService(
|
||||
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||
this.encryptService,
|
||||
this.cryptoService,
|
||||
this.policyService,
|
||||
this.stateService,
|
||||
this.accountService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||
@@ -1092,10 +1092,14 @@ export default class MainBackground {
|
||||
this.vaultTimeoutSettingsService,
|
||||
);
|
||||
|
||||
this.usernameGenerationService = new UsernameGenerationService(
|
||||
this.cryptoService,
|
||||
this.stateService,
|
||||
this.usernameGenerationService = legacyUsernameGenerationServiceFactory(
|
||||
this.apiService,
|
||||
this.i18nService,
|
||||
this.cryptoService,
|
||||
this.encryptService,
|
||||
this.policyService,
|
||||
this.accountService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
if (!this.popupOnlyContext) {
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface OffscreenDocument {
|
||||
}
|
||||
|
||||
export abstract class OffscreenDocumentService {
|
||||
abstract offscreenApiSupported(): boolean;
|
||||
abstract withDocument<T>(
|
||||
reasons: chrome.offscreen.Reason[],
|
||||
justification: string,
|
||||
|
||||
@@ -49,6 +49,12 @@ describe.each([
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("offscreenApiSupported", () => {
|
||||
it("indicates whether the offscreen API is supported", () => {
|
||||
expect(sut.offscreenApiSupported()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withDocument", () => {
|
||||
it("creates a document when none exists", async () => {
|
||||
await sut.withDocument(reasons, justification, () => {});
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
export class DefaultOffscreenDocumentService implements DefaultOffscreenDocumentService {
|
||||
import { OffscreenDocumentService } from "./abstractions/offscreen-document";
|
||||
|
||||
export class DefaultOffscreenDocumentService implements OffscreenDocumentService {
|
||||
private workerCount = 0;
|
||||
|
||||
constructor(private logService: LogService) {}
|
||||
|
||||
offscreenApiSupported(): boolean {
|
||||
return typeof chrome.offscreen !== "undefined";
|
||||
}
|
||||
|
||||
async withDocument<T>(
|
||||
reasons: chrome.offscreen.Reason[],
|
||||
justification: string,
|
||||
|
||||
@@ -229,9 +229,7 @@ describe("Browser Utils Service", () => {
|
||||
|
||||
it("copies the passed text using the offscreen document if the extension is using manifest v3", async () => {
|
||||
const text = "test";
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
offscreenDocumentService.offscreenApiSupported.mockReturnValue(true);
|
||||
getManifestVersionSpy.mockReturnValue(3);
|
||||
|
||||
browserPlatformUtilsService.copyToClipboard(text);
|
||||
@@ -304,9 +302,7 @@ describe("Browser Utils Service", () => {
|
||||
});
|
||||
|
||||
it("reads the clipboard text using the offscreen document", async () => {
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
offscreenDocumentService.offscreenApiSupported.mockReturnValue(true);
|
||||
getManifestVersionSpy.mockReturnValue(3);
|
||||
offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) =>
|
||||
Promise.resolve("test"),
|
||||
|
||||
@@ -243,7 +243,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
||||
text = "\u0000";
|
||||
}
|
||||
|
||||
if (this.isChrome() && BrowserApi.isManifestVersion(3)) {
|
||||
if (BrowserApi.isManifestVersion(3) && this.offscreenDocumentService.offscreenApiSupported()) {
|
||||
void this.triggerOffscreenCopyToClipboard(text).then(handleClipboardWriteCallback);
|
||||
|
||||
return;
|
||||
@@ -268,7 +268,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
||||
return await SafariApp.sendMessageToApp("readFromClipboard");
|
||||
}
|
||||
|
||||
if (this.isChrome() && BrowserApi.isManifestVersion(3)) {
|
||||
if (BrowserApi.isManifestVersion(3) && this.offscreenDocumentService.offscreenApiSupported()) {
|
||||
return await this.triggerOffscreenReadFromClipboard();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Location } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -29,22 +28,22 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
stateService: StateService,
|
||||
accountService: AccountService,
|
||||
cipherService: CipherService,
|
||||
route: ActivatedRoute,
|
||||
logService: LogService,
|
||||
accountService: AccountService,
|
||||
ngZone: NgZone,
|
||||
private location: Location,
|
||||
) {
|
||||
super(
|
||||
passwordGenerationService,
|
||||
usernameGenerationService,
|
||||
platformUtilsService,
|
||||
stateService,
|
||||
accountService,
|
||||
i18nService,
|
||||
logService,
|
||||
route,
|
||||
accountService,
|
||||
ngZone,
|
||||
window,
|
||||
);
|
||||
this.cipherService = cipherService;
|
||||
|
||||
@@ -29,11 +29,6 @@
|
||||
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<app-user-verification ngDefaultControl formControlName="secret" name="Secret">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div id="confirmIdentityHelp" class="box-footer">
|
||||
<p>{{ "confirmIdentity" | i18n }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Router } from "@angular/router";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -27,7 +26,6 @@ export class ExportComponent extends BaseExportComponent {
|
||||
policyService: PolicyService,
|
||||
private router: Router,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
formBuilder: UntypedFormBuilder,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
@@ -40,7 +38,6 @@ export class ExportComponent extends BaseExportComponent {
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
formBuilder,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
|
||||
@@ -103,10 +103,8 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa
|
||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||
import {
|
||||
PasswordGenerationService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/password";
|
||||
import { legacyPasswordGenerationServiceFactory } from "@bitwarden/common/tools/generator";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
@@ -499,10 +497,12 @@ export class ServiceContainer {
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
|
||||
this.passwordGenerationService = new PasswordGenerationService(
|
||||
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||
this.encryptService,
|
||||
this.cryptoService,
|
||||
this.policyService,
|
||||
this.stateService,
|
||||
this.accountService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||
|
||||
@@ -411,7 +411,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.masterPasswordService.forceSetPasswordReason$(message.userId),
|
||||
)) != ForceSetPasswordReason.None;
|
||||
if (locked) {
|
||||
this.messagingService.send("locked", { userId: message.userId });
|
||||
this.modalService.closeAll();
|
||||
await this.router.navigate(["lock"]);
|
||||
} else if (forcedPasswordReset) {
|
||||
// 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
|
||||
|
||||
@@ -21,11 +21,6 @@
|
||||
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<app-user-verification ngDefaultControl formControlName="secret" name="secret">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div id="confirmIdentityHelp" class="box-footer">
|
||||
<p>{{ "confirmIdentity" | i18n }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { UntypedFormBuilder } from "@angular/forms";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -24,7 +23,6 @@ export class ExportComponent extends BaseExportComponent implements OnInit {
|
||||
exportService: VaultExportServiceAbstraction,
|
||||
eventCollectionService: EventCollectionService,
|
||||
policyService: PolicyService,
|
||||
userVerificationService: UserVerificationService,
|
||||
formBuilder: UntypedFormBuilder,
|
||||
logService: LogService,
|
||||
fileDownloadService: FileDownloadService,
|
||||
@@ -38,7 +36,6 @@ export class ExportComponent extends BaseExportComponent implements OnInit {
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
formBuilder,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -36,10 +35,6 @@ describe("GeneratorComponent", () => {
|
||||
provide: UsernameGenerationServiceAbstraction,
|
||||
useValue: mock<UsernameGenerationServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: StateService,
|
||||
useValue: mock<StateService>(),
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: platformUtilsServiceMock,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
||||
@@ -6,7 +6,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
|
||||
@@ -18,22 +17,22 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
||||
constructor(
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||
stateService: StateService,
|
||||
accountService: AccountService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
ngZone: NgZone,
|
||||
logService: LogService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
passwordGenerationService,
|
||||
usernameGenerationService,
|
||||
platformUtilsService,
|
||||
stateService,
|
||||
accountService,
|
||||
i18nService,
|
||||
logService,
|
||||
route,
|
||||
accountService,
|
||||
ngZone,
|
||||
window,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,115 +1,99 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="bulkTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="bulkTitle">
|
||||
{{ "confirmUsers" | i18n }}
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
<bit-dialog dialogSize="large" [title]="'confirmUsers' | i18n" [loading]="loading">
|
||||
<ng-container bitDialogContent>
|
||||
<app-callout type="danger" *ngIf="filteredUsers.length <= 0">
|
||||
{{ "noSelectedUsersApplicable" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!loading && !done">
|
||||
<p bitTypography="body1">
|
||||
{{ "fingerprintEnsureIntegrityVerify" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/fingerprint-phrase/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="card-body text-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</div>
|
||||
<app-callout type="danger" *ngIf="filteredUsers.length <= 0">
|
||||
{{ "noSelectedUsersApplicable" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!loading && !done">
|
||||
<p>
|
||||
{{ "fingerprintEnsureIntegrityVerify" | i18n }}
|
||||
<a
|
||||
href="https://bitwarden.com/help/fingerprint-phrase/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ "learnMore" | i18n }}</a
|
||||
>
|
||||
</p>
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th>{{ "fingerprint" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of filteredUsers">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ fingerprints.get(user.id) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngFor="let user of excludedUsers">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading && done">
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of filteredUsers">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="statuses.has(user.id)">
|
||||
{{ statuses.get(user.id) }}
|
||||
</td>
|
||||
<td *ngIf="!statuses.has(user.id)">
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
*ngIf="!done"
|
||||
[disabled]="loading"
|
||||
(click)="submit()"
|
||||
{{ "learnMore" | i18n }}</a
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "confirm" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell colspan="2">{{ "user" | i18n }}</th>
|
||||
<th bitCell>{{ "fingerprint" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of filteredUsers" alignContent="middle">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
<p class="tw-text-muted tw-text-sm" *ngIf="user.name">{{ user.name }}</p>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ fingerprints.get(user.id) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngFor="let user of excludedUsers" alignContent="middle">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
<p class="tw-text-muted tw-text-sm" *ngIf="user.name">{{ user.name }}</p>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading && done">
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell colspan="2">{{ "user" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of filteredUsers" alignContent="middle">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
<p class="tw-text-muted tw-text-sm" *ngIf="user.name">{{ user.name }}</p>
|
||||
</td>
|
||||
<td bitCell *ngIf="statuses.has(user.id)">
|
||||
{{ statuses.get(user.id) }}
|
||||
</td>
|
||||
<td bitCell *ngIf="!statuses.has(user.id)">
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
*ngIf="!done"
|
||||
bitButton
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
(click)="submit()"
|
||||
[disabled]="loading"
|
||||
>
|
||||
{{ "confirm" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
@@ -8,16 +9,22 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
type BulkConfirmDialogData = {
|
||||
organizationId: string;
|
||||
users: BulkUserDetails[];
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-bulk-confirm",
|
||||
templateUrl: "bulk-confirm.component.html",
|
||||
})
|
||||
export class BulkConfirmComponent implements OnInit {
|
||||
@Input() organizationId: string;
|
||||
@Input() users: BulkUserDetails[];
|
||||
organizationId: string;
|
||||
users: BulkUserDetails[];
|
||||
|
||||
excludedUsers: BulkUserDetails[];
|
||||
filteredUsers: BulkUserDetails[];
|
||||
@@ -30,11 +37,15 @@ export class BulkConfirmComponent implements OnInit {
|
||||
error: string;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: BulkConfirmDialogData,
|
||||
protected cryptoService: CryptoService,
|
||||
protected apiService: ApiService,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
) {
|
||||
this.organizationId = data.organizationId;
|
||||
this.users = data.users;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.excludedUsers = this.users.filter((u) => !this.isAccepted(u));
|
||||
@@ -110,4 +121,8 @@ export class BulkConfirmComponent implements OnInit {
|
||||
request,
|
||||
);
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<BulkConfirmDialogData>) {
|
||||
return dialogService.open(BulkConfirmComponent, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,88 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="bulkTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="bulkTitle">
|
||||
{{ "removeUsers" | i18n }}
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-callout type="danger" *ngIf="users.length <= 0">
|
||||
{{ "noSelectedUsersApplicable" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!done">
|
||||
<app-callout type="warning" *ngIf="users.length > 0 && !error">
|
||||
<p>{{ removeUsersWarning }}</p>
|
||||
<p *ngIf="this.showNoMasterPasswordWarning">
|
||||
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
|
||||
</p>
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of users">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="this.showNoMasterPasswordWarning">
|
||||
<span class="text-muted d-block tw-lowercase">
|
||||
<ng-container *ngIf="user.hasMasterPassword === true"> - </ng-container>
|
||||
<ng-container *ngIf="user.hasMasterPassword === false">
|
||||
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{ "noMasterPassword" | i18n }}
|
||||
</ng-container>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<bit-dialog dialogSize="large" [title]="'removeUsers' | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<app-callout type="danger" *ngIf="users.length <= 0">
|
||||
{{ "noSelectedUsersApplicable" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!done">
|
||||
<app-callout type="warning" *ngIf="users.length > 0 && !error">
|
||||
<p bitTypography="body1">{{ removeUsersWarning }}</p>
|
||||
<p *ngIf="this.showNoMasterPasswordWarning" bitTypography="body1">
|
||||
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
|
||||
</p>
|
||||
</app-callout>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell colspan="2">{{ "user" | i18n }}</th>
|
||||
<th bitCell *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="done">
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of users">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="statuses.has(user.id)">
|
||||
{{ statuses.get(user.id) }}
|
||||
</td>
|
||||
<td *ngIf="!statuses.has(user.id)">
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of users">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
<small class="tw-text-muted tw-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td bitCell *ngIf="this.showNoMasterPasswordWarning">
|
||||
<span class="tw-text-muted tw-block tw-lowercase">
|
||||
<ng-container *ngIf="user.hasMasterPassword === true"> - </ng-container>
|
||||
<ng-container *ngIf="user.hasMasterPassword === false">
|
||||
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{ "noMasterPassword" | i18n }}
|
||||
</ng-container>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="done">
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell colspan="2">{{ "user" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
*ngIf="!done && users.length > 0"
|
||||
[disabled]="loading"
|
||||
(click)="submit()"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "removeUsers" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of users">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
<small class="tw-text-muted tw-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="statuses.has(user.id)" bitCell>
|
||||
{{ statuses.get(user.id) }}
|
||||
</td>
|
||||
<td *ngIf="!statuses.has(user.id)" bitCell>
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
*ngIf="!done && users.length > 0"
|
||||
bitButton
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
[disabled]="loading"
|
||||
[bitAction]="submit"
|
||||
>
|
||||
{{ "removeUsers" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
type BulkRemoveDialogData = {
|
||||
organizationId: string;
|
||||
users: BulkUserDetails[];
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-bulk-remove",
|
||||
templateUrl: "bulk-remove.component.html",
|
||||
})
|
||||
export class BulkRemoveComponent {
|
||||
@Input() organizationId: string;
|
||||
@Input() set users(value: BulkUserDetails[]) {
|
||||
this._users = value;
|
||||
this.showNoMasterPasswordWarning = this._users.some(
|
||||
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
|
||||
);
|
||||
}
|
||||
|
||||
get users(): BulkUserDetails[] {
|
||||
return this._users;
|
||||
}
|
||||
|
||||
private _users: BulkUserDetails[];
|
||||
organizationId: string;
|
||||
users: BulkUserDetails[];
|
||||
|
||||
statuses: Map<string, string> = new Map();
|
||||
|
||||
@@ -34,12 +30,19 @@ export class BulkRemoveComponent {
|
||||
showNoMasterPasswordWarning = false;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: BulkRemoveDialogData,
|
||||
protected apiService: ApiService,
|
||||
protected i18nService: I18nService,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
) {}
|
||||
) {
|
||||
this.organizationId = data.organizationId;
|
||||
this.users = data.users;
|
||||
this.showNoMasterPasswordWarning = this.users.some(
|
||||
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
|
||||
);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
submit = async () => {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await this.deleteUsers();
|
||||
@@ -54,7 +57,7 @@ export class BulkRemoveComponent {
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
protected async deleteUsers() {
|
||||
return await this.organizationUserService.deleteManyOrganizationUsers(
|
||||
@@ -66,4 +69,8 @@ export class BulkRemoveComponent {
|
||||
protected get removeUsersWarning() {
|
||||
return this.i18nService.t("removeOrgUsersConfirmation");
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<BulkRemoveDialogData>) {
|
||||
return dialogService.open(BulkRemoveComponent, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<bit-tab-group
|
||||
*ngIf="!loading && organization$ | async as organization"
|
||||
@@ -22,7 +22,7 @@
|
||||
>
|
||||
<bit-tab [label]="'role' | i18n">
|
||||
<ng-container *ngIf="!editMode">
|
||||
<p>{{ "inviteUserDesc" | i18n }}</p>
|
||||
<p bitTypography="body1">{{ "inviteUserDesc" | i18n }}</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||
<input id="emails" type="text" appAutoFocus bitInput formControlName="emails" />
|
||||
@@ -32,13 +32,11 @@
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
<fieldset role="radiogroup" aria-labelledby="roleGroupLabel" class="tw-mb-6">
|
||||
<legend
|
||||
id="roleGroupLabel"
|
||||
class="tw-mb-2 tw-block tw-text-base tw-font-semibold tw-text-main"
|
||||
>
|
||||
<bit-radio-group formControlName="type">
|
||||
<bit-label>
|
||||
{{ "memberRole" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
@@ -46,112 +44,63 @@
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</legend>
|
||||
<div class="tw-mb-2 tw-flex tw-items-baseline">
|
||||
<input
|
||||
type="radio"
|
||||
id="userTypeUser"
|
||||
[value]="organizationUserType.User"
|
||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
||||
formControlName="type"
|
||||
name="type"
|
||||
/>
|
||||
<label class="tw-m-0" for="userTypeUser">
|
||||
{{ "user" | i18n }}
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
{{ "userDesc" | i18n }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
</bit-label>
|
||||
<bit-radio-button id="userTypeUser" [value]="organizationUserType.User">
|
||||
<bit-label>{{ "user" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "userDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button
|
||||
*ngIf="!organization.flexibleCollections"
|
||||
class="tw-mb-2 tw-flex tw-items-baseline"
|
||||
id="userTypeManager"
|
||||
[value]="organizationUserType.Manager"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="userTypeManager"
|
||||
[value]="organizationUserType.Manager"
|
||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
||||
formControlName="type"
|
||||
name="type"
|
||||
/>
|
||||
<label class="tw-m-0" for="userTypeManager">
|
||||
{{ "manager" | i18n }}
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
{{ "managerDesc" | i18n }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="tw-mb-2 tw-flex tw-items-baseline">
|
||||
<input
|
||||
type="radio"
|
||||
id="userTypeAdmin"
|
||||
[value]="organizationUserType.Admin"
|
||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
||||
formControlName="type"
|
||||
name="type"
|
||||
/>
|
||||
<label class="tw-m-0" for="userTypeAdmin">
|
||||
{{ "admin" | i18n }}
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
{{ "adminDesc" | i18n }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="tw-mb-2 tw-flex tw-items-baseline">
|
||||
<input
|
||||
type="radio"
|
||||
id="userTypeOwner"
|
||||
[value]="organizationUserType.Owner"
|
||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
||||
formControlName="type"
|
||||
name="type"
|
||||
/>
|
||||
<label class="tw-m-0" for="userTypeOwner">
|
||||
{{ "owner" | i18n }}
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
{{ "ownerDesc" | i18n }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="tw-flex tw-items-baseline">
|
||||
<input
|
||||
type="radio"
|
||||
id="userTypeCustom"
|
||||
[value]="organizationUserType.Custom"
|
||||
formControlName="type"
|
||||
name="type"
|
||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
||||
[attr.disabled]="!organization.useCustomPermissions || null"
|
||||
/>
|
||||
<label class="tw-m-0" for="userTypeCustom">
|
||||
{{ "custom" | i18n }}
|
||||
<bit-label>{{ "manager" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "managerDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="userTypeAdmin" [value]="organizationUserType.Admin">
|
||||
<bit-label>{{ "admin" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "adminDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="userTypeOwner" [value]="organizationUserType.Owner">
|
||||
<bit-label>{{ "owner" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "ownerDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button
|
||||
id="userTypeCustom"
|
||||
[value]="organizationUserType.Custom"
|
||||
[disabled]="!organization.useCustomPermissions || null"
|
||||
>
|
||||
<bit-label>{{ "custom" | i18n }}</bit-label>
|
||||
<bit-hint>
|
||||
<ng-container *ngIf="!organization.useCustomPermissions; else enterprise">
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
<p>
|
||||
{{ "customDescNonEnterpriseStart" | i18n
|
||||
}}<a href="https://bitwarden.com/contact/" target="_blank" rel="noreferrer">{{
|
||||
"customDescNonEnterpriseLink" | i18n
|
||||
}}</a
|
||||
}}<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/contact/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>{{ "customDescNonEnterpriseLink" | i18n }}</a
|
||||
>{{ "customDescNonEnterpriseEnd" | i18n }}
|
||||
</div>
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-template #enterprise>
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
{{ "customDesc" | i18n }}
|
||||
</div>
|
||||
<p>{{ "customDesc" | i18n }}</p>
|
||||
</ng-template>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
<ng-container *ngIf="customUserTypeSelected">
|
||||
<ng-container *ngIf="!organization.flexibleCollections; else customPermissionsFC">
|
||||
<h3 class="mt-4 d-flex tw-font-semibold">
|
||||
<h3 bitTypography="h3">
|
||||
{{ "permissions" | i18n }}
|
||||
</h3>
|
||||
<div class="row" [formGroup]="permissionsGroup">
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<label class="tw-font-semibold">{{ "managerPermissions" | i18n }}</label>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
|
||||
<div class="tw-col-span-6">
|
||||
<div class="tw-mb-3">
|
||||
<bit-label class="tw-font-semibold">{{
|
||||
"managerPermissions" | i18n
|
||||
}}</bit-label>
|
||||
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
||||
<app-nested-checkbox
|
||||
parentId="manageAssignedCollections"
|
||||
@@ -160,221 +109,126 @@
|
||||
</app-nested-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</label>
|
||||
<div class="tw-col-span-6">
|
||||
<div class="tw-mb-3">
|
||||
<bit-label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</bit-label>
|
||||
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessEventLogs"
|
||||
id="accessEventLogs"
|
||||
formControlName="accessEventLogs"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessEventLogs">
|
||||
{{ "accessEventLogs" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessImportExport"
|
||||
id="accessImportExport"
|
||||
formControlName="accessImportExport"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessImportExport">
|
||||
{{ "accessImportExport" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessReports"
|
||||
id="accessReports"
|
||||
formControlName="accessReports"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessReports">
|
||||
{{ "accessReports" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessEventLogs" />
|
||||
<bit-label>{{ "accessEventLogs" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessImportExport" />
|
||||
<bit-label>{{ "accessImportExport" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessReports" />
|
||||
<bit-label>{{ "accessReports" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<app-nested-checkbox
|
||||
parentId="manageAllCollections"
|
||||
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
||||
>
|
||||
</app-nested-checkbox>
|
||||
<div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageGroups" />
|
||||
<bit-label>{{ "manageGroups" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageSso" />
|
||||
<bit-label>{{ "manageSso" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="managePolicies" />
|
||||
<bit-label>{{ "managePolicies" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageGroups"
|
||||
id="manageGroups"
|
||||
formControlName="manageGroups"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageGroups">
|
||||
{{ "manageGroups" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageSso"
|
||||
id="manageSso"
|
||||
formControlName="manageSso"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageSso">
|
||||
{{ "manageSso" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="managePolicies"
|
||||
id="managePolicies"
|
||||
formControlName="managePolicies"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="managePolicies">
|
||||
{{ "managePolicies" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageUsers"
|
||||
id="manageUsers"
|
||||
bitCheckbox
|
||||
formControlName="manageUsers"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageUsers">
|
||||
{{ "manageUsers" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageResetPassword"
|
||||
id="manageResetPassword"
|
||||
bitCheckbox
|
||||
formControlName="manageResetPassword"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageResetPassword">
|
||||
{{ "manageAccountRecovery" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #customPermissionsFC>
|
||||
<div class="row" [formGroup]="permissionsGroup">
|
||||
<div class="col-4">
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessEventLogs"
|
||||
id="accessEventLogs"
|
||||
formControlName="accessEventLogs"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessEventLogs">
|
||||
{{ "accessEventLogs" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessImportExport"
|
||||
id="accessImportExport"
|
||||
formControlName="accessImportExport"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessImportExport">
|
||||
{{ "accessImportExport" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessReports"
|
||||
id="accessReports"
|
||||
formControlName="accessReports"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessReports">
|
||||
{{ "accessReports" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
|
||||
<div class="tw-col-span-4">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessEventLogs" />
|
||||
<bit-label>{{ "accessEventLogs" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessImportExport" />
|
||||
<bit-label>{{ "accessImportExport" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessReports" />
|
||||
<bit-label>{{ "accessReports" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="tw-col-span-4">
|
||||
<app-nested-checkbox
|
||||
parentId="manageAllCollections"
|
||||
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
||||
>
|
||||
</app-nested-checkbox>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
<div class="tw-col-span-4">
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageGroups" />
|
||||
<bit-label>{{ "manageGroups" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageSso" />
|
||||
<bit-label>{{ "manageSso" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="managePolicies" />
|
||||
<bit-label>{{ "managePolicies" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageGroups"
|
||||
id="manageGroups"
|
||||
formControlName="manageGroups"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageGroups">
|
||||
{{ "manageGroups" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageSso"
|
||||
id="manageSso"
|
||||
formControlName="manageSso"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageSso">
|
||||
{{ "manageSso" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="managePolicies"
|
||||
id="managePolicies"
|
||||
formControlName="managePolicies"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="managePolicies">
|
||||
{{ "managePolicies" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageUsers"
|
||||
id="manageUsers"
|
||||
bitCheckbox
|
||||
formControlName="manageUsers"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageUsers">
|
||||
{{ "manageUsers" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageResetPassword"
|
||||
id="manageResetPassword"
|
||||
bitCheckbox
|
||||
formControlName="manageResetPassword"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageResetPassword">
|
||||
{{ "manageAccountRecovery" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="organization.useSecretsManager">
|
||||
<h3 class="mt-4">
|
||||
<h3 class="tw-mt-4">
|
||||
{{ "secretsManager" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
@@ -436,6 +290,7 @@
|
||||
<bit-label>
|
||||
{{ "accessAllCollectionsDesc" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
<div [formGroup]="checkboxes">
|
||||
<input
|
||||
type="checkbox"
|
||||
[name]="pascalize(parentId)"
|
||||
[id]="parentId"
|
||||
[formControlName]="parentId"
|
||||
[indeterminate]="parentIndeterminate"
|
||||
/>
|
||||
<label class="!tw-font-normal" [for]="parentId">
|
||||
{{ parentId | i18n }}
|
||||
</label>
|
||||
<div class="tw-ml-6">
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
[formControlName]="parentId"
|
||||
[indeterminate]="parentIndeterminate"
|
||||
/>
|
||||
<bit-label>{{ parentId | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<div class="tw-ml-4">
|
||||
<ng-container *ngFor="let c of checkboxes.controls | keyvalue; trackBy: key">
|
||||
<div class="" *ngIf="c.key != parentId">
|
||||
<input
|
||||
class=""
|
||||
type="checkbox"
|
||||
[name]="pascalize(c.key)"
|
||||
[id]="c.key"
|
||||
[formControl]="c.value"
|
||||
(change)="onChildCheck()"
|
||||
/>
|
||||
<label class="!tw-font-normal" [for]="c.key">
|
||||
{{ c.key | i18n }}
|
||||
</label>
|
||||
<div *ngIf="c.key != parentId">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="c.value" (change)="onChildCheck()" />
|
||||
<bit-label>{{ c.key | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -481,16 +481,13 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
||||
return;
|
||||
}
|
||||
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
BulkRemoveComponent,
|
||||
this.bulkRemoveModalRef,
|
||||
(comp) => {
|
||||
comp.organizationId = this.organization.id;
|
||||
comp.users = this.getCheckedUsers();
|
||||
const dialogRef = BulkRemoveComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organization.id,
|
||||
users: this.getCheckedUsers(),
|
||||
},
|
||||
);
|
||||
|
||||
await modal.onClosedPromise();
|
||||
});
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
await this.load();
|
||||
}
|
||||
|
||||
@@ -558,16 +555,14 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
||||
return;
|
||||
}
|
||||
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
BulkConfirmComponent,
|
||||
this.bulkConfirmModalRef,
|
||||
(comp) => {
|
||||
comp.organizationId = this.organization.id;
|
||||
comp.users = this.getCheckedUsers();
|
||||
const dialogRef = BulkConfirmComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organization.id,
|
||||
users: this.getCheckedUsers(),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await modal.onClosedPromise();
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
await this.load();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,7 @@
|
||||
{{ "disableSendExemption" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
@@ -1,144 +1,64 @@
|
||||
<div [formGroup]="data">
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6 tw-mb-0">
|
||||
<bit-label>{{ "defaultType" | i18n }}</bit-label>
|
||||
<bit-select formControlName="defaultType" id="defaultType">
|
||||
<bit-option *ngFor="let o of defaultTypes" [value]="o.value" [label]="o.name"></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 form-group mb-0">
|
||||
<label for="defaultType">{{ "defaultType" | i18n }}</label>
|
||||
<select
|
||||
id="defaultType"
|
||||
name="defaultType"
|
||||
formControlName="defaultType"
|
||||
class="form-control"
|
||||
>
|
||||
<option *ngFor="let o of defaultTypes" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<h3 bitTypography="h3" class="tw-mt-4">{{ "password" | i18n }}</h3>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minLength" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="5" max="128" formControlName="minLength" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<h3 class="mt-4">{{ "password" | i18n }}</h3>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="minLength">{{ "minLength" | i18n }}</label>
|
||||
<input
|
||||
id="minLength"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="minLength"
|
||||
min="5"
|
||||
max="128"
|
||||
formControlName="minLength"
|
||||
/>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minNumbers" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="0" max="9" formControlName="minNumbers" />
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minSpecial" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="0" max="9" formControlName="minSpecial" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="minNumbers">{{ "minNumbers" | i18n }}</label>
|
||||
<input
|
||||
id="minNumbers"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="minNumbers"
|
||||
min="0"
|
||||
max="9"
|
||||
formControlName="minNumbers"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="minSpecial">{{ "minSpecial" | i18n }}</label>
|
||||
<input
|
||||
id="minSpecial"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="minSpecial"
|
||||
min="0"
|
||||
max="9"
|
||||
formControlName="minSpecial"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="useUpper"
|
||||
formControlName="useUpper"
|
||||
name="useUpper"
|
||||
/>
|
||||
<label class="form-check-label" for="useUpper">A-Z</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="useLower"
|
||||
name="useLower"
|
||||
formControlName="useLower"
|
||||
/>
|
||||
<label class="form-check-label" for="useLower">a-z</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="useNumbers"
|
||||
name="useNumbers"
|
||||
formControlName="useNumbers"
|
||||
/>
|
||||
<label class="form-check-label" for="useNumbers">0-9</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="useSpecial"
|
||||
name="useSpecial"
|
||||
formControlName="useSpecial"
|
||||
/>
|
||||
<label class="form-check-label" for="useSpecial">!@#$%^&*</label>
|
||||
</div>
|
||||
<h3 class="mt-4">{{ "passphrase" | i18n }}</h3>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="minNumberWords">{{ "minimumNumberOfWords" | i18n }}</label>
|
||||
<input
|
||||
id="minNumberWords"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="minNumberWords"
|
||||
min="3"
|
||||
max="20"
|
||||
formControlName="minNumberWords"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="capitalize"
|
||||
name="capitalize"
|
||||
formControlName="capitalize"
|
||||
/>
|
||||
<label class="form-check-label" for="capitalize">{{ "capitalize" | i18n }}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="includeNumber"
|
||||
name="includeNumber"
|
||||
formControlName="includeNumber"
|
||||
/>
|
||||
<label class="form-check-label" for="includeNumber">{{ "includeNumber" | i18n }}</label>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useUpper" id="useUpper" />
|
||||
<bit-label>A-Z</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useLower" id="useLower" />
|
||||
<bit-label>a-z</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useNumbers" id="useNumbers" />
|
||||
<bit-label>0-9</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useSpecial" id="useSpecial" />
|
||||
<bit-label>!@#$%^&*</bit-label>
|
||||
</bit-form-control>
|
||||
<h3 bitTypography="h3" class="tw-mt-4">{{ "passphrase" | i18n }}</h3>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minimumNumberOfWords" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="3" max="20" formControlName="minNumberWords" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="capitalize" id="capitalize" />
|
||||
<bit-label>{{ "capitalize" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="includeNumber" id="includeNumber" />
|
||||
<bit-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { UntypedFormBuilder } from "@angular/forms";
|
||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -20,14 +20,14 @@ export class PasswordGeneratorPolicy extends BasePolicy {
|
||||
export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
|
||||
data = this.formBuilder.group({
|
||||
defaultType: [null],
|
||||
minLength: [null],
|
||||
minLength: [null, [Validators.min(5), Validators.max(128)]],
|
||||
useUpper: [null],
|
||||
useLower: [null],
|
||||
useNumbers: [null],
|
||||
useSpecial: [null],
|
||||
minNumbers: [null],
|
||||
minSpecial: [null],
|
||||
minNumberWords: [null],
|
||||
minNumbers: [null, [Validators.min(0), Validators.max(9)]],
|
||||
minSpecial: [null, [Validators.min(0), Validators.max(9)]],
|
||||
minNumberWords: [null, [Validators.min(3), Validators.max(20)]],
|
||||
capitalize: [null],
|
||||
includeNumber: [null],
|
||||
});
|
||||
|
||||
@@ -5,15 +5,7 @@
|
||||
{{ "requireSsoExemption" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
@@ -2,29 +2,15 @@
|
||||
{{ "sendOptionsExemption" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<div [formGroup]="data">
|
||||
<h3 class="mt-4">{{ "options" | i18n }}</h3>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="disableHideEmail"
|
||||
name="DisableHideEmail"
|
||||
formControlName="disableHideEmail"
|
||||
/>
|
||||
<label class="form-check-label" for="disableHideEmail">{{ "disableHideEmail" | i18n }}</label>
|
||||
</div>
|
||||
<h3 bitTypography="h3" class="tw-mt-4">{{ "options" | i18n }}</h3>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="disableHideEmail" id="disableHideEmail" />
|
||||
<bit-label>{{ "disableHideEmail" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,7 @@
|
||||
{{ "singleOrgPolicyWarning" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
@@ -104,12 +104,11 @@
|
||||
<button type="button" bitButton buttonType="danger" (click)="deleteOrganization()">
|
||||
{{ "deleteOrganization" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="danger" (click)="purgeVault()">
|
||||
<button type="button" bitButton buttonType="danger" [bitAction]="purgeVault">
|
||||
{{ "purgeVault" | i18n }}
|
||||
</button>
|
||||
</app-danger-zone>
|
||||
|
||||
<ng-template #purgeOrganizationTemplate></ng-template>
|
||||
<ng-template #apiKeyTemplate></ng-template>
|
||||
<ng-template #rotateApiKeyTemplate></ng-template>
|
||||
</bit-container>
|
||||
|
||||
@@ -28,8 +28,6 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./
|
||||
templateUrl: "account.component.html",
|
||||
})
|
||||
export class AccountComponent {
|
||||
@ViewChild("purgeOrganizationTemplate", { read: ViewContainerRef, static: true })
|
||||
purgeModalRef: ViewContainerRef;
|
||||
@ViewChild("apiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
apiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
@@ -232,11 +230,14 @@ export class AccountComponent {
|
||||
}
|
||||
}
|
||||
|
||||
async purgeVault() {
|
||||
await this.modalService.openViewRef(PurgeVaultComponent, this.purgeModalRef, (comp) => {
|
||||
comp.organizationId = this.organizationId;
|
||||
purgeVault = async () => {
|
||||
const dialogRef = PurgeVaultComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
};
|
||||
|
||||
async viewApiKey() {
|
||||
await this.modalService.openViewRef(ApiKeyComponent, this.apiKeyModalRef, (comp) => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ActivatedRoute } from "@angular/router";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -30,7 +29,6 @@ export class OrganizationVaultExportComponent extends ExportComponent {
|
||||
private route: ActivatedRoute,
|
||||
policyService: PolicyService,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
formBuilder: UntypedFormBuilder,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
@@ -43,7 +41,6 @@ export class OrganizationVaultExportComponent extends ExportComponent {
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
formBuilder,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
|
||||
@@ -1,50 +1,36 @@
|
||||
<td>
|
||||
<td bitCell>
|
||||
{{ sponsoringOrg.familySponsorshipFriendlyName }}
|
||||
</td>
|
||||
<td>{{ sponsoringOrg.name }}</td>
|
||||
<td>
|
||||
<td bitCell>{{ sponsoringOrg.name }}</td>
|
||||
<td bitCell>
|
||||
<span [ngClass]="statusClass">{{ statusMessage }}</span>
|
||||
</td>
|
||||
<td class="table-action-right">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<td bitCell>
|
||||
<button
|
||||
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
buttonType="main"
|
||||
[bitMenuTriggerFor]="appListDropdown"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #appListDropdown>
|
||||
<button
|
||||
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
|
||||
class="btn btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownMenuButton"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
bitMenuItem
|
||||
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
|
||||
(click)="resendEmail()"
|
||||
[attr.aria-label]="'resendEmailLabel' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||
>
|
||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
||||
{{ "resendEmail" | i18n }}
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<button
|
||||
type="button"
|
||||
#resendEmailBtn
|
||||
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
|
||||
[appApiAction]="resendEmailPromise"
|
||||
class="dropdown-item btn-submit"
|
||||
[disabled]="$any(resendEmailBtn).loading"
|
||||
(click)="resendEmail()"
|
||||
[attr.aria-label]="'resendEmailLabel' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "resendEmail" | i18n }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
#revokeSponsorshipBtn
|
||||
[appApiAction]="revokeSponsorshipPromise"
|
||||
class="dropdown-item text-danger btn-submit"
|
||||
[disabled]="$any(revokeSponsorshipBtn).loading"
|
||||
(click)="revokeSponsorship()"
|
||||
[attr.aria-label]="'revokeAccount' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "remove" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="revokeSponsorship()"
|
||||
[attr.aria-label]="'revokeAccount' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||
>
|
||||
<span class="tw-text-danger">{{ "remove" | i18n }}</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
|
||||
@@ -20,10 +20,7 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
@Output() sponsorshipRemoved = new EventEmitter();
|
||||
|
||||
statusMessage = "loading";
|
||||
statusClass: "text-success" | "text-danger" = "text-success";
|
||||
|
||||
revokeSponsorshipPromise: Promise<any>;
|
||||
resendEmailPromise: Promise<any>;
|
||||
statusClass: "tw-text-success" | "tw-text-danger" = "tw-text-success";
|
||||
|
||||
private locale = "";
|
||||
|
||||
@@ -48,20 +45,15 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
|
||||
async revokeSponsorship() {
|
||||
try {
|
||||
this.revokeSponsorshipPromise = this.doRevokeSponsorship();
|
||||
await this.revokeSponsorshipPromise;
|
||||
await this.doRevokeSponsorship();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
this.revokeSponsorshipPromise = null;
|
||||
}
|
||||
|
||||
async resendEmail() {
|
||||
this.resendEmailPromise = this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
|
||||
await this.resendEmailPromise;
|
||||
await this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("emailSent"));
|
||||
this.resendEmailPromise = null;
|
||||
}
|
||||
|
||||
get isSentAwaitingSync() {
|
||||
@@ -106,31 +98,31 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
"revokeWhenExpired",
|
||||
formatDate(validUntil, "MM/dd/yyyy", this.locale),
|
||||
);
|
||||
this.statusClass = "text-danger";
|
||||
this.statusClass = "tw-text-danger";
|
||||
} else if (toDelete) {
|
||||
// They want to delete and we don't have a valid until date so we can
|
||||
// this should only happen on a self-hosted install
|
||||
this.statusMessage = this.i18nService.t("requestRemoved");
|
||||
this.statusClass = "text-danger";
|
||||
this.statusClass = "tw-text-danger";
|
||||
} else if (validUntil) {
|
||||
// They don't want to delete and they have a valid until date
|
||||
// that means they are actively sponsoring someone
|
||||
this.statusMessage = this.i18nService.t("active");
|
||||
this.statusClass = "text-success";
|
||||
this.statusClass = "tw-text-success";
|
||||
} else if (selfHosted && lastSyncDate) {
|
||||
// We are on a self-hosted install and it has been synced but we have not gotten
|
||||
// a valid until date so we can't know if they are actively sponsoring someone
|
||||
this.statusMessage = this.i18nService.t("sent");
|
||||
this.statusClass = "text-success";
|
||||
this.statusClass = "tw-text-success";
|
||||
} else if (!selfHosted) {
|
||||
// We are in cloud and all other status checks have been false therefore we have
|
||||
// sent the request but it hasn't been accepted yet
|
||||
this.statusMessage = this.i18nService.t("sent");
|
||||
this.statusClass = "text-success";
|
||||
this.statusClass = "tw-text-success";
|
||||
} else {
|
||||
// We are on a self-hosted install and we have not synced yet
|
||||
this.statusMessage = this.i18nService.t("requested");
|
||||
this.statusClass = "text-success";
|
||||
this.statusClass = "tw-text-success";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<button type="button" bitButton buttonType="danger" (click)="deauthorizeSessions()">
|
||||
{{ "deauthorizeSessions" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="danger" (click)="purgeVault()">
|
||||
<button type="button" bitButton buttonType="danger" [bitAction]="purgeVault">
|
||||
{{ "purgeVault" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="danger" (click)="deleteAccount()">
|
||||
@@ -21,7 +21,6 @@
|
||||
</app-danger-zone>
|
||||
|
||||
<ng-template #deauthorizeSessionsTemplate></ng-template>
|
||||
<ng-template #purgeVaultTemplate></ng-template>
|
||||
<ng-template #deleteAccountTemplate></ng-template>
|
||||
<ng-template #viewUserApiKeyTemplate></ng-template>
|
||||
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.component";
|
||||
|
||||
@@ -15,8 +17,6 @@ import { DeleteAccountComponent } from "./delete-account.component";
|
||||
export class AccountComponent {
|
||||
@ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true })
|
||||
deauthModalRef: ViewContainerRef;
|
||||
@ViewChild("purgeVaultTemplate", { read: ViewContainerRef, static: true })
|
||||
purgeModalRef: ViewContainerRef;
|
||||
@ViewChild("deleteAccountTemplate", { read: ViewContainerRef, static: true })
|
||||
deleteModalRef: ViewContainerRef;
|
||||
|
||||
@@ -24,6 +24,7 @@ export class AccountComponent {
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private dialogService: DialogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
) {}
|
||||
|
||||
@@ -35,9 +36,10 @@ export class AccountComponent {
|
||||
await this.modalService.openViewRef(DeauthorizeSessionsComponent, this.deauthModalRef);
|
||||
}
|
||||
|
||||
async purgeVault() {
|
||||
await this.modalService.openViewRef(PurgeVaultComponent, this.purgeModalRef);
|
||||
}
|
||||
purgeVault = async () => {
|
||||
const dialogRef = PurgeVaultComponent.open(this.dialogService);
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
};
|
||||
|
||||
async deleteAccount() {
|
||||
await this.modalService.openViewRef(DeleteAccountComponent, this.deleteModalRef);
|
||||
|
||||
@@ -1,46 +1,35 @@
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<form
|
||||
*ngIf="profile && !loading"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="name">{{ "name" | i18n }}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="profile.name" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">{{ "email" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Email"
|
||||
[(ngModel)]="profile.email"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<form *ngIf="profile && !loading" [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-6">
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="email" readonly />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<div class="tw-col-span-6">
|
||||
<div class="tw-mb-3">
|
||||
<dynamic-avatar text="{{ profile | userName }}" [id]="profile.id" [size]="'large'">
|
||||
</dynamic-avatar>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary tw-ml-3.5"
|
||||
buttonType="secondary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
appStopClick
|
||||
appStopProp
|
||||
(click)="openChangeAvatar()"
|
||||
[bitAction]="openChangeAvatar"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-pencil-square" aria-hidden="true"></i>
|
||||
Customize
|
||||
@@ -53,9 +42,6 @@
|
||||
</app-account-fingerprint>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary">{{ "save" | i18n }}</button>
|
||||
</form>
|
||||
<ng-template #avatarModalTemplate></ng-template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ViewChild, ViewContainerRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
@@ -6,7 +7,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
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";
|
||||
|
||||
@@ -21,16 +21,19 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
profile: ProfileResponse;
|
||||
fingerprintMaterial: string;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
@ViewChild("avatarModalTemplate", { read: ViewContainerRef, static: true })
|
||||
avatarModalRef: ViewContainerRef;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected formGroup = new FormGroup({
|
||||
name: new FormControl(null),
|
||||
email: new FormControl(null),
|
||||
});
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
private modalService: ModalService,
|
||||
) {}
|
||||
@@ -39,6 +42,15 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
this.profile = await this.apiService.getProfile();
|
||||
this.loading = false;
|
||||
this.fingerprintMaterial = await this.stateService.getUserId();
|
||||
this.formGroup.get("name").setValue(this.profile.name);
|
||||
this.formGroup.get("email").setValue(this.profile.email);
|
||||
|
||||
this.formGroup
|
||||
.get("name")
|
||||
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((name) => {
|
||||
this.profile.name = name;
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
@@ -46,7 +58,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async openChangeAvatar() {
|
||||
openChangeAvatar = async () => {
|
||||
const modalOpened = await this.modalService.openViewRef(
|
||||
ChangeAvatarComponent,
|
||||
this.avatarModalRef,
|
||||
@@ -57,16 +69,14 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new UpdateProfileRequest(this.profile.name, this.profile.masterPasswordHint);
|
||||
this.formPromise = this.apiService.putProfile(request);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("accountUpdated"));
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
submit = async () => {
|
||||
const request = new UpdateProfileRequest(
|
||||
this.formGroup.get("name").value,
|
||||
this.profile.masterPasswordHint,
|
||||
);
|
||||
await this.apiService.putProfile(request);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("accountUpdated"));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<div class="tw-rounded tw-border tw-border-solid tw-border-warning-600 tw-bg-background">
|
||||
<div class="tw-bg-warning-600 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast">
|
||||
<i class="bwi bwi-envelope bwi-fw" aria-hidden="true"></i> {{ "verifyEmail" | i18n }}
|
||||
</div>
|
||||
<div class="tw-p-5">
|
||||
<p>{{ "verifyEmailDesc" | i18n }}</p>
|
||||
<button id="sendBtn" bitButton type="button" block [bitAction]="send">
|
||||
{{ "sendEmail" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<bit-banner bannerType="warning" (onClose)="onDismiss.emit()">
|
||||
{{ "verifyEmailDesc" | i18n }}
|
||||
<button
|
||||
id="sendBtn"
|
||||
bitLink
|
||||
linkType="contrast"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="unstyled"
|
||||
[bitAction]="send"
|
||||
>
|
||||
{{ "sendEmail" | i18n }}
|
||||
</button>
|
||||
</bit-banner>
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.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, BannerModule, ButtonModule, LinkModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-verify-email",
|
||||
templateUrl: "verify-email.component.html",
|
||||
imports: [AsyncActionsModule, BannerModule, ButtonModule, CommonModule, JslibModule, LinkModule],
|
||||
})
|
||||
export class VerifyEmailComponent {
|
||||
actionPromise: Promise<unknown>;
|
||||
|
||||
@Output() onVerified = new EventEmitter<boolean>();
|
||||
@Output() onDismiss = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private tokenService: TokenService,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "chooseYourPlan" | i18n }}</h2>
|
||||
<div *ngFor="let selectableProduct of selectableProducts">
|
||||
<bit-radio-group formControlName="product" [block]="true">
|
||||
<bit-radio-group formControlName="product" [block]="true">
|
||||
<div *ngFor="let selectableProduct of selectableProducts" class="tw-mb-3">
|
||||
<bit-radio-button [value]="selectableProduct.product" (change)="changedProduct()">
|
||||
<bit-label>{{ selectableProduct.nameLocalizationKey | i18n }}</bit-label>
|
||||
<bit-hint class="tw-text-sm"
|
||||
@@ -147,7 +147,7 @@
|
||||
</ng-template>
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
<span *ngIf="selectableProduct.product != productTypes.Free">
|
||||
<span *ngIf="selectableProduct.product != productTypes.Free" class="tw-pl-4">
|
||||
<ng-container
|
||||
*ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship"
|
||||
>
|
||||
@@ -176,6 +176,7 @@
|
||||
!selectableProduct.PasswordManager.basePrice &&
|
||||
selectableProduct.PasswordManager.hasAdditionalSeatsOption
|
||||
"
|
||||
class="tw-pl-4"
|
||||
>
|
||||
{{
|
||||
"costPerUser"
|
||||
@@ -188,11 +189,11 @@
|
||||
}}
|
||||
/{{ "month" | i18n }}
|
||||
</span>
|
||||
<span *ngIf="selectableProduct.product == productTypes.Free">{{
|
||||
<span *ngIf="selectableProduct.product == productTypes.Free" class="tw-pl-4">{{
|
||||
"freeForever" | i18n
|
||||
}}</span>
|
||||
</bit-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</bit-radio-group>
|
||||
</bit-section>
|
||||
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
|
||||
<bit-section
|
||||
@@ -277,126 +278,128 @@
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</bit-section>
|
||||
<bit-section *ngFor="let selectablePlan of selectablePlans">
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||
<bit-radio-group formControlName="plan">
|
||||
<bit-radio-button
|
||||
type="radio"
|
||||
id="interval{{ selectablePlan.type }}"
|
||||
[value]="selectablePlan.type"
|
||||
>
|
||||
<bit-label>{{ (selectablePlan.isAnnual ? "annually" : "monthly") | i18n }}</bit-label>
|
||||
<bit-hint *ngIf="selectablePlan.isAnnual">
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||
>
|
||||
{{ "basePrice" | i18n }}:
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.basePrice / 12
|
||||
: selectablePlan.PasswordManager.basePrice
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
||||
<span class="tw-line-through">{{
|
||||
selectablePlan.PasswordManager.basePrice | currency: "$"
|
||||
}}</span>
|
||||
{{ "freeWithSponsorship" | i18n }}
|
||||
</ng-container>
|
||||
<ng-template #notAcceptingSponsorship>
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
<div *ngFor="let selectablePlan of selectablePlans">
|
||||
<bit-radio-button
|
||||
type="radio"
|
||||
id="interval{{ selectablePlan.type }}"
|
||||
[value]="selectablePlan.type"
|
||||
>
|
||||
<bit-label>{{ (selectablePlan.isAnnual ? "annually" : "monthly") | i18n }}</bit-label>
|
||||
<bit-hint *ngIf="selectablePlan.isAnnual">
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||
>
|
||||
{{ "basePrice" | i18n }}:
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.basePrice / 12
|
||||
: selectablePlan.PasswordManager.basePrice
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
||||
<span class="tw-line-through">{{
|
||||
selectablePlan.PasswordManager.basePrice | currency: "$"
|
||||
}}</span>
|
||||
{{ "freeWithSponsorship" | i18n }}
|
||||
</ng-container>
|
||||
<ng-template #notAcceptingSponsorship>
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
/{{ "year" | i18n }}
|
||||
</ng-template>
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||
>
|
||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||
>{{ "additionalUsers" | i18n }}:</span
|
||||
>
|
||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.seatPrice / 12
|
||||
: selectablePlan.PasswordManager.seatPrice
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12 {{ "monthAbbr" | i18n }} =
|
||||
{{
|
||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||
| currency: "$"
|
||||
}}
|
||||
/{{ "year" | i18n }}
|
||||
</ng-template>
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||
>
|
||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||
>{{ "additionalUsers" | i18n }}:</span
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||
>
|
||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.seatPrice / 12
|
||||
: selectablePlan.PasswordManager.seatPrice
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12 {{ "monthAbbr" | i18n }} =
|
||||
{{
|
||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||
| currency: "$"
|
||||
}}
|
||||
/{{ "year" | i18n }}
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||
>
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
|
||||
: selectablePlan.PasswordManager.additionalStoragePricePerGb
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12 {{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
|
||||
</p>
|
||||
</bit-hint>
|
||||
<bit-hint *ngIf="!selectablePlan.isAnnual">
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||
>
|
||||
{{ "basePrice" | i18n }}:
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
/{{ "month" | i18n }}
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||
>
|
||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||
>{{ "additionalUsers" | i18n }}:</span
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
|
||||
: selectablePlan.PasswordManager.additionalStoragePricePerGb
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12 {{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
|
||||
</p>
|
||||
</bit-hint>
|
||||
<bit-hint *ngIf="!selectablePlan.isAnnual">
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||
>
|
||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }} =
|
||||
{{
|
||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||
| currency: "$"
|
||||
}}
|
||||
/{{ "month" | i18n }}
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||
>
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
|
||||
</p>
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
{{ "basePrice" | i18n }}:
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
/{{ "month" | i18n }}
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||
>
|
||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||
>{{ "additionalUsers" | i18n }}:</span
|
||||
>
|
||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }} =
|
||||
{{
|
||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||
| currency: "$"
|
||||
}}
|
||||
/{{ "month" | i18n }}
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||
>
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
|
||||
</p>
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
</div>
|
||||
</bit-radio-group>
|
||||
</bit-section>
|
||||
</bit-section>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="default" [title]="'addCredit' | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<p bitTypography="body1">{{ "creditDelayed" | i18n }}</p>
|
||||
<div class="tw-grid tw-grid-cols-2">
|
||||
<bit-radio-group formControlName="method">
|
||||
<bit-radio-button id="credit-method-paypal" [value]="paymentMethodType.PayPal">
|
||||
<bit-label> <i class="bwi bwi-paypal"></i>PayPal</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="credit-method-bitcoin" [value]="paymentMethodType.BitPay">
|
||||
<bit-label> <i class="bwi bwi-bitcoin"></i>Bitcoin</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-2">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "amount" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="creditAmount"
|
||||
(blur)="formatAmount()"
|
||||
required
|
||||
/>
|
||||
<span bitPrefix>$USD</span>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="DialogResult.Cancelled"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
<form #ppButtonForm action="{{ ppButtonFormAction }}" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_xclick" />
|
||||
<input type="hidden" name="business" value="{{ ppButtonBusinessId }}" />
|
||||
<input type="hidden" name="button_subtype" value="services" />
|
||||
<input type="hidden" name="no_note" value="1" />
|
||||
<input type="hidden" name="no_shipping" value="1" />
|
||||
<input type="hidden" name="rm" value="1" />
|
||||
<input type="hidden" name="return" value="{{ returnUrl }}" />
|
||||
<input type="hidden" name="cancel_return" value="{{ returnUrl }}" />
|
||||
<input type="hidden" name="currency_code" value="USD" />
|
||||
<input type="hidden" name="image_url" value="https://bitwarden.com/images/paypal-banner.png" />
|
||||
<input type="hidden" name="bn" value="PP-BuyNowBF:btn_buynow_LG.gif:NonHosted" />
|
||||
<input type="hidden" name="amount" value="{{ formGroup.get('creditAmount').value }}" />
|
||||
<input type="hidden" name="custom" value="{{ ppButtonCustomField }}" />
|
||||
<input type="hidden" name="item_name" value="Bitwarden Account Credit" />
|
||||
<input type="hidden" name="item_number" value="{{ subject }}" />
|
||||
</form>
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -17,6 +11,16 @@ import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/b
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export interface AddCreditDialogData {
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export enum AddCreditDialogResult {
|
||||
Added = "added",
|
||||
Cancelled = "cancelled",
|
||||
}
|
||||
|
||||
export type PayPalConfig = {
|
||||
businessId?: string;
|
||||
@@ -24,17 +28,9 @@ export type PayPalConfig = {
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-add-credit",
|
||||
templateUrl: "add-credit.component.html",
|
||||
templateUrl: "add-credit-dialog.component.html",
|
||||
})
|
||||
export class AddCreditComponent implements OnInit {
|
||||
@Input() creditAmount: string;
|
||||
@Input() showOptions = true;
|
||||
@Input() method = PaymentMethodType.PayPal;
|
||||
@Input() organizationId: string;
|
||||
@Output() onAdded = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
export class AddCreditDialogComponent implements OnInit {
|
||||
@ViewChild("ppButtonForm", { read: ElementRef, static: true }) ppButtonFormRef: ElementRef;
|
||||
|
||||
paymentMethodType = PaymentMethodType;
|
||||
@@ -44,14 +40,22 @@ export class AddCreditComponent implements OnInit {
|
||||
ppLoading = false;
|
||||
subject: string;
|
||||
returnUrl: string;
|
||||
formPromise: Promise<any>;
|
||||
organizationId: string;
|
||||
|
||||
private userId: string;
|
||||
private name: string;
|
||||
private email: string;
|
||||
private region: string;
|
||||
|
||||
protected DialogResult = AddCreditDialogResult;
|
||||
protected formGroup = new FormGroup({
|
||||
method: new FormControl(PaymentMethodType.PayPal),
|
||||
creditAmount: new FormControl(null, [Validators.required]),
|
||||
});
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) protected data: AddCreditDialogData,
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@@ -59,6 +63,7 @@ export class AddCreditComponent implements OnInit {
|
||||
private logService: LogService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.organizationId = data.organizationId;
|
||||
const payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig;
|
||||
this.ppButtonFormAction = payPalConfig.buttonAction;
|
||||
this.ppButtonBusinessId = payPalConfig.businessId;
|
||||
@@ -93,7 +98,18 @@ export class AddCreditComponent implements OnInit {
|
||||
this.returnUrl = window.location.href;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
get creditAmount() {
|
||||
return this.formGroup.value.creditAmount;
|
||||
}
|
||||
set creditAmount(value: string) {
|
||||
this.formGroup.get("creditAmount").setValue(value);
|
||||
}
|
||||
|
||||
get method() {
|
||||
return this.formGroup.value.method;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (this.creditAmount == null || this.creditAmount === "") {
|
||||
return;
|
||||
}
|
||||
@@ -104,33 +120,20 @@ export class AddCreditComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
if (this.method === PaymentMethodType.BitPay) {
|
||||
try {
|
||||
const req = new BitPayInvoiceRequest();
|
||||
req.email = this.email;
|
||||
req.name = this.name;
|
||||
req.credit = true;
|
||||
req.amount = this.creditAmountNumber;
|
||||
req.organizationId = this.organizationId;
|
||||
req.userId = this.userId;
|
||||
req.returnUrl = this.returnUrl;
|
||||
this.formPromise = this.apiService.postBitPayInvoice(req);
|
||||
const bitPayUrl: string = await this.formPromise;
|
||||
this.platformUtilsService.launchUri(bitPayUrl);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
const req = new BitPayInvoiceRequest();
|
||||
req.email = this.email;
|
||||
req.name = this.name;
|
||||
req.credit = true;
|
||||
req.amount = this.creditAmountNumber;
|
||||
req.organizationId = this.organizationId;
|
||||
req.userId = this.userId;
|
||||
req.returnUrl = this.returnUrl;
|
||||
const bitPayUrl: string = await this.apiService.postBitPayInvoice(req);
|
||||
this.platformUtilsService.launchUri(bitPayUrl);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.onAdded.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
this.dialogRef.close(AddCreditDialogResult.Added);
|
||||
};
|
||||
|
||||
formatAmount() {
|
||||
try {
|
||||
@@ -160,3 +163,15 @@ export class AddCreditComponent implements OnInit {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a AddCreditDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export function openAddCreditDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AddCreditDialogData>,
|
||||
) {
|
||||
return dialogService.open<AddCreditDialogResult>(AddCreditDialogComponent, config);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h3 class="card-body-header">{{ "addCredit" | i18n }}</h3>
|
||||
<div class="mb-4 text-lg" *ngIf="showOptions">
|
||||
<div class="form-check form-check-inline">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="Method"
|
||||
id="credit-method-paypal"
|
||||
[value]="paymentMethodType.PayPal"
|
||||
[(ngModel)]="method"
|
||||
/>
|
||||
<label class="form-check-label" for="credit-method-paypal">
|
||||
<i class="bwi bwi-fw bwi-paypal" aria-hidden="true"></i> PayPal</label
|
||||
>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="Method"
|
||||
id="credit-method-bitcoin"
|
||||
[value]="paymentMethodType.BitPay"
|
||||
[(ngModel)]="method"
|
||||
/>
|
||||
<label class="form-check-label" for="credit-method-bitcoin">
|
||||
<i class="bwi bwi-fw bwi-bitcoin" aria-hidden="true"></i> Bitcoin</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<label for="creditAmount">{{ "amount" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text">$USD</span></div>
|
||||
<input
|
||||
id="creditAmount"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="CreditAmount"
|
||||
[(ngModel)]="creditAmount"
|
||||
(blur)="formatAmount()"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{ "creditDelayed" | i18n }}</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading || ppLoading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "submit" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<form #ppButtonForm action="{{ ppButtonFormAction }}" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_xclick" />
|
||||
<input type="hidden" name="business" value="{{ ppButtonBusinessId }}" />
|
||||
<input type="hidden" name="button_subtype" value="services" />
|
||||
<input type="hidden" name="no_note" value="1" />
|
||||
<input type="hidden" name="no_shipping" value="1" />
|
||||
<input type="hidden" name="rm" value="1" />
|
||||
<input type="hidden" name="return" value="{{ returnUrl }}" />
|
||||
<input type="hidden" name="cancel_return" value="{{ returnUrl }}" />
|
||||
<input type="hidden" name="currency_code" value="USD" />
|
||||
<input type="hidden" name="image_url" value="https://bitwarden.com/images/paypal-banner.png" />
|
||||
<input type="hidden" name="bn" value="PP-BuyNowBF:btn_buynow_LG.gif:NonHosted" />
|
||||
<input type="hidden" name="amount" value="{{ creditAmount }}" />
|
||||
<input type="hidden" name="custom" value="{{ ppButtonCustomField }}" />
|
||||
<input type="hidden" name="item_name" value="Bitwarden Account Credit" />
|
||||
<input type="hidden" name="item_number" value="{{ subject }}" />
|
||||
</form>
|
||||
@@ -3,7 +3,7 @@ import { NgModule } from "@angular/core";
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { AddCreditComponent } from "./add-credit.component";
|
||||
import { AddCreditDialogComponent } from "./add-credit-dialog.component";
|
||||
import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog.component";
|
||||
import { AdjustStorageComponent } from "./adjust-storage.component";
|
||||
import { BillingHistoryComponent } from "./billing-history.component";
|
||||
@@ -17,7 +17,7 @@ import { UpdateLicenseComponent } from "./update-license.component";
|
||||
@NgModule({
|
||||
imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule],
|
||||
declarations: [
|
||||
AddCreditComponent,
|
||||
AddCreditDialogComponent,
|
||||
AdjustPaymentDialogComponent,
|
||||
AdjustStorageComponent,
|
||||
BillingHistoryComponent,
|
||||
|
||||
@@ -33,22 +33,9 @@
|
||||
<strong>{{ creditOrBalance | currency: "$" }}</strong>
|
||||
</p>
|
||||
<p>{{ "creditAppliedDesc" | i18n }}</p>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
(click)="addCredit()"
|
||||
*ngIf="!showAddCredit"
|
||||
>
|
||||
<button type="button" bitButton buttonType="secondary" [bitAction]="addCredit">
|
||||
{{ "addCredit" | i18n }}
|
||||
</button>
|
||||
<app-add-credit
|
||||
[organizationId]="organizationId"
|
||||
(onAdded)="closeAddCredit(true)"
|
||||
(onCanceled)="closeAddCredit(false)"
|
||||
*ngIf="showAddCredit"
|
||||
>
|
||||
</app-add-credit>
|
||||
<h2 class="spaced-header">{{ "paymentMethod" | i18n }}</h2>
|
||||
<p *ngIf="!paymentSource">{{ "noPaymentMethod" | i18n }}</p>
|
||||
<ng-container *ngIf="paymentSource">
|
||||
|
||||
@@ -15,6 +15,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component";
|
||||
import {
|
||||
AdjustPaymentDialogResult,
|
||||
openAdjustPaymentDialog,
|
||||
@@ -30,7 +31,6 @@ export class PaymentMethodComponent implements OnInit {
|
||||
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
showAddCredit = false;
|
||||
billing: BillingPaymentResponse;
|
||||
org: OrganizationSubscriptionResponse;
|
||||
sub: SubscriptionResponse;
|
||||
@@ -111,18 +111,17 @@ export class PaymentMethodComponent implements OnInit {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
addCredit() {
|
||||
this.showAddCredit = true;
|
||||
}
|
||||
|
||||
closeAddCredit(load: boolean) {
|
||||
this.showAddCredit = false;
|
||||
if (load) {
|
||||
// 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.load();
|
||||
addCredit = async () => {
|
||||
const dialogRef = openAddCreditDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organizationId,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AddCreditDialogResult.Added) {
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
changePayment = async () => {
|
||||
const dialogRef = openAdjustPaymentDialog(this.dialogService, {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<div class="tw-rounded tw-border tw-border-solid tw-border-warning-600 tw-bg-background">
|
||||
<div class="tw-bg-warning-600 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast">
|
||||
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
|
||||
{{ "lowKdfIterations" | i18n }}
|
||||
</div>
|
||||
<div class="tw-p-5">
|
||||
<p>{{ "updateLowKdfIterationsDesc" | i18n }}</p>
|
||||
<a
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
[block]="true"
|
||||
routerLink="/settings/security/security-keys"
|
||||
>
|
||||
{{ "updateKdfSettings" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-low-kdf",
|
||||
templateUrl: "low-kdf.component.html",
|
||||
})
|
||||
export class LowKdfComponent {}
|
||||
@@ -53,7 +53,6 @@ import { TwoFactorSetupComponent } from "../auth/settings/two-factor-setup.compo
|
||||
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor-verify.component";
|
||||
import { TwoFactorWebAuthnComponent } from "../auth/settings/two-factor-webauthn.component";
|
||||
import { TwoFactorYubiKeyComponent } from "../auth/settings/two-factor-yubikey.component";
|
||||
import { VerifyEmailComponent } from "../auth/settings/verify-email.component";
|
||||
import { UserVerificationModule } from "../auth/shared/components/user-verification";
|
||||
import { SsoComponent } from "../auth/sso.component";
|
||||
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component";
|
||||
@@ -70,7 +69,6 @@ import { HeaderModule } from "../layouts/header/header.module";
|
||||
import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module";
|
||||
import { UserLayoutComponent } from "../layouts/user-layout.component";
|
||||
import { DomainRulesComponent } from "../settings/domain-rules.component";
|
||||
import { LowKdfComponent } from "../settings/low-kdf.component";
|
||||
import { PreferencesComponent } from "../settings/preferences.component";
|
||||
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
||||
import { GeneratorComponent } from "../tools/generator.component";
|
||||
@@ -186,11 +184,9 @@ import { SharedModule } from "./shared.module";
|
||||
UpdatePasswordComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
VaultTimeoutInputComponent,
|
||||
VerifyEmailComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
VerifyRecoverDeleteProviderComponent,
|
||||
LowKdfComponent,
|
||||
],
|
||||
exports: [
|
||||
UserVerificationModule,
|
||||
@@ -264,11 +260,9 @@ import { SharedModule } from "./shared.module";
|
||||
UpdateTempPasswordComponent,
|
||||
UserLayoutComponent,
|
||||
VaultTimeoutInputComponent,
|
||||
VerifyEmailComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
VerifyRecoverDeleteProviderComponent,
|
||||
LowKdfComponent,
|
||||
HeaderModule,
|
||||
DangerZoneComponent,
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
||||
@@ -6,7 +6,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
@@ -21,23 +20,23 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
||||
constructor(
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||
stateService: StateService,
|
||||
accountService: AccountService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
logService: LogService,
|
||||
route: ActivatedRoute,
|
||||
ngZone: NgZone,
|
||||
private dialogService: DialogService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
passwordGenerationService,
|
||||
usernameGenerationService,
|
||||
platformUtilsService,
|
||||
stateService,
|
||||
accountService,
|
||||
i18nService,
|
||||
logService,
|
||||
route,
|
||||
accountService,
|
||||
ngZone,
|
||||
window,
|
||||
);
|
||||
if (platformUtilsService.isSelfHost()) {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { UntypedFormBuilder } from "@angular/forms";
|
||||
|
||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -26,7 +24,6 @@ export class ExportComponent extends BaseExportComponent {
|
||||
eventCollectionService: EventCollectionService,
|
||||
policyService: PolicyService,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
formBuilder: UntypedFormBuilder,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
@@ -39,84 +36,10 @@ export class ExportComponent extends BaseExportComponent {
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
formBuilder,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
organizationService,
|
||||
);
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (this.isFileEncryptedExport && this.filePassword != this.confirmFilePassword) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("filePasswordAndConfirmFilePasswordDoNotMatch"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.exportForm.markAllAsTouched();
|
||||
if (this.exportForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.disabledByPolicy) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("personalVaultExportPolicyInEffect"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const userVerified = await this.verifyUser();
|
||||
if (!userVerified) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.doExport();
|
||||
};
|
||||
|
||||
protected saved() {
|
||||
super.saved();
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("exportSuccess"));
|
||||
}
|
||||
|
||||
private async verifyUser(): Promise<boolean> {
|
||||
let confirmDescription = "exportWarningDesc";
|
||||
if (this.isFileEncryptedExport) {
|
||||
confirmDescription = "fileEncryptedExportWarningDesc";
|
||||
} else if (this.isAccountEncryptedExport) {
|
||||
confirmDescription = "encExportKeyWarningDesc";
|
||||
}
|
||||
|
||||
const result = await UserVerificationDialogComponent.open(this.dialogService, {
|
||||
title: "confirmVaultExport",
|
||||
bodyText: confirmDescription,
|
||||
confirmButtonOptions: {
|
||||
text: "exportVault",
|
||||
type: "primary",
|
||||
},
|
||||
});
|
||||
|
||||
// Handle the result of the dialog based on user action and verification success
|
||||
if (result.userAction === "cancel") {
|
||||
// User cancelled the dialog
|
||||
return false;
|
||||
}
|
||||
|
||||
// User confirmed the dialog so check verification success
|
||||
if (!result.verificationSuccess) {
|
||||
if (result.noAvailableClientVerificationMethods) {
|
||||
// No client-side verification methods are available
|
||||
// Could send user to configure a verification method like PIN or biometrics
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,14 +85,26 @@ export class CollectionAdminView extends CollectionView {
|
||||
* Whether the user can modify user access to this collection
|
||||
*/
|
||||
canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageUsers;
|
||||
const allowAdminAccessToAllCollectionItems =
|
||||
!flexibleCollectionsV1Enabled || org.allowAdminAccessToAllCollectionItems;
|
||||
|
||||
return (
|
||||
(org.permissions.manageUsers && allowAdminAccessToAllCollectionItems) ||
|
||||
this.canEdit(org, flexibleCollectionsV1Enabled)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user can modify group access to this collection
|
||||
*/
|
||||
canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups;
|
||||
const allowAdminAccessToAllCollectionItems =
|
||||
!flexibleCollectionsV1Enabled || org.allowAdminAccessToAllCollectionItems;
|
||||
|
||||
return (
|
||||
(org.permissions.manageGroups && allowAdminAccessToAllCollectionItems) ||
|
||||
this.canEdit(org, flexibleCollectionsV1Enabled)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { KdfType } from "@bitwarden/common/platform/enums";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
import {
|
||||
PREMIUM_BANNER_REPROMPT_KEY,
|
||||
VaultBannersService,
|
||||
VisibleVaultBanner,
|
||||
} from "./vault-banners.service";
|
||||
|
||||
describe("VaultBannersService", () => {
|
||||
let service: VaultBannersService;
|
||||
const isSelfHost = jest.fn().mockReturnValue(false);
|
||||
const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(false);
|
||||
const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
|
||||
const getEmailVerified = jest.fn().mockResolvedValue(true);
|
||||
const hasMasterPassword = jest.fn().mockResolvedValue(true);
|
||||
const getKdfConfig = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600000 });
|
||||
const getLastSync = jest.fn().mockResolvedValue(null);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
getLastSync.mockClear().mockResolvedValue(new Date("2024-05-14"));
|
||||
isSelfHost.mockClear();
|
||||
getEmailVerified.mockClear().mockResolvedValue(true);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
VaultBannersService,
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: { isSelfHost },
|
||||
},
|
||||
{
|
||||
provide: BillingAccountProfileStateService,
|
||||
useValue: { hasPremiumFromAnySource$: hasPremiumFromAnySource$ },
|
||||
},
|
||||
{
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: { isSelfHost },
|
||||
},
|
||||
{
|
||||
provide: TokenService,
|
||||
useValue: { getEmailVerified },
|
||||
},
|
||||
{
|
||||
provide: UserVerificationService,
|
||||
useValue: { hasMasterPassword },
|
||||
},
|
||||
{
|
||||
provide: KdfConfigService,
|
||||
useValue: { getKdfConfig },
|
||||
},
|
||||
{
|
||||
provide: SyncService,
|
||||
useValue: { getLastSync },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("Premium", () => {
|
||||
it("waits until sync is completed before showing premium banner", async () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
isSelfHost.mockReturnValue(false);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
jest.advanceTimersByTime(201);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show a premium banner for self-hosted users", async () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
isSelfHost.mockReturnValue(true);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
jest.advanceTimersByTime(201);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show a premium banner when they have access to premium", async () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(true);
|
||||
isSelfHost.mockReturnValue(false);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
jest.advanceTimersByTime(201);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(false);
|
||||
});
|
||||
|
||||
describe("dismissing", () => {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
const date = new Date("2023-06-08");
|
||||
date.setHours(0, 0, 0, 0);
|
||||
jest.setSystemTime(date.getTime());
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
await service.dismissBanner(VisibleVaultBanner.Premium);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("updates state on first dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneWeekLater = new Date("2023-06-15");
|
||||
oneWeekLater.setHours(0, 0, 0, 0);
|
||||
|
||||
expect(state).toEqual({
|
||||
numberOfDismissals: 1,
|
||||
nextPromptDate: oneWeekLater.getTime(),
|
||||
});
|
||||
});
|
||||
|
||||
it("updates state on second dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneMonthLater = new Date("2023-07-08");
|
||||
oneMonthLater.setHours(0, 0, 0, 0);
|
||||
|
||||
expect(state).toEqual({
|
||||
numberOfDismissals: 2,
|
||||
nextPromptDate: oneMonthLater.getTime(),
|
||||
});
|
||||
});
|
||||
|
||||
it("updates state on third dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneYearLater = new Date("2024-06-08");
|
||||
oneYearLater.setHours(0, 0, 0, 0);
|
||||
|
||||
expect(state).toEqual({
|
||||
numberOfDismissals: 3,
|
||||
nextPromptDate: oneYearLater.getTime(),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("KDFSettings", () => {
|
||||
beforeEach(async () => {
|
||||
hasMasterPassword.mockResolvedValue(true);
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 });
|
||||
});
|
||||
|
||||
it("shows low KDF iteration banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show low KDF iteration banner if KDF type is not PBKDF2_SHA256", async () => {
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.Argon2id, iterations: 600001 });
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show low KDF for iterations about 600,000", async () => {
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 });
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||
});
|
||||
|
||||
it("dismisses low KDF iteration banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(true);
|
||||
|
||||
await service.dismissBanner(VisibleVaultBanner.KDFSettings);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OutdatedBrowser", () => {
|
||||
beforeEach(async () => {
|
||||
// Hardcode `MSIE` in userAgent string
|
||||
const userAgent = "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 MSIE";
|
||||
Object.defineProperty(navigator, "userAgent", {
|
||||
configurable: true,
|
||||
get: () => userAgent,
|
||||
});
|
||||
});
|
||||
|
||||
it("shows outdated browser banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowUpdateBrowserBanner()).toBe(true);
|
||||
});
|
||||
|
||||
it("dismisses outdated browser banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowUpdateBrowserBanner()).toBe(true);
|
||||
|
||||
await service.dismissBanner(VisibleVaultBanner.OutdatedBrowser);
|
||||
|
||||
expect(await service.shouldShowUpdateBrowserBanner()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("VerifyEmail", () => {
|
||||
beforeEach(async () => {
|
||||
getEmailVerified.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it("shows verify email banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowVerifyEmailBanner()).toBe(true);
|
||||
});
|
||||
|
||||
it("dismisses verify email banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowVerifyEmailBanner()).toBe(true);
|
||||
|
||||
await service.dismissBanner(VisibleVaultBanner.VerifyEmail);
|
||||
|
||||
expect(await service.shouldShowVerifyEmailBanner()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject, Observable, combineLatest, firstValueFrom, map } from "rxjs";
|
||||
import { mergeMap, take } from "rxjs/operators";
|
||||
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
|
||||
import {
|
||||
StateProvider,
|
||||
ActiveUserState,
|
||||
KeyDefinition,
|
||||
PREMIUM_BANNER_DISK_LOCAL,
|
||||
BANNERS_DISMISSED_DISK,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
export enum VisibleVaultBanner {
|
||||
KDFSettings = "kdf-settings",
|
||||
OutdatedBrowser = "outdated-browser",
|
||||
Premium = "premium",
|
||||
VerifyEmail = "verify-email",
|
||||
}
|
||||
|
||||
type PremiumBannerReprompt = {
|
||||
numberOfDismissals: number;
|
||||
/** Timestamp representing when to show the prompt next */
|
||||
nextPromptDate: number;
|
||||
};
|
||||
|
||||
/** Banners that will be re-shown on a new session */
|
||||
type SessionBanners = Omit<VisibleVaultBanner, VisibleVaultBanner.Premium>;
|
||||
|
||||
export const PREMIUM_BANNER_REPROMPT_KEY = new KeyDefinition<PremiumBannerReprompt>(
|
||||
PREMIUM_BANNER_DISK_LOCAL,
|
||||
"bannerReprompt",
|
||||
{
|
||||
deserializer: (bannerReprompt) => bannerReprompt,
|
||||
},
|
||||
);
|
||||
|
||||
export const BANNERS_DISMISSED_DISK_KEY = new KeyDefinition<SessionBanners[]>(
|
||||
BANNERS_DISMISSED_DISK,
|
||||
"bannersDismissed",
|
||||
{
|
||||
deserializer: (bannersDismissed) => bannersDismissed,
|
||||
},
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class VaultBannersService {
|
||||
shouldShowPremiumBanner$: Observable<boolean>;
|
||||
|
||||
private premiumBannerState: ActiveUserState<PremiumBannerReprompt>;
|
||||
private sessionBannerState: ActiveUserState<SessionBanners[]>;
|
||||
|
||||
/**
|
||||
* Emits when the sync service has completed a sync
|
||||
*
|
||||
* This is needed because `hasPremiumFromAnySource$` will emit false until the sync is completed
|
||||
* resulting in the premium banner being shown briefly on startup when the user has access to
|
||||
* premium features.
|
||||
*/
|
||||
private syncCompleted$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private tokenService: TokenService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private stateProvider: StateProvider,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private syncService: SyncService,
|
||||
) {
|
||||
this.pollUntilSynced();
|
||||
this.premiumBannerState = this.stateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY);
|
||||
this.sessionBannerState = this.stateProvider.getActive(BANNERS_DISMISSED_DISK_KEY);
|
||||
|
||||
const premiumSources$ = combineLatest([
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
|
||||
this.premiumBannerState.state$,
|
||||
]);
|
||||
|
||||
this.shouldShowPremiumBanner$ = this.syncCompleted$.pipe(
|
||||
take(1), // Wait until the first sync is complete before considering the premium status
|
||||
mergeMap(() => premiumSources$),
|
||||
map(([canAccessPremium, dismissedState]) => {
|
||||
const shouldShowPremiumBanner =
|
||||
!canAccessPremium && !this.platformUtilsService.isSelfHost();
|
||||
|
||||
// Check if nextPromptDate is in the past passed
|
||||
if (shouldShowPremiumBanner && dismissedState?.nextPromptDate) {
|
||||
const nextPromptDate = new Date(dismissedState.nextPromptDate);
|
||||
const now = new Date();
|
||||
return now >= nextPromptDate;
|
||||
}
|
||||
|
||||
return shouldShowPremiumBanner;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Returns true when the update browser banner should be shown */
|
||||
async shouldShowUpdateBrowserBanner(): Promise<boolean> {
|
||||
const outdatedBrowser = window.navigator.userAgent.indexOf("MSIE") !== -1;
|
||||
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||
VisibleVaultBanner.OutdatedBrowser,
|
||||
);
|
||||
|
||||
return outdatedBrowser && !alreadyDismissed;
|
||||
}
|
||||
|
||||
/** Returns true when the verify email banner should be shown */
|
||||
async shouldShowVerifyEmailBanner(): Promise<boolean> {
|
||||
const needsVerification = !(await this.tokenService.getEmailVerified());
|
||||
|
||||
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||
VisibleVaultBanner.VerifyEmail,
|
||||
);
|
||||
|
||||
return needsVerification && !alreadyDismissed;
|
||||
}
|
||||
|
||||
/** Returns true when the low KDF iteration banner should be shown */
|
||||
async shouldShowLowKDFBanner(): Promise<boolean> {
|
||||
const hasLowKDF = (await this.userVerificationService.hasMasterPassword())
|
||||
? await this.isLowKdfIteration()
|
||||
: false;
|
||||
|
||||
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||
VisibleVaultBanner.KDFSettings,
|
||||
);
|
||||
|
||||
return hasLowKDF && !alreadyDismissed;
|
||||
}
|
||||
|
||||
/** Dismiss the given banner and perform any respective side effects */
|
||||
async dismissBanner(banner: SessionBanners): Promise<void> {
|
||||
if (banner === VisibleVaultBanner.Premium) {
|
||||
await this.dismissPremiumBanner();
|
||||
} else {
|
||||
await this.sessionBannerState.update((current) => {
|
||||
const bannersDismissed = current ?? [];
|
||||
|
||||
return [...bannersDismissed, banner];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns banners that have already been dismissed */
|
||||
private async getBannerDismissedState(): Promise<SessionBanners[]> {
|
||||
// `state$` can emit null when a value has not been set yet,
|
||||
// use nullish coalescing to default to an empty array
|
||||
return (await firstValueFrom(this.sessionBannerState.state$)) ?? [];
|
||||
}
|
||||
|
||||
/** Increment dismissal state of the premium banner */
|
||||
private async dismissPremiumBanner(): Promise<void> {
|
||||
await this.premiumBannerState.update((current) => {
|
||||
const numberOfDismissals = current?.numberOfDismissals ?? 0;
|
||||
const now = new Date();
|
||||
|
||||
// Set midnight of the current day
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
// First dismissal, re-prompt in 1 week
|
||||
if (numberOfDismissals === 0) {
|
||||
now.setDate(now.getDate() + 7);
|
||||
return {
|
||||
numberOfDismissals: 1,
|
||||
nextPromptDate: now.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
// Second dismissal, re-prompt in 1 month
|
||||
if (numberOfDismissals === 1) {
|
||||
now.setMonth(now.getMonth() + 1);
|
||||
return {
|
||||
numberOfDismissals: 2,
|
||||
nextPromptDate: now.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
// 3+ dismissals, re-prompt each year
|
||||
// Avoid day/month edge cases and only increment year
|
||||
const nextYear = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate());
|
||||
nextYear.setHours(0, 0, 0, 0);
|
||||
return {
|
||||
numberOfDismissals: numberOfDismissals + 1,
|
||||
nextPromptDate: nextYear.getTime(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async isLowKdfIteration() {
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
return (
|
||||
kdfConfig.kdfType === KdfType.PBKDF2_SHA256 &&
|
||||
kdfConfig.iterations < PBKDF2_ITERATIONS.defaultValue
|
||||
);
|
||||
}
|
||||
|
||||
/** Poll the `syncService` until a sync is completed */
|
||||
private pollUntilSynced() {
|
||||
const interval = setInterval(async () => {
|
||||
const lastSync = await this.syncService.getLastSync();
|
||||
if (lastSync !== null) {
|
||||
clearInterval(interval);
|
||||
this.syncCompleted$.next();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<bit-banner
|
||||
id="update-browser-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
bannerType="warning"
|
||||
*ngIf="visibleBanners.includes(VisibleVaultBanner.OutdatedBrowser)"
|
||||
(onClose)="dismissBanner(VisibleVaultBanner.OutdatedBrowser)"
|
||||
>
|
||||
{{ "updateBrowserDesc" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="contrast"
|
||||
target="_blank"
|
||||
href="https://browser-update.org/update-browser.html"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ "updateBrowser" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
|
||||
<bit-banner
|
||||
id="kdf-settings-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
bannerType="warning"
|
||||
*ngIf="visibleBanners.includes(VisibleVaultBanner.KDFSettings)"
|
||||
(onClose)="dismissBanner(VisibleVaultBanner.KDFSettings)"
|
||||
>
|
||||
{{ "lowKDFIterationsBanner" | i18n }}
|
||||
<a bitLink linkType="contrast" routerLink="/settings/security/security-keys">
|
||||
{{ "changeKDFSettings" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
|
||||
<app-verify-email
|
||||
id="verify-email-banner"
|
||||
*ngIf="visibleBanners.includes(VisibleVaultBanner.VerifyEmail)"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
(onDismiss)="dismissBanner(VisibleVaultBanner.VerifyEmail)"
|
||||
(onVerified)="dismissBanner(VisibleVaultBanner.VerifyEmail)"
|
||||
></app-verify-email>
|
||||
|
||||
<bit-banner
|
||||
id="premium-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
bannerType="premium"
|
||||
*ngIf="premiumBannerVisible$ | async"
|
||||
(onClose)="dismissBanner(VisibleVaultBanner.Premium)"
|
||||
>
|
||||
{{ "premiumUpgradeUnlockFeatures" | i18n }}
|
||||
<a bitLink linkType="contrast" routerLink="/settings/subscription/premium">
|
||||
{{ "goPremium" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
@@ -0,0 +1,140 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { BannerComponent, BannerModule } from "@bitwarden/components";
|
||||
|
||||
import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
|
||||
import { LooseComponentsModule } from "../../../shared";
|
||||
|
||||
import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service";
|
||||
import { VaultBannersComponent } from "./vault-banners.component";
|
||||
|
||||
describe("VaultBannersComponent", () => {
|
||||
let component: VaultBannersComponent;
|
||||
let fixture: ComponentFixture<VaultBannersComponent>;
|
||||
const premiumBanner$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
const bannerService = mock<VaultBannersService>({
|
||||
shouldShowPremiumBanner$: premiumBanner$,
|
||||
shouldShowUpdateBrowserBanner: jest.fn(),
|
||||
shouldShowVerifyEmailBanner: jest.fn(),
|
||||
shouldShowLowKDFBanner: jest.fn(),
|
||||
dismissBanner: jest.fn(),
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
bannerService.shouldShowPremiumBanner$ = premiumBanner$;
|
||||
bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false);
|
||||
bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false);
|
||||
bannerService.shouldShowLowKDFBanner.mockResolvedValue(false);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [BannerModule, LooseComponentsModule, VerifyEmailComponent],
|
||||
declarations: [VaultBannersComponent, I18nPipe],
|
||||
providers: [
|
||||
{
|
||||
provide: VaultBannersService,
|
||||
useValue: bannerService,
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>({ t: (key) => key }),
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useValue: mock<ApiService>(),
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: mock<PlatformUtilsService>(),
|
||||
},
|
||||
{
|
||||
provide: TokenService,
|
||||
useValue: mock<TokenService>(),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(VaultBannersComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe("premiumBannerVisible$", () => {
|
||||
it("shows premium banner", async () => {
|
||||
premiumBanner$.next(true);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
||||
expect(banner.componentInstance.bannerType).toBe("premium");
|
||||
});
|
||||
|
||||
it("dismisses premium banner", async () => {
|
||||
premiumBanner$.next(false);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("determineVisibleBanner", () => {
|
||||
[
|
||||
{
|
||||
name: "OutdatedBrowser",
|
||||
method: bannerService.shouldShowUpdateBrowserBanner,
|
||||
banner: VisibleVaultBanner.OutdatedBrowser,
|
||||
},
|
||||
{
|
||||
name: "VerifyEmail",
|
||||
method: bannerService.shouldShowVerifyEmailBanner,
|
||||
banner: VisibleVaultBanner.VerifyEmail,
|
||||
},
|
||||
{
|
||||
name: "LowKDF",
|
||||
method: bannerService.shouldShowLowKDFBanner,
|
||||
banner: VisibleVaultBanner.KDFSettings,
|
||||
},
|
||||
].forEach(({ name, method, banner }) => {
|
||||
describe(name, () => {
|
||||
beforeEach(async () => {
|
||||
method.mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it(`shows ${name} banner`, async () => {
|
||||
expect(component.visibleBanners).toEqual([banner]);
|
||||
});
|
||||
|
||||
it(`dismisses ${name} banner`, async () => {
|
||||
const dismissButton = fixture.debugElement.nativeElement.querySelector(
|
||||
'button[biticonbutton="bwi-close"]',
|
||||
);
|
||||
|
||||
// Mock out the banner service returning false after dismissing
|
||||
method.mockResolvedValue(false);
|
||||
|
||||
dismissButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(bannerService.dismissBanner).toHaveBeenCalledWith(banner);
|
||||
|
||||
expect(component.visibleBanners).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-banners",
|
||||
templateUrl: "./vault-banners.component.html",
|
||||
})
|
||||
export class VaultBannersComponent implements OnInit {
|
||||
visibleBanners: VisibleVaultBanner[] = [];
|
||||
premiumBannerVisible$: Observable<boolean>;
|
||||
VisibleVaultBanner = VisibleVaultBanner;
|
||||
|
||||
constructor(private vaultBannerService: VaultBannersService) {
|
||||
this.premiumBannerVisible$ = this.vaultBannerService.shouldShowPremiumBanner$;
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.determineVisibleBanners();
|
||||
}
|
||||
|
||||
async dismissBanner(banner: VisibleVaultBanner): Promise<void> {
|
||||
await this.vaultBannerService.dismissBanner(banner);
|
||||
|
||||
await this.determineVisibleBanners();
|
||||
}
|
||||
|
||||
/** Determine which banners should be present */
|
||||
private async determineVisibleBanners(): Promise<void> {
|
||||
const showBrowserOutdated = await this.vaultBannerService.shouldShowUpdateBrowserBanner();
|
||||
const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner();
|
||||
const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner();
|
||||
|
||||
this.visibleBanners = [
|
||||
showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null,
|
||||
showVerifyEmail ? VisibleVaultBanner.VerifyEmail : null,
|
||||
showLowKdf ? VisibleVaultBanner.KDFSettings : null,
|
||||
].filter(Boolean); // remove all falsy values, i.e. null
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
<app-vault-banners></app-vault-banners>
|
||||
|
||||
<app-vault-header
|
||||
[filter]="filter"
|
||||
[loading]="refreshing && !performingInitialLoad"
|
||||
@@ -14,8 +16,8 @@
|
||||
<app-vault-onboarding [ciphers]="ciphers" [orgs]="allOrganizations" (onAddCipher)="addCipher()">
|
||||
</app-vault-onboarding>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="tw-flex tw-flex-row -tw-mx-2.5">
|
||||
<div class="tw-basis-1/4 tw-max-w-1/4 tw-px-2.5">
|
||||
<div class="groupings">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
@@ -30,7 +32,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
|
||||
<div class="tw-basis-3/4 tw-max-w-3/4 tw-px-2.5">
|
||||
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
||||
{{ trashCleanupWarning }}
|
||||
</app-callout>
|
||||
@@ -81,44 +83,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<app-low-kdf class="d-block mb-4" *ngIf="showLowKdf"> </app-low-kdf>
|
||||
|
||||
<app-verify-email
|
||||
*ngIf="showVerifyEmail"
|
||||
class="d-block mb-4"
|
||||
(onVerified)="emailVerified($event)"
|
||||
></app-verify-email>
|
||||
|
||||
<div class="card border-warning mb-4" *ngIf="showBrowserOutdated">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
|
||||
{{ "updateBrowser" | i18n }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ "updateBrowserDesc" | i18n }}</p>
|
||||
<a
|
||||
class="btn btn-block btn-outline-secondary"
|
||||
target="_blank"
|
||||
href="https://browser-update.org/update-browser.html"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ "updateBrowser" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card border-success mb-4" *ngIf="showPremiumCallout">
|
||||
<div class="card-header bg-success text-white">
|
||||
<i class="bwi bwi-star-f bwi-fw" aria-hidden="true"></i> {{ "goPremium" | i18n }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/subscription/premium">
|
||||
{{ "goPremium" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #attachments></ng-template>
|
||||
|
||||
@@ -35,9 +35,6 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -47,7 +44,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
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 { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
@@ -122,10 +118,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
|
||||
collectionsModalRef: ViewContainerRef;
|
||||
|
||||
showVerifyEmail = false;
|
||||
showBrowserOutdated = false;
|
||||
showPremiumCallout = false;
|
||||
showLowKdf = false;
|
||||
trashCleanupWarning: string = null;
|
||||
kdfIterations: number;
|
||||
activeFilter: VaultFilter = new VaultFilter();
|
||||
@@ -161,7 +153,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private i18nService: I18nService,
|
||||
private modalService: ModalService,
|
||||
private dialogService: DialogService,
|
||||
private tokenService: TokenService,
|
||||
private messagingService: MessagingService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
@@ -180,14 +171,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private searchPipe: SearchPipe,
|
||||
private configService: ConfigService,
|
||||
private apiService: ApiService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private toastService: ToastService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.showBrowserOutdated = window.navigator.userAgent.indexOf("MSIE") !== -1;
|
||||
this.trashCleanupWarning = this.i18nService.t(
|
||||
this.platformUtilsService.isSelfHost()
|
||||
? "trashCleanupWarningSelfHosted"
|
||||
@@ -197,18 +185,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
const firstSetup$ = this.route.queryParams.pipe(
|
||||
first(),
|
||||
switchMap(async (params: Params) => {
|
||||
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
|
||||
this.showLowKdf = (await this.userVerificationService.hasMasterPassword())
|
||||
? await this.isLowKdfIteration()
|
||||
: false;
|
||||
await this.syncService.fullSync(false);
|
||||
|
||||
const canAccessPremium = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
|
||||
);
|
||||
this.showPremiumCallout =
|
||||
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
||||
|
||||
const cipherId = getCipherIdFromParams(params);
|
||||
if (!cipherId) {
|
||||
return;
|
||||
@@ -412,16 +390,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
get isShowingCards() {
|
||||
return (
|
||||
this.showBrowserOutdated || this.showPremiumCallout || this.showVerifyEmail || this.showLowKdf
|
||||
);
|
||||
}
|
||||
|
||||
emailVerified(verified: boolean) {
|
||||
this.showVerifyEmail = !verified;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
this.destroy$.next();
|
||||
@@ -1005,14 +973,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
: this.cipherService.softDeleteWithServer(id);
|
||||
}
|
||||
|
||||
async isLowKdfIteration() {
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
return (
|
||||
kdfConfig.kdfType === KdfType.PBKDF2_SHA256 &&
|
||||
kdfConfig.iterations < PBKDF2_ITERATIONS.defaultValue
|
||||
);
|
||||
}
|
||||
|
||||
protected async repromptCipher(ciphers: CipherView[]) {
|
||||
const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None);
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { BreadcrumbsModule } from "@bitwarden/components";
|
||||
import { BannerModule, BreadcrumbsModule } from "@bitwarden/components";
|
||||
|
||||
import { VerifyEmailComponent } from "../../auth/settings/verify-email.component";
|
||||
import { LooseComponentsModule, SharedModule } from "../../shared";
|
||||
import { CollectionDialogModule } from "../components/collection-dialog";
|
||||
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
||||
@@ -11,6 +12,8 @@ import { GroupBadgeModule } from "../org-vault/group-badge/group-badge.module";
|
||||
import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module";
|
||||
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "./pipes/pipes.module";
|
||||
import { VaultBannersService } from "./vault-banners/services/vault-banners.service";
|
||||
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./vault-onboarding/services/abstraction/vault-onboarding.service";
|
||||
@@ -34,10 +37,13 @@ import { VaultComponent } from "./vault.component";
|
||||
VaultItemsModule,
|
||||
CollectionDialogModule,
|
||||
VaultOnboardingComponent,
|
||||
BannerModule,
|
||||
VerifyEmailComponent,
|
||||
],
|
||||
declarations: [VaultComponent, VaultHeaderComponent],
|
||||
declarations: [VaultComponent, VaultHeaderComponent, VaultBannersComponent],
|
||||
exports: [VaultComponent],
|
||||
providers: [
|
||||
VaultBannersService,
|
||||
{
|
||||
provide: VaultOnboardingServiceAbstraction,
|
||||
useClass: VaultOnboardingService,
|
||||
|
||||
@@ -1,38 +1,19 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="purgeVaultTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="purgeVaultTitle">{{ "purgeVault" | i18n }}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ (organizationId ? "purgeOrgVaultDesc" : "purgeVaultDesc") | i18n }}</p>
|
||||
<app-callout type="warning">{{ "purgeVaultWarning" | i18n }}</app-callout>
|
||||
<app-user-verification [(ngModel)]="masterPassword" ngDefaultControl name="secret">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "purgeVault" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="default" [title]="'purgeVault' | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<p bitTypography="body1">
|
||||
{{ (organizationId ? "purgeOrgVaultDesc" : "purgeVaultDesc") | i18n }}
|
||||
</p>
|
||||
<app-callout type="warning">{{ "purgeVaultWarning" | i18n }}</app-callout>
|
||||
<app-user-verification formControlName="masterPassword"></app-user-verification>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton type="submit" buttonType="danger">
|
||||
{{ "purgeVault" | i18n }}
|
||||
</button>
|
||||
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -1,55 +1,60 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export interface PurgeVaultDialogData {
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-purge-vault",
|
||||
templateUrl: "purge-vault.component.html",
|
||||
})
|
||||
export class PurgeVaultComponent {
|
||||
@Input() organizationId?: string = null;
|
||||
organizationId: string = null;
|
||||
|
||||
masterPassword: Verification;
|
||||
formPromise: Promise<unknown>;
|
||||
formGroup = new FormGroup({
|
||||
masterPassword: new FormControl<Verification>(null),
|
||||
});
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: PurgeVaultDialogData,
|
||||
private dialogRef: DialogRef,
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private router: Router,
|
||||
private logService: LogService,
|
||||
private syncService: SyncService,
|
||||
) {}
|
||||
) {
|
||||
this.organizationId = data && data.organizationId ? data.organizationId : null;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.masterPassword)
|
||||
.then((request) => this.apiService.postPurgeCiphers(request, this.organizationId));
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("vaultPurged"));
|
||||
// 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);
|
||||
if (this.organizationId != 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(["organizations", this.organizationId, "vault"]);
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["vault"]);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
submit = async () => {
|
||||
const response = this.userVerificationService
|
||||
.buildRequest(this.formGroup.value.masterPassword)
|
||||
.then((request) => this.apiService.postPurgeCiphers(request, this.organizationId));
|
||||
await response;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("vaultPurged"));
|
||||
await this.syncService.fullSync(true);
|
||||
if (this.organizationId != null) {
|
||||
await this.router.navigate(["organizations", this.organizationId, "vault"]);
|
||||
} else {
|
||||
await this.router.navigate(["vault"]);
|
||||
}
|
||||
this.dialogRef.close();
|
||||
};
|
||||
|
||||
static open(dialogService: DialogService, config?: DialogConfig<PurgeVaultDialogData>) {
|
||||
return dialogService.open(PurgeVaultComponent, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8223,6 +8223,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lowKDFIterationsBanner": {
|
||||
"message": "Low KDF iterations. Increase your iterations to improve the security of your account."
|
||||
},
|
||||
"changeKDFSettings": {
|
||||
"message": "Change KDF settings"
|
||||
},
|
||||
"secureYourInfrastructure": {
|
||||
"message": "Secure your infrastructure"
|
||||
},
|
||||
|
||||
@@ -193,13 +193,11 @@ import { SearchService } from "@bitwarden/common/services/search.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||
import {
|
||||
PasswordGenerationService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/password";
|
||||
import {
|
||||
UsernameGenerationService,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/username";
|
||||
legacyPasswordGenerationServiceFactory,
|
||||
legacyUsernameGenerationServiceFactory,
|
||||
} from "@bitwarden/common/tools/generator";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
@@ -559,13 +557,27 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
useClass: PasswordGenerationService,
|
||||
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
|
||||
useFactory: legacyPasswordGenerationServiceFactory,
|
||||
deps: [
|
||||
EncryptService,
|
||||
CryptoServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
StateProvider,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: UsernameGenerationServiceAbstraction,
|
||||
useClass: UsernameGenerationService,
|
||||
deps: [CryptoServiceAbstraction, StateServiceAbstraction, ApiServiceAbstraction],
|
||||
useFactory: legacyUsernameGenerationServiceFactory,
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
EncryptService,
|
||||
PolicyServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
StateProvider,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ApiServiceAbstraction,
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Directive, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
import { debounceTime, first, map } from "rxjs/operators";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, Subject } from "rxjs";
|
||||
import { debounceTime, first, map, skipWhile, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { GeneratorOptions } from "@bitwarden/common/tools/generator/generator-options";
|
||||
import { GeneratorType } from "@bitwarden/common/tools/generator/generator-type";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PasswordGeneratorOptions,
|
||||
@@ -22,9 +21,9 @@ import {
|
||||
import { EmailForwarderOptions } from "@bitwarden/common/tools/models/domain/email-forwarder-options";
|
||||
|
||||
@Directive()
|
||||
export class GeneratorComponent implements OnInit {
|
||||
export class GeneratorComponent implements OnInit, OnDestroy {
|
||||
@Input() comingFromAddEdit = false;
|
||||
@Input() type: string;
|
||||
@Input() type: GeneratorType | "";
|
||||
@Output() onSelected = new EventEmitter<string>();
|
||||
|
||||
usernameGeneratingPromise: Promise<string>;
|
||||
@@ -43,6 +42,9 @@ export class GeneratorComponent implements OnInit {
|
||||
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
||||
usernameWebsite: string = null;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private isInitialized$ = new BehaviorSubject(false);
|
||||
|
||||
// update screen reader minimum password length with 500ms debounce
|
||||
// so that the user isn't flooded with status updates
|
||||
private _passwordOptionsMinLengthForReader = new BehaviorSubject<number>(
|
||||
@@ -53,15 +55,17 @@ export class GeneratorComponent implements OnInit {
|
||||
debounceTime(500),
|
||||
);
|
||||
|
||||
private _password = new BehaviorSubject<string>("-");
|
||||
|
||||
constructor(
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected stateService: StateService,
|
||||
protected accountService: AccountService,
|
||||
protected i18nService: I18nService,
|
||||
protected logService: LogService,
|
||||
protected route: ActivatedRoute,
|
||||
protected accountService: AccountService,
|
||||
protected ngZone: NgZone,
|
||||
private win: Window,
|
||||
) {
|
||||
this.typeOptions = [
|
||||
@@ -92,61 +96,115 @@ export class GeneratorComponent implements OnInit {
|
||||
];
|
||||
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||
this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||
// 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.initForwardOptions();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
const passwordOptionsResponse = await this.passwordGenerationService.getOptions();
|
||||
this.passwordOptions = passwordOptionsResponse[0];
|
||||
this.enforcedPasswordPolicyOptions = passwordOptionsResponse[1];
|
||||
this.avoidAmbiguous = !this.passwordOptions.ambiguous;
|
||||
this.passwordOptions.type =
|
||||
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
|
||||
this.forwardOptions = [
|
||||
{ name: "", value: "", validForSelfHosted: false },
|
||||
{ name: "addy.io", value: "anonaddy", validForSelfHosted: true },
|
||||
{ name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false },
|
||||
{ name: "Fastmail", value: "fastmail", validForSelfHosted: true },
|
||||
{ name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },
|
||||
{ name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true },
|
||||
{ name: "Forward Email", value: "forwardemail", validForSelfHosted: true },
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
this.usernameOptions = await this.usernameGenerationService.getOptions();
|
||||
if (this.usernameOptions.type == null) {
|
||||
this.usernameOptions.type = "word";
|
||||
}
|
||||
if (
|
||||
this.usernameOptions.subaddressEmail == null ||
|
||||
this.usernameOptions.subaddressEmail === ""
|
||||
) {
|
||||
this.usernameOptions.subaddressEmail = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
}
|
||||
if (this.usernameWebsite == null) {
|
||||
this.usernameOptions.subaddressType = this.usernameOptions.catchallType = "random";
|
||||
} else {
|
||||
this.usernameOptions.website = this.usernameWebsite;
|
||||
const websiteOption = { name: this.i18nService.t("websiteName"), value: "website-name" };
|
||||
this.subaddressOptions.push(websiteOption);
|
||||
this.catchallOptions.push(websiteOption);
|
||||
}
|
||||
|
||||
if (this.type !== "username" && this.type !== "password") {
|
||||
if (qParams.type === "username" || qParams.type === "password") {
|
||||
this.type = qParams.type;
|
||||
} else {
|
||||
const generatorOptions = await this.stateService.getGeneratorOptions();
|
||||
this.type = generatorOptions?.type ?? "password";
|
||||
}
|
||||
}
|
||||
if (this.regenerateWithoutButtonPress()) {
|
||||
await this.regenerate();
|
||||
}
|
||||
this._password.pipe(debounceTime(250)).subscribe((password) => {
|
||||
ngZone.run(() => {
|
||||
this.password = password;
|
||||
});
|
||||
this.passwordGenerationService.addHistory(this.password).catch((e) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async typeChanged() {
|
||||
await this.stateService.setGeneratorOptions({ type: this.type } as GeneratorOptions);
|
||||
if (this.regenerateWithoutButtonPress()) {
|
||||
await this.regenerate();
|
||||
cascadeOptions(navigationType: GeneratorType = undefined, accountEmail: string) {
|
||||
this.avoidAmbiguous = !this.passwordOptions.ambiguous;
|
||||
|
||||
if (!this.type) {
|
||||
if (navigationType) {
|
||||
this.type = navigationType;
|
||||
} else {
|
||||
this.type = this.passwordOptions.type === "username" ? "username" : "password";
|
||||
}
|
||||
}
|
||||
|
||||
this.passwordOptions.type =
|
||||
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
|
||||
|
||||
if (this.usernameOptions.type == null) {
|
||||
this.usernameOptions.type = "word";
|
||||
}
|
||||
if (
|
||||
this.usernameOptions.subaddressEmail == null ||
|
||||
this.usernameOptions.subaddressEmail === ""
|
||||
) {
|
||||
this.usernameOptions.subaddressEmail = accountEmail;
|
||||
}
|
||||
if (this.usernameWebsite == null) {
|
||||
this.usernameOptions.subaddressType = this.usernameOptions.catchallType = "random";
|
||||
} else {
|
||||
this.usernameOptions.website = this.usernameWebsite;
|
||||
const websiteOption = { name: this.i18nService.t("websiteName"), value: "website-name" };
|
||||
this.subaddressOptions.push(websiteOption);
|
||||
this.catchallOptions.push(websiteOption);
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
combineLatest([
|
||||
this.route.queryParams.pipe(first()),
|
||||
this.accountService.activeAccount$.pipe(first()),
|
||||
this.passwordGenerationService.getOptions$(),
|
||||
this.usernameGenerationService.getOptions$(),
|
||||
])
|
||||
.pipe(
|
||||
map(([qParams, account, [passwordOptions, passwordPolicy], usernameOptions]) => ({
|
||||
navigationType: qParams.type as GeneratorType,
|
||||
accountEmail: account.email,
|
||||
passwordOptions,
|
||||
passwordPolicy,
|
||||
usernameOptions,
|
||||
})),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((options) => {
|
||||
this.passwordOptions = options.passwordOptions;
|
||||
this.enforcedPasswordPolicyOptions = options.passwordPolicy;
|
||||
this.usernameOptions = options.usernameOptions;
|
||||
|
||||
this.cascadeOptions(options.navigationType, options.accountEmail);
|
||||
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
|
||||
|
||||
if (this.regenerateWithoutButtonPress()) {
|
||||
this.regenerate().catch((e) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
this.isInitialized$.next(true);
|
||||
});
|
||||
|
||||
// once initialization is complete, `ngOnInit` should return.
|
||||
//
|
||||
// FIXME(#6944): if a sync is in progress, wait to complete until after
|
||||
// the sync completes.
|
||||
await firstValueFrom(
|
||||
this.isInitialized$.pipe(
|
||||
skipWhile((initialized) => !initialized),
|
||||
takeUntil(this.destroy$),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.isInitialized$.complete();
|
||||
this._passwordOptionsMinLengthForReader.complete();
|
||||
}
|
||||
|
||||
async typeChanged() {
|
||||
await this.savePasswordOptions();
|
||||
}
|
||||
|
||||
async regenerate() {
|
||||
@@ -160,7 +218,7 @@ export class GeneratorComponent implements OnInit {
|
||||
async sliderChanged() {
|
||||
// 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.savePasswordOptions(false);
|
||||
this.savePasswordOptions();
|
||||
await this.passwordGenerationService.addHistory(this.password);
|
||||
}
|
||||
|
||||
@@ -204,31 +262,34 @@ export class GeneratorComponent implements OnInit {
|
||||
|
||||
async sliderInput() {
|
||||
await this.normalizePasswordOptions();
|
||||
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
||||
}
|
||||
|
||||
async savePasswordOptions(regenerate = true) {
|
||||
async savePasswordOptions() {
|
||||
// map navigation state into generator type
|
||||
const restoreType = this.passwordOptions.type;
|
||||
if (this.type === "username") {
|
||||
this.passwordOptions.type = this.type;
|
||||
}
|
||||
|
||||
// save options
|
||||
await this.normalizePasswordOptions();
|
||||
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
||||
|
||||
if (regenerate && this.regenerateWithoutButtonPress()) {
|
||||
await this.regeneratePassword();
|
||||
}
|
||||
// restore the original format
|
||||
this.passwordOptions.type = restoreType;
|
||||
}
|
||||
|
||||
async saveUsernameOptions(regenerate = true) {
|
||||
async saveUsernameOptions() {
|
||||
await this.usernameGenerationService.saveOptions(this.usernameOptions);
|
||||
if (this.usernameOptions.type === "forwarded") {
|
||||
this.username = "-";
|
||||
}
|
||||
if (regenerate && this.regenerateWithoutButtonPress()) {
|
||||
await this.regenerateUsername();
|
||||
}
|
||||
}
|
||||
|
||||
async regeneratePassword() {
|
||||
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
||||
await this.passwordGenerationService.addHistory(this.password);
|
||||
this._password.next(
|
||||
await this.passwordGenerationService.generatePassword(this.passwordOptions),
|
||||
);
|
||||
}
|
||||
|
||||
regenerateUsername() {
|
||||
@@ -297,28 +358,5 @@ export class GeneratorComponent implements OnInit {
|
||||
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(
|
||||
this.passwordOptions,
|
||||
);
|
||||
|
||||
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
|
||||
}
|
||||
|
||||
private async initForwardOptions() {
|
||||
this.forwardOptions = [
|
||||
{ name: "addy.io", value: "anonaddy", validForSelfHosted: true },
|
||||
{ name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false },
|
||||
{ name: "Fastmail", value: "fastmail", validForSelfHosted: true },
|
||||
{ name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },
|
||||
{ name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true },
|
||||
{ name: "Forward Email", value: "forwardemail", validForSelfHosted: true },
|
||||
];
|
||||
|
||||
this.usernameOptions = await this.usernameGenerationService.getOptions();
|
||||
if (
|
||||
this.usernameOptions.forwardedService == null ||
|
||||
this.usernameOptions.forwardedService === ""
|
||||
) {
|
||||
this.forwardOptions.push({ name: "", value: null, validForSelfHosted: false });
|
||||
}
|
||||
|
||||
this.forwardOptions = this.forwardOptions.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ export class PasswordGeneratorHistoryComponent implements OnInit {
|
||||
}
|
||||
|
||||
clear = async () => {
|
||||
this.history = [];
|
||||
await this.passwordGenerationService.clear();
|
||||
this.history = await this.passwordGenerationService.clear();
|
||||
};
|
||||
|
||||
copy(password: string) {
|
||||
|
||||
@@ -5,6 +5,12 @@ import { AnonLayoutComponent } from "@bitwarden/auth/angular";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Icon } from "@bitwarden/components";
|
||||
|
||||
export interface AnonLayoutWrapperData {
|
||||
pageTitle?: string;
|
||||
pageSubtitle?: string;
|
||||
pageIcon?: Icon;
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "anon-layout-wrapper.component.html",
|
||||
@@ -13,7 +13,7 @@
|
||||
</h1>
|
||||
<p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p>
|
||||
</div>
|
||||
<div class="tw-mb-auto tw-mx-auto tw-flex tw-flex-col tw-items-center">
|
||||
<div class="tw-mb-auto tw-max-w-md tw-mx-auto tw-flex tw-flex-col tw-items-center">
|
||||
<div
|
||||
class="tw-rounded-xl tw-mb-9 tw-mx-auto sm:tw-bg-background sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"
|
||||
>
|
||||
|
||||
@@ -31,14 +31,13 @@ writing:
|
||||
Instead the AnonLayoutComponent is implemented solely in the router via routable composition, which
|
||||
gives us the advantages of nested routes in Angular.
|
||||
|
||||
To allow for routable composition, Auth will also provide a wrapper component in each client, called
|
||||
AnonLayout**Wrapper**Component.
|
||||
To allow for routable composition, Auth also provides an AnonLayout**Wrapper**Component which embeds
|
||||
the AnonLayoutComponent.
|
||||
|
||||
For clarity:
|
||||
|
||||
- AnonLayoutComponent = the Auth-owned library component - `<auth-anon-layout>`
|
||||
- AnonLayout**Wrapper**Component = the client-specific wrapper component to be used in a client
|
||||
routing module
|
||||
- AnonLayoutComponent = the base, Auth-owned library component - `<auth-anon-layout>`
|
||||
- AnonLayout**Wrapper**Component = the wrapper to be used in client routing modules
|
||||
|
||||
The AnonLayout**Wrapper**Component embeds the AnonLayoutComponent along with the router outlets:
|
||||
|
||||
@@ -79,7 +78,7 @@ example) to construct the page via routable composition:
|
||||
pageTitle: "logIn", // example of a translation key from messages.json
|
||||
pageSubtitle: "loginWithMasterPassword", // example of a translation key from messages.json
|
||||
pageIcon: LockIcon, // example of an icon to pass in
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -99,7 +98,7 @@ In the `oss-routing.module.ts` example above, notice the data properties being p
|
||||
All 3 of these properties are optional.
|
||||
|
||||
```javascript
|
||||
import { LockIcon } from "@bitwarden/auth/angular";
|
||||
import { AnonLayoutWrapperData, LockIcon } from "@bitwarden/auth/angular";
|
||||
|
||||
// ...
|
||||
|
||||
@@ -109,7 +108,7 @@ import { LockIcon } from "@bitwarden/auth/angular";
|
||||
pageTitle: "logIn",
|
||||
pageSubtitle: "loginWithMasterPassword",
|
||||
pageIcon: LockIcon,
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export const WithPrimaryContent: Story = {
|
||||
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
|
||||
`
|
||||
<auth-anon-layout [title]="title" [subtitle]="subtitle">
|
||||
<div class="tw-max-w-md">
|
||||
<div>
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
||||
</div>
|
||||
@@ -58,12 +58,12 @@ export const WithSecondaryContent: Story = {
|
||||
// Notice that slot="secondary" is requred to project any secondary content.
|
||||
`
|
||||
<auth-anon-layout [title]="title" [subtitle]="subtitle">
|
||||
<div class="tw-max-w-md">
|
||||
<div>
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
||||
</div>
|
||||
|
||||
<div slot="secondary" class="text-center tw-max-w-md">
|
||||
<div slot="secondary" class="text-center">
|
||||
<div class="tw-font-bold tw-mb-2">Secondary Projected Content (optional)</div>
|
||||
<button bitButton>Perform Action</button>
|
||||
</div>
|
||||
@@ -79,12 +79,12 @@ export const WithLongContent: Story = {
|
||||
// Projected content (the <div>'s) and styling is just a sample and can be replaced with any content/styling.
|
||||
`
|
||||
<auth-anon-layout title="Page Title lorem ipsum dolor consectetur sit amet expedita quod est" subtitle="Subtitle here Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?">
|
||||
<div class="tw-max-w-md">
|
||||
<div>
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.</div>
|
||||
</div>
|
||||
|
||||
<div slot="secondary" class="text-center tw-max-w-md">
|
||||
<div slot="secondary" class="text-center">
|
||||
<div class="tw-font-bold tw-mb-2">Secondary Projected Content (optional)</div>
|
||||
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Expedita, quod est? </p>
|
||||
<button bitButton>Perform Action</button>
|
||||
@@ -101,7 +101,7 @@ export const WithIcon: Story = {
|
||||
// Projected content (the <div>) and styling is just a sample and can be replaced with any content/styling.
|
||||
`
|
||||
<auth-anon-layout [title]="title" [subtitle]="subtitle" [icon]="icon">
|
||||
<div class="tw-max-w-md">
|
||||
<div>
|
||||
<div class="tw-font-bold">Primary Projected Content Area (customizable)</div>
|
||||
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export * from "./icons";
|
||||
|
||||
export * from "./anon-layout/anon-layout.component";
|
||||
export * from "./anon-layout/anon-layout-wrapper.component";
|
||||
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
||||
export * from "./password-callout/password-callout.component";
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
|
||||
authRequestLoginStrategy = new AuthRequestLoginStrategy(
|
||||
cache,
|
||||
deviceTrustService,
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
@@ -109,7 +110,6 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptions,
|
||||
deviceTrustService,
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
@@ -51,40 +36,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||
|
||||
constructor(
|
||||
data: AuthRequestLoginStrategyData,
|
||||
accountService: AccountService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
appIdService: AppIdService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
);
|
||||
super(...sharedDeps);
|
||||
|
||||
this.cache = new BehaviorSubject(data);
|
||||
this.email$ = this.cache.pipe(map((data) => data.tokenRequest.email));
|
||||
|
||||
@@ -150,6 +150,9 @@ describe("LoginStrategy", () => {
|
||||
// The base class is abstract so we test it via PasswordLoginStrategy
|
||||
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||
cache,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
loginStrategyService,
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
@@ -162,9 +165,6 @@ describe("LoginStrategy", () => {
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
loginStrategyService,
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
@@ -461,6 +461,9 @@ describe("LoginStrategy", () => {
|
||||
|
||||
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||
cache,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
loginStrategyService,
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
@@ -473,9 +476,6 @@ describe("LoginStrategy", () => {
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
loginStrategyService,
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
|
||||
@@ -121,6 +121,9 @@ describe("PasswordLoginStrategy", () => {
|
||||
|
||||
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||
cache,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
loginStrategyService,
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
@@ -133,9 +136,6 @@ describe("PasswordLoginStrategy", () => {
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
passwordStrengthService,
|
||||
policyService,
|
||||
loginStrategyService,
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
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 { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
|
||||
@@ -17,13 +10,6 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
|
||||
import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
@@ -31,7 +17,6 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
@@ -75,42 +60,12 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
|
||||
constructor(
|
||||
data: PasswordLoginStrategyData,
|
||||
accountService: AccountService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
appIdService: AppIdService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
logService: LogService,
|
||||
protected stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
);
|
||||
super(...sharedDeps);
|
||||
|
||||
this.cache = new BehaviorSubject(data);
|
||||
this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email));
|
||||
|
||||
@@ -118,6 +118,10 @@ describe("SsoLoginStrategy", () => {
|
||||
|
||||
ssoLoginStrategy = new SsoLoginStrategy(
|
||||
null,
|
||||
keyConnectorService,
|
||||
deviceTrustService,
|
||||
authRequestService,
|
||||
i18nService,
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
@@ -130,10 +134,6 @@ describe("SsoLoginStrategy", () => {
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
keyConnectorService,
|
||||
deviceTrustService,
|
||||
authRequestService,
|
||||
i18nService,
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
|
||||
@@ -1,36 +1,19 @@
|
||||
import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
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 { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
AuthRequestServiceAbstraction,
|
||||
} from "../abstractions";
|
||||
import { AuthRequestServiceAbstraction } from "../abstractions";
|
||||
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
@@ -84,43 +67,13 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
|
||||
constructor(
|
||||
data: SsoLoginStrategyData,
|
||||
accountService: AccountService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
appIdService: AppIdService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private i18nService: I18nService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
);
|
||||
super(...sharedDeps);
|
||||
|
||||
this.cache = new BehaviorSubject(data);
|
||||
this.email$ = this.cache.pipe(map((state) => state.email));
|
||||
|
||||
@@ -94,6 +94,8 @@ describe("UserApiLoginStrategy", () => {
|
||||
|
||||
apiLogInStrategy = new UserApiLoginStrategy(
|
||||
cache,
|
||||
environmentService,
|
||||
keyConnectorService,
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
@@ -106,8 +108,6 @@ describe("UserApiLoginStrategy", () => {
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
environmentService,
|
||||
keyConnectorService,
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import { firstValueFrom, BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
||||
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
@@ -44,41 +29,12 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||
|
||||
constructor(
|
||||
data: UserApiLoginStrategyData,
|
||||
accountService: AccountService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
appIdService: AppIdService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private environmentService: EnvironmentService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
);
|
||||
super(...sharedDeps);
|
||||
|
||||
this.cache = new BehaviorSubject(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions";
|
||||
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
|
||||
@@ -46,39 +31,9 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||
|
||||
constructor(
|
||||
data: WebAuthnLoginStrategyData,
|
||||
accountService: AccountService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
appIdService: AppIdService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
userDecryptionOptionsService,
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
);
|
||||
super(...sharedDeps);
|
||||
|
||||
this.cache = new BehaviorSubject(data);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
|
||||
import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy";
|
||||
import { LoginStrategy } from "../../login-strategies/login.strategy";
|
||||
import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy";
|
||||
import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy";
|
||||
import { UserApiLoginStrategy } from "../../login-strategies/user-api-login.strategy";
|
||||
@@ -338,6 +339,24 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
private initializeLoginStrategy(
|
||||
source: Observable<[AuthenticationType | null, CacheData | null]>,
|
||||
) {
|
||||
const sharedDeps: ConstructorParameters<typeof LoginStrategy> = [
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.billingAccountProfileStateService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.kdfConfigService,
|
||||
];
|
||||
|
||||
return source.pipe(
|
||||
map(([strategy, data]) => {
|
||||
if (strategy == null) {
|
||||
@@ -347,108 +366,35 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
case AuthenticationType.Password:
|
||||
return new PasswordLoginStrategy(
|
||||
data?.password,
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.passwordStrengthService,
|
||||
this.policyService,
|
||||
this,
|
||||
this.billingAccountProfileStateService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.kdfConfigService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.Sso:
|
||||
return new SsoLoginStrategy(
|
||||
data?.sso,
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.keyConnectorService,
|
||||
this.deviceTrustService,
|
||||
this.authRequestService,
|
||||
this.i18nService,
|
||||
this.billingAccountProfileStateService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.kdfConfigService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.UserApiKey:
|
||||
return new UserApiLoginStrategy(
|
||||
data?.userApiKey,
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.environmentService,
|
||||
this.keyConnectorService,
|
||||
this.billingAccountProfileStateService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.kdfConfigService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.AuthRequest:
|
||||
return new AuthRequestLoginStrategy(
|
||||
data?.authRequest,
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.deviceTrustService,
|
||||
this.billingAccountProfileStateService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.kdfConfigService,
|
||||
...sharedDeps,
|
||||
);
|
||||
case AuthenticationType.WebAuthn:
|
||||
return new WebAuthnLoginStrategy(
|
||||
data?.webAuthn,
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.billingAccountProfileStateService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
return new WebAuthnLoginStrategy(data?.webAuthn, ...sharedDeps);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -70,7 +70,7 @@ export class PasswordGeneratorPolicyOptions extends Domain {
|
||||
*/
|
||||
inEffect() {
|
||||
return (
|
||||
this.defaultType !== "" ||
|
||||
this.defaultType ||
|
||||
this.minLength > 0 ||
|
||||
this.numberCount > 0 ||
|
||||
this.specialCount > 0 ||
|
||||
|
||||
@@ -174,7 +174,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
|
||||
return await this.validateUserKey(masterKey as unknown as UserKey);
|
||||
return await this.validateUserKey(masterKey as unknown as UserKey, userId);
|
||||
}
|
||||
|
||||
// TODO: legacy support for user key is no longer needed since we require users to migrate on login
|
||||
@@ -193,9 +193,10 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise<UserKey> {
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
const userKey = await this.getKeyFromStorage(keySuffix, userId);
|
||||
if (userKey) {
|
||||
if (!(await this.validateUserKey(userKey))) {
|
||||
if (!(await this.validateUserKey(userKey, userId))) {
|
||||
this.logService.warning("Invalid key, throwing away stored keys");
|
||||
await this.clearAllStoredUserKeys(userId);
|
||||
}
|
||||
@@ -663,13 +664,15 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||
}
|
||||
|
||||
// ---HELPERS---
|
||||
protected async validateUserKey(key: UserKey): Promise<boolean> {
|
||||
protected async validateUserKey(key: UserKey, userId: UserId): Promise<boolean> {
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const encPrivateKey = await firstValueFrom(this.activeUserEncryptedPrivateKeyState.state$);
|
||||
const encPrivateKey = await firstValueFrom(
|
||||
this.stateProvider.getUserState$(USER_ENCRYPTED_PRIVATE_KEY, userId),
|
||||
);
|
||||
if (encPrivateKey == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -160,3 +160,7 @@ export const CIPHERS_DISK_LOCAL = new StateDefinition("ciphersLocal", "disk", {
|
||||
export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory", {
|
||||
browser: "memory-large-object",
|
||||
});
|
||||
export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerReprompt", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk");
|
||||
|
||||
@@ -60,13 +60,16 @@ import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key
|
||||
import { KnownAccountsMigrator } from "./migrations/60-known-accounts";
|
||||
import { PinStateMigrator } from "./migrations/61-move-pin-state-to-providers";
|
||||
import { VaultTimeoutSettingsServiceStateProviderMigrator } from "./migrations/62-migrate-vault-timeout-settings-svc-to-state-provider";
|
||||
import { PasswordOptionsMigrator } from "./migrations/63-migrate-password-settings";
|
||||
import { GeneratorHistoryMigrator } from "./migrations/64-migrate-generator-history";
|
||||
import { ForwarderOptionsMigrator } from "./migrations/65-migrate-forwarder-settings";
|
||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 62;
|
||||
export const CURRENT_VERSION = 65;
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
export function createMigrationBuilder() {
|
||||
@@ -130,7 +133,10 @@ export function createMigrationBuilder() {
|
||||
.with(KdfConfigMigrator, 58, 59)
|
||||
.with(KnownAccountsMigrator, 59, 60)
|
||||
.with(PinStateMigrator, 60, 61)
|
||||
.with(VaultTimeoutSettingsServiceStateProviderMigrator, 61, CURRENT_VERSION);
|
||||
.with(VaultTimeoutSettingsServiceStateProviderMigrator, 61, 62)
|
||||
.with(PasswordOptionsMigrator, 62, 63)
|
||||
.with(GeneratorHistoryMigrator, 63, 64)
|
||||
.with(ForwarderOptionsMigrator, 64, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
// Represents data in state service pre-migration
|
||||
function preMigrationJson() {
|
||||
return {
|
||||
// desktop only global data format
|
||||
"global.vaultTimeout": -1,
|
||||
"global.vaultTimeoutAction": "lock",
|
||||
|
||||
global: {
|
||||
vaultTimeout: 30,
|
||||
vaultTimeoutAction: "lock",
|
||||
@@ -267,6 +271,10 @@ describe("VaultTimeoutSettingsServiceStateProviderMigrator", () => {
|
||||
otherStuff: "otherStuff",
|
||||
});
|
||||
|
||||
// Expect we removed desktop specially formatted global data
|
||||
expect(helper.remove).toHaveBeenCalledWith("global\\.vaultTimeout");
|
||||
expect(helper.remove).toHaveBeenCalledWith("global\\.vaultTimeoutAction");
|
||||
|
||||
// User data
|
||||
expect(helper.set).toHaveBeenCalledWith("user1", {
|
||||
settings: {
|
||||
|
||||
@@ -122,10 +122,15 @@ export class VaultTimeoutSettingsServiceStateProviderMigrator extends Migrator<6
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
|
||||
// Delete global data
|
||||
// Delete global data (works for browser extension and web; CLI doesn't have these as global settings).
|
||||
delete globalData?.vaultTimeout;
|
||||
delete globalData?.vaultTimeoutAction;
|
||||
await helper.set("global", globalData);
|
||||
|
||||
// Remove desktop only settings. These aren't found by the above global key removal b/c of
|
||||
// the different storage key format. This removal does not cause any issues on migrating for other clients.
|
||||
await helper.remove("global\\.vaultTimeout");
|
||||
await helper.remove("global\\.vaultTimeoutAction");
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
ExpectedOptions,
|
||||
PasswordOptionsMigrator,
|
||||
NAVIGATION,
|
||||
PASSWORD,
|
||||
PASSPHRASE,
|
||||
} from "./63-migrate-password-settings";
|
||||
|
||||
function migrationHelper(passwordGenerationOptions: ExpectedOptions) {
|
||||
const helper = mockMigrationHelper(
|
||||
{
|
||||
global_account_accounts: {
|
||||
SomeAccount: {
|
||||
email: "SomeAccount",
|
||||
name: "SomeAccount",
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
SomeAccount: {
|
||||
settings: {
|
||||
passwordGenerationOptions,
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
},
|
||||
},
|
||||
62,
|
||||
);
|
||||
|
||||
return helper;
|
||||
}
|
||||
|
||||
function expectOtherSettingsRemain(helper: MigrationHelper) {
|
||||
expect(helper.set).toHaveBeenCalledWith("SomeAccount", {
|
||||
settings: {
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("PasswordOptionsMigrator", () => {
|
||||
describe("migrate", () => {
|
||||
it("migrates generator type", async () => {
|
||||
const helper = migrationHelper({
|
||||
type: "password",
|
||||
});
|
||||
helper.getFromUser.mockResolvedValue({ some: { other: "data" } });
|
||||
const migrator = new PasswordOptionsMigrator(62, 63);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", NAVIGATION, {
|
||||
type: "password",
|
||||
some: { other: "data" },
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates password settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
length: 20,
|
||||
ambiguous: true,
|
||||
uppercase: false,
|
||||
minUppercase: 4,
|
||||
lowercase: true,
|
||||
minLowercase: 3,
|
||||
number: false,
|
||||
minNumber: 2,
|
||||
special: true,
|
||||
minSpecial: 1,
|
||||
});
|
||||
const migrator = new PasswordOptionsMigrator(62, 63);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", PASSWORD, {
|
||||
length: 20,
|
||||
ambiguous: true,
|
||||
uppercase: false,
|
||||
minUppercase: 4,
|
||||
lowercase: true,
|
||||
minLowercase: 3,
|
||||
number: false,
|
||||
minNumber: 2,
|
||||
special: true,
|
||||
minSpecial: 1,
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates passphrase settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
numWords: 5,
|
||||
wordSeparator: "4",
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
});
|
||||
const migrator = new PasswordOptionsMigrator(62, 63);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", PASSPHRASE, {
|
||||
numWords: 5,
|
||||
wordSeparator: "4",
|
||||
capitalize: true,
|
||||
includeNumber: false,
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
/** settings targeted by migrator */
|
||||
export type AccountType = {
|
||||
settings?: {
|
||||
passwordGenerationOptions?: ExpectedOptions;
|
||||
};
|
||||
};
|
||||
|
||||
export type GeneratorType = "password" | "passphrase" | "username";
|
||||
|
||||
/** username generation options prior to refactoring */
|
||||
export type ExpectedOptions = {
|
||||
type?: GeneratorType;
|
||||
length?: number;
|
||||
minLength?: number;
|
||||
ambiguous?: boolean;
|
||||
uppercase?: boolean;
|
||||
minUppercase?: number;
|
||||
lowercase?: boolean;
|
||||
minLowercase?: number;
|
||||
number?: boolean;
|
||||
minNumber?: number;
|
||||
special?: boolean;
|
||||
minSpecial?: number;
|
||||
numWords?: number;
|
||||
wordSeparator?: string;
|
||||
capitalize?: boolean;
|
||||
includeNumber?: boolean;
|
||||
};
|
||||
|
||||
/** username generation options after refactoring */
|
||||
type ConvertedOptions = {
|
||||
generator: GeneratorNavigation;
|
||||
password: PasswordGenerationOptions;
|
||||
passphrase: PassphraseGenerationOptions;
|
||||
};
|
||||
|
||||
export const NAVIGATION: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "generatorSettings",
|
||||
};
|
||||
|
||||
export const PASSWORD: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "passwordGeneratorSettings",
|
||||
};
|
||||
|
||||
export const PASSPHRASE: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "passphraseGeneratorSettings",
|
||||
};
|
||||
|
||||
export type GeneratorNavigation = {
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export type PassphraseGenerationOptions = {
|
||||
numWords?: number;
|
||||
wordSeparator?: string;
|
||||
capitalize?: boolean;
|
||||
includeNumber?: boolean;
|
||||
};
|
||||
|
||||
export type PasswordGenerationOptions = {
|
||||
length?: number;
|
||||
minLength?: number;
|
||||
ambiguous?: boolean;
|
||||
uppercase?: boolean;
|
||||
minUppercase?: number;
|
||||
lowercase?: boolean;
|
||||
minLowercase?: number;
|
||||
number?: boolean;
|
||||
minNumber?: number;
|
||||
special?: boolean;
|
||||
minSpecial?: number;
|
||||
};
|
||||
|
||||
export class PasswordOptionsMigrator extends Migrator<62, 63> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<AccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: AccountType) {
|
||||
const legacyOptions = account?.settings?.passwordGenerationOptions;
|
||||
|
||||
if (legacyOptions) {
|
||||
const converted = convertSettings(legacyOptions);
|
||||
await storeSettings(helper, userId, converted);
|
||||
await deleteSettings(helper, userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
// not supported
|
||||
}
|
||||
}
|
||||
|
||||
function convertSettings(options: ExpectedOptions): ConvertedOptions {
|
||||
const password = {
|
||||
length: options.length,
|
||||
ambiguous: options.ambiguous,
|
||||
uppercase: options.uppercase,
|
||||
minUppercase: options.minUppercase,
|
||||
lowercase: options.lowercase,
|
||||
minLowercase: options.minLowercase,
|
||||
number: options.number,
|
||||
minNumber: options.minNumber,
|
||||
special: options.special,
|
||||
minSpecial: options.minSpecial,
|
||||
};
|
||||
|
||||
const generator = {
|
||||
type: options.type,
|
||||
};
|
||||
|
||||
const passphrase = {
|
||||
numWords: options.numWords,
|
||||
wordSeparator: options.wordSeparator,
|
||||
capitalize: options.capitalize,
|
||||
includeNumber: options.includeNumber,
|
||||
};
|
||||
|
||||
return { generator, password, passphrase };
|
||||
}
|
||||
|
||||
async function storeSettings(helper: MigrationHelper, userId: string, converted: ConvertedOptions) {
|
||||
const existing = (await helper.getFromUser(userId, NAVIGATION)) ?? {};
|
||||
const updated = Object.assign(existing, converted.generator);
|
||||
|
||||
await Promise.all([
|
||||
helper.setToUser(userId, NAVIGATION, updated),
|
||||
helper.setToUser(userId, PASSPHRASE, converted.passphrase),
|
||||
helper.setToUser(userId, PASSWORD, converted.password),
|
||||
]);
|
||||
}
|
||||
|
||||
async function deleteSettings(helper: MigrationHelper, userId: string, account: AccountType) {
|
||||
delete account?.settings?.passwordGenerationOptions;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
EncryptedHistory,
|
||||
GeneratorHistoryMigrator,
|
||||
HISTORY,
|
||||
} from "./64-migrate-generator-history";
|
||||
|
||||
function migrationHelper(encrypted: EncryptedHistory) {
|
||||
const helper = mockMigrationHelper(
|
||||
{
|
||||
global_account_accounts: {
|
||||
SomeAccount: {
|
||||
email: "SomeAccount",
|
||||
name: "SomeAccount",
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
SomeAccount: {
|
||||
data: {
|
||||
passwordGenerationHistory: {
|
||||
encrypted,
|
||||
},
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
},
|
||||
},
|
||||
63,
|
||||
);
|
||||
|
||||
return helper;
|
||||
}
|
||||
|
||||
function expectOtherSettingsRemain(helper: MigrationHelper) {
|
||||
expect(helper.set).toHaveBeenCalledWith("SomeAccount", {
|
||||
data: {
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("PasswordOptionsMigrator", () => {
|
||||
describe("migrate", () => {
|
||||
it("migrates generator type", async () => {
|
||||
const helper = migrationHelper([{ this: "should be copied" }, { this: "too" }]);
|
||||
const migrator = new GeneratorHistoryMigrator(63, 64);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", HISTORY, [
|
||||
{ this: "should be copied" },
|
||||
{ this: "too" },
|
||||
]);
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
/** settings targeted by migrator */
|
||||
export type AccountType = {
|
||||
data?: {
|
||||
passwordGenerationHistory?: {
|
||||
encrypted: EncryptedHistory;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/** the actual data stored in the history is opaque to the migrator */
|
||||
export type EncryptedHistory = Array<unknown>;
|
||||
|
||||
export const HISTORY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "localGeneratorHistoryBuffer",
|
||||
};
|
||||
|
||||
export class GeneratorHistoryMigrator extends Migrator<63, 64> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<AccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: AccountType) {
|
||||
const data = account?.data?.passwordGenerationHistory;
|
||||
if (data && data.encrypted) {
|
||||
await helper.setToUser(userId, HISTORY, data.encrypted);
|
||||
delete account.data.passwordGenerationHistory;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
// not supported
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import { MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import {
|
||||
ADDY_IO,
|
||||
CATCHALL,
|
||||
DUCK_DUCK_GO,
|
||||
EFF_USERNAME,
|
||||
ExpectedOptions,
|
||||
FASTMAIL,
|
||||
FIREFOX_RELAY,
|
||||
FORWARD_EMAIL,
|
||||
ForwarderOptionsMigrator,
|
||||
NAVIGATION,
|
||||
SIMPLE_LOGIN,
|
||||
SUBADDRESS,
|
||||
} from "./65-migrate-forwarder-settings";
|
||||
|
||||
function migrationHelper(usernameGenerationOptions: ExpectedOptions) {
|
||||
const helper = mockMigrationHelper(
|
||||
{
|
||||
global_account_accounts: {
|
||||
SomeAccount: {
|
||||
email: "SomeAccount",
|
||||
name: "SomeAccount",
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
SomeAccount: {
|
||||
settings: {
|
||||
usernameGenerationOptions,
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
},
|
||||
},
|
||||
64,
|
||||
);
|
||||
|
||||
return helper;
|
||||
}
|
||||
|
||||
function expectOtherSettingsRemain(helper: MigrationHelper) {
|
||||
expect(helper.set).toHaveBeenCalledWith("SomeAccount", {
|
||||
settings: {
|
||||
this: {
|
||||
looks: "important",
|
||||
},
|
||||
},
|
||||
cant: {
|
||||
touch: "this",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("ForwarderOptionsMigrator", () => {
|
||||
describe("migrate", () => {
|
||||
it("migrates generator settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
type: "catchall",
|
||||
forwardedService: "simplelogin",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", NAVIGATION, {
|
||||
username: "catchall",
|
||||
forwarder: "simplelogin",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates catchall settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
catchallType: "random",
|
||||
catchallDomain: "example.com",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", CATCHALL, {
|
||||
catchallType: "random",
|
||||
catchallDomain: "example.com",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates EFF username settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
wordCapitalize: true,
|
||||
wordIncludeNumber: false,
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", EFF_USERNAME, {
|
||||
wordCapitalize: true,
|
||||
wordIncludeNumber: false,
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates subaddress settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
subaddressType: "random",
|
||||
subaddressEmail: "j.d@example.com",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", SUBADDRESS, {
|
||||
subaddressType: "random",
|
||||
subaddressEmail: "j.d@example.com",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates addyIo settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedAnonAddyBaseUrl: "some_addyio_base",
|
||||
forwardedAnonAddyApiToken: "some_addyio_token",
|
||||
forwardedAnonAddyDomain: "some_addyio_domain",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", ADDY_IO, {
|
||||
baseUrl: "some_addyio_base",
|
||||
token: "some_addyio_token",
|
||||
domain: "some_addyio_domain",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates DuckDuckGo settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedDuckDuckGoToken: "some_duckduckgo_token",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", DUCK_DUCK_GO, {
|
||||
token: "some_duckduckgo_token",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates Firefox Relay settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedFirefoxApiToken: "some_firefox_token",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", FIREFOX_RELAY, {
|
||||
token: "some_firefox_token",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates Fastmail settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedFastmailApiToken: "some_fastmail_token",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", FASTMAIL, {
|
||||
token: "some_fastmail_token",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates ForwardEmail settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedForwardEmailApiToken: "some_forwardemail_token",
|
||||
forwardedForwardEmailDomain: "some_forwardemail_domain",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", FORWARD_EMAIL, {
|
||||
token: "some_forwardemail_token",
|
||||
domain: "some_forwardemail_domain",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
|
||||
it("migrates SimpleLogin settings", async () => {
|
||||
const helper = migrationHelper({
|
||||
forwardedSimpleLoginApiKey: "some_simplelogin_token",
|
||||
forwardedSimpleLoginBaseUrl: "some_simplelogin_baseurl",
|
||||
});
|
||||
const migrator = new ForwarderOptionsMigrator(64, 65);
|
||||
|
||||
await migrator.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("SomeAccount", SIMPLE_LOGIN, {
|
||||
token: "some_simplelogin_token",
|
||||
baseUrl: "some_simplelogin_baseurl",
|
||||
});
|
||||
expectOtherSettingsRemain(helper);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
/** settings targeted by migrator */
|
||||
export type AccountType = {
|
||||
settings?: {
|
||||
usernameGenerationOptions?: ExpectedOptions;
|
||||
};
|
||||
};
|
||||
|
||||
/** username generation options prior to refactoring */
|
||||
export type ExpectedOptions = {
|
||||
type?: "word" | "subaddress" | "catchall" | "forwarded";
|
||||
wordCapitalize?: boolean;
|
||||
wordIncludeNumber?: boolean;
|
||||
subaddressType?: "random" | "website-name";
|
||||
subaddressEmail?: string;
|
||||
catchallType?: "random" | "website-name";
|
||||
catchallDomain?: string;
|
||||
forwardedService?: string;
|
||||
forwardedAnonAddyApiToken?: string;
|
||||
forwardedAnonAddyDomain?: string;
|
||||
forwardedAnonAddyBaseUrl?: string;
|
||||
forwardedDuckDuckGoToken?: string;
|
||||
forwardedFirefoxApiToken?: string;
|
||||
forwardedFastmailApiToken?: string;
|
||||
forwardedForwardEmailApiToken?: string;
|
||||
forwardedForwardEmailDomain?: string;
|
||||
forwardedSimpleLoginApiKey?: string;
|
||||
forwardedSimpleLoginBaseUrl?: string;
|
||||
};
|
||||
|
||||
/** username generation options after refactoring */
|
||||
type ConvertedOptions = {
|
||||
generator: GeneratorNavigation;
|
||||
algorithms: {
|
||||
catchall: CatchallGenerationOptions;
|
||||
effUsername: EffUsernameGenerationOptions;
|
||||
subaddress: SubaddressGenerationOptions;
|
||||
};
|
||||
forwarders: {
|
||||
addyIo: SelfHostedApiOptions & EmailDomainOptions;
|
||||
duckDuckGo: ApiOptions;
|
||||
fastmail: ApiOptions;
|
||||
firefoxRelay: ApiOptions;
|
||||
forwardEmail: ApiOptions & EmailDomainOptions;
|
||||
simpleLogin: SelfHostedApiOptions;
|
||||
};
|
||||
};
|
||||
|
||||
export const NAVIGATION: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "generatorSettings",
|
||||
};
|
||||
|
||||
export const CATCHALL: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "catchallGeneratorSettings",
|
||||
};
|
||||
|
||||
export const EFF_USERNAME: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "effUsernameGeneratorSettings",
|
||||
};
|
||||
|
||||
export const SUBADDRESS: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "subaddressGeneratorSettings",
|
||||
};
|
||||
|
||||
export const ADDY_IO: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "addyIoBuffer",
|
||||
};
|
||||
|
||||
export const DUCK_DUCK_GO: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "duckDuckGoBuffer",
|
||||
};
|
||||
|
||||
export const FASTMAIL: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "fastmailBuffer",
|
||||
};
|
||||
|
||||
export const FIREFOX_RELAY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "firefoxRelayBuffer",
|
||||
};
|
||||
|
||||
export const FORWARD_EMAIL: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "forwardEmailBuffer",
|
||||
};
|
||||
|
||||
export const SIMPLE_LOGIN: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "generator",
|
||||
},
|
||||
key: "simpleLoginBuffer",
|
||||
};
|
||||
|
||||
export type GeneratorNavigation = {
|
||||
type?: string;
|
||||
username?: string;
|
||||
forwarder?: string;
|
||||
};
|
||||
|
||||
type UsernameGenerationMode = "random" | "website-name";
|
||||
|
||||
type CatchallGenerationOptions = {
|
||||
catchallType?: UsernameGenerationMode;
|
||||
catchallDomain?: string;
|
||||
};
|
||||
|
||||
type EffUsernameGenerationOptions = {
|
||||
wordCapitalize?: boolean;
|
||||
wordIncludeNumber?: boolean;
|
||||
};
|
||||
|
||||
type SubaddressGenerationOptions = {
|
||||
subaddressType?: UsernameGenerationMode;
|
||||
subaddressEmail?: string;
|
||||
};
|
||||
|
||||
type ApiOptions = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
type SelfHostedApiOptions = ApiOptions & {
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
type EmailDomainOptions = {
|
||||
domain: string;
|
||||
};
|
||||
|
||||
export class ForwarderOptionsMigrator extends Migrator<64, 65> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<AccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: AccountType) {
|
||||
const legacyOptions = account?.settings?.usernameGenerationOptions;
|
||||
|
||||
if (legacyOptions) {
|
||||
const converted = convertSettings(legacyOptions);
|
||||
await storeSettings(helper, userId, converted);
|
||||
await deleteSettings(helper, userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
// not supported
|
||||
}
|
||||
}
|
||||
|
||||
function convertSettings(options: ExpectedOptions): ConvertedOptions {
|
||||
const forwarders = {
|
||||
addyIo: {
|
||||
baseUrl: options.forwardedAnonAddyBaseUrl,
|
||||
token: options.forwardedAnonAddyApiToken,
|
||||
domain: options.forwardedAnonAddyDomain,
|
||||
},
|
||||
duckDuckGo: {
|
||||
token: options.forwardedDuckDuckGoToken,
|
||||
},
|
||||
fastmail: {
|
||||
token: options.forwardedFastmailApiToken,
|
||||
},
|
||||
firefoxRelay: {
|
||||
token: options.forwardedFirefoxApiToken,
|
||||
},
|
||||
forwardEmail: {
|
||||
token: options.forwardedForwardEmailApiToken,
|
||||
domain: options.forwardedForwardEmailDomain,
|
||||
},
|
||||
simpleLogin: {
|
||||
token: options.forwardedSimpleLoginApiKey,
|
||||
baseUrl: options.forwardedSimpleLoginBaseUrl,
|
||||
},
|
||||
};
|
||||
|
||||
const generator = {
|
||||
username: options.type,
|
||||
forwarder: options.forwardedService,
|
||||
};
|
||||
|
||||
const algorithms = {
|
||||
effUsername: {
|
||||
wordCapitalize: options.wordCapitalize,
|
||||
wordIncludeNumber: options.wordIncludeNumber,
|
||||
},
|
||||
subaddress: {
|
||||
subaddressType: options.subaddressType,
|
||||
subaddressEmail: options.subaddressEmail,
|
||||
},
|
||||
catchall: {
|
||||
catchallType: options.catchallType,
|
||||
catchallDomain: options.catchallDomain,
|
||||
},
|
||||
};
|
||||
|
||||
return { generator, algorithms, forwarders };
|
||||
}
|
||||
|
||||
async function storeSettings(helper: MigrationHelper, userId: string, converted: ConvertedOptions) {
|
||||
await Promise.all([
|
||||
helper.setToUser(userId, NAVIGATION, converted.generator),
|
||||
helper.setToUser(userId, CATCHALL, converted.algorithms.catchall),
|
||||
helper.setToUser(userId, EFF_USERNAME, converted.algorithms.effUsername),
|
||||
helper.setToUser(userId, SUBADDRESS, converted.algorithms.subaddress),
|
||||
helper.setToUser(userId, ADDY_IO, converted.forwarders.addyIo),
|
||||
helper.setToUser(userId, DUCK_DUCK_GO, converted.forwarders.duckDuckGo),
|
||||
helper.setToUser(userId, FASTMAIL, converted.forwarders.fastmail),
|
||||
helper.setToUser(userId, FIREFOX_RELAY, converted.forwarders.firefoxRelay),
|
||||
helper.setToUser(userId, FORWARD_EMAIL, converted.forwarders.forwardEmail),
|
||||
helper.setToUser(userId, SIMPLE_LOGIN, converted.forwarders.simpleLogin),
|
||||
]);
|
||||
}
|
||||
|
||||
async function deleteSettings(helper: MigrationHelper, userId: string, account: AccountType) {
|
||||
delete account?.settings?.usernameGenerationOptions;
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user