mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +00:00
[PM-5189] Merging main and fixing conflicts
This commit is contained in:
@@ -153,7 +153,7 @@ describe("AccountSwitcherService", () => {
|
|||||||
|
|
||||||
await selectAccountPromise;
|
await selectAccountPromise;
|
||||||
|
|
||||||
expect(accountService.switchAccount).toBeCalledWith(null);
|
expect(messagingService.send).toHaveBeenCalledWith("switchAccount", { userId: null });
|
||||||
|
|
||||||
expect(removeListenerSpy).toBeCalledTimes(1);
|
expect(removeListenerSpy).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
@@ -176,7 +176,7 @@ describe("AccountSwitcherService", () => {
|
|||||||
|
|
||||||
await selectAccountPromise;
|
await selectAccountPromise;
|
||||||
|
|
||||||
expect(accountService.switchAccount).toBeCalledWith("1");
|
expect(messagingService.send).toHaveBeenCalledWith("switchAccount", { userId: "1" });
|
||||||
expect(messagingService.send).toBeCalledWith(
|
expect(messagingService.send).toBeCalledWith(
|
||||||
"switchAccount",
|
"switchAccount",
|
||||||
matches((payload) => {
|
matches((payload) => {
|
||||||
|
|||||||
@@ -134,7 +134,6 @@ export class AccountSwitcherService {
|
|||||||
const switchAccountFinishedPromise = this.listenForSwitchAccountFinish(userId);
|
const switchAccountFinishedPromise = this.listenForSwitchAccountFinish(userId);
|
||||||
|
|
||||||
// Initiate the actions required to make account switching happen
|
// 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
|
this.messagingService.send("switchAccount", { userId }); // This message should cause switchAccountFinish to be sent
|
||||||
|
|
||||||
// Wait until we receive the switchAccountFinished message
|
// Wait until we receive the switchAccountFinished message
|
||||||
|
|||||||
@@ -172,7 +172,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
* list of ciphers if the extension is not unlocked.
|
* list of ciphers if the extension is not unlocked.
|
||||||
*/
|
*/
|
||||||
async updateOverlayCiphers() {
|
async updateOverlayCiphers() {
|
||||||
if (this.userAuthStatus !== AuthenticationStatus.Unlocked) {
|
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||||
|
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
private async getOverlayCipherData(): Promise<OverlayCipherData[]> {
|
private async getOverlayCipherData(): Promise<OverlayCipherData[]> {
|
||||||
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
||||||
const overlayCiphersArray = Array.from(this.overlayLoginCiphers);
|
const overlayCiphersArray = Array.from(this.overlayLoginCiphers);
|
||||||
const overlayCipherData = [];
|
const overlayCipherData: OverlayCipherData[] = [];
|
||||||
|
|
||||||
for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) {
|
for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) {
|
||||||
const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex];
|
const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex];
|
||||||
|
|||||||
@@ -113,9 +113,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
// Autofill user settings loaded from state can await the active account state indefinitely
|
// 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)
|
// if not guarded by an active account check (e.g. the user is logged in)
|
||||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
|
let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off;
|
||||||
let autoFillOnPageLoadIsEnabled = false;
|
let autoFillOnPageLoadIsEnabled = false;
|
||||||
const overlayVisibility = await this.getInlineMenuVisibility();
|
|
||||||
|
if (activeAccount) {
|
||||||
|
overlayVisibility = await this.getInlineMenuVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
const mainAutofillScript = overlayVisibility
|
const mainAutofillScript = overlayVisibility
|
||||||
? "bootstrap-autofill-overlay.js"
|
? "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 { SearchService } from "@bitwarden/common/services/search.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||||
import {
|
import {
|
||||||
PasswordGenerationService,
|
legacyPasswordGenerationServiceFactory,
|
||||||
PasswordGenerationServiceAbstraction,
|
legacyUsernameGenerationServiceFactory,
|
||||||
} from "@bitwarden/common/tools/generator/password";
|
} from "@bitwarden/common/tools/generator";
|
||||||
import {
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
UsernameGenerationService,
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
UsernameGenerationServiceAbstraction,
|
|
||||||
} from "@bitwarden/common/tools/generator/username";
|
|
||||||
import {
|
import {
|
||||||
PasswordStrengthService,
|
PasswordStrengthService,
|
||||||
PasswordStrengthServiceAbstraction,
|
PasswordStrengthServiceAbstraction,
|
||||||
@@ -649,10 +647,12 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.passwordStrengthService = new PasswordStrengthService();
|
this.passwordStrengthService = new PasswordStrengthService();
|
||||||
|
|
||||||
this.passwordGenerationService = new PasswordGenerationService(
|
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||||
|
this.encryptService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.policyService,
|
this.policyService,
|
||||||
this.stateService,
|
this.accountService,
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||||
@@ -1093,10 +1093,14 @@ export default class MainBackground {
|
|||||||
this.vaultTimeoutSettingsService,
|
this.vaultTimeoutSettingsService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.usernameGenerationService = new UsernameGenerationService(
|
this.usernameGenerationService = legacyUsernameGenerationServiceFactory(
|
||||||
this.cryptoService,
|
|
||||||
this.stateService,
|
|
||||||
this.apiService,
|
this.apiService,
|
||||||
|
this.i18nService,
|
||||||
|
this.cryptoService,
|
||||||
|
this.encryptService,
|
||||||
|
this.policyService,
|
||||||
|
this.accountService,
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.popupOnlyContext) {
|
if (!this.popupOnlyContext) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface OffscreenDocument {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class OffscreenDocumentService {
|
export abstract class OffscreenDocumentService {
|
||||||
|
abstract offscreenApiSupported(): boolean;
|
||||||
abstract withDocument<T>(
|
abstract withDocument<T>(
|
||||||
reasons: chrome.offscreen.Reason[],
|
reasons: chrome.offscreen.Reason[],
|
||||||
justification: string,
|
justification: string,
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ describe.each([
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("offscreenApiSupported", () => {
|
||||||
|
it("indicates whether the offscreen API is supported", () => {
|
||||||
|
expect(sut.offscreenApiSupported()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("withDocument", () => {
|
describe("withDocument", () => {
|
||||||
it("creates a document when none exists", async () => {
|
it("creates a document when none exists", async () => {
|
||||||
await sut.withDocument(reasons, justification, () => {});
|
await sut.withDocument(reasons, justification, () => {});
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
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;
|
private workerCount = 0;
|
||||||
|
|
||||||
constructor(private logService: LogService) {}
|
constructor(private logService: LogService) {}
|
||||||
|
|
||||||
|
offscreenApiSupported(): boolean {
|
||||||
|
return typeof chrome.offscreen !== "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
async withDocument<T>(
|
async withDocument<T>(
|
||||||
reasons: chrome.offscreen.Reason[],
|
reasons: chrome.offscreen.Reason[],
|
||||||
justification: string,
|
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 () => {
|
it("copies the passed text using the offscreen document if the extension is using manifest v3", async () => {
|
||||||
const text = "test";
|
const text = "test";
|
||||||
jest
|
offscreenDocumentService.offscreenApiSupported.mockReturnValue(true);
|
||||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
|
||||||
.mockReturnValue(DeviceType.ChromeExtension);
|
|
||||||
getManifestVersionSpy.mockReturnValue(3);
|
getManifestVersionSpy.mockReturnValue(3);
|
||||||
|
|
||||||
browserPlatformUtilsService.copyToClipboard(text);
|
browserPlatformUtilsService.copyToClipboard(text);
|
||||||
@@ -304,9 +302,7 @@ describe("Browser Utils Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("reads the clipboard text using the offscreen document", async () => {
|
it("reads the clipboard text using the offscreen document", async () => {
|
||||||
jest
|
offscreenDocumentService.offscreenApiSupported.mockReturnValue(true);
|
||||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
|
||||||
.mockReturnValue(DeviceType.ChromeExtension);
|
|
||||||
getManifestVersionSpy.mockReturnValue(3);
|
getManifestVersionSpy.mockReturnValue(3);
|
||||||
offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) =>
|
offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) =>
|
||||||
Promise.resolve("test"),
|
Promise.resolve("test"),
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
|||||||
text = "\u0000";
|
text = "\u0000";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isChrome() && BrowserApi.isManifestVersion(3)) {
|
if (BrowserApi.isManifestVersion(3) && this.offscreenDocumentService.offscreenApiSupported()) {
|
||||||
void this.triggerOffscreenCopyToClipboard(text).then(handleClipboardWriteCallback);
|
void this.triggerOffscreenCopyToClipboard(text).then(handleClipboardWriteCallback);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -268,7 +268,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
|||||||
return await SafariApp.sendMessageToApp("readFromClipboard");
|
return await SafariApp.sendMessageToApp("readFromClipboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isChrome() && BrowserApi.isManifestVersion(3)) {
|
if (BrowserApi.isManifestVersion(3) && this.offscreenDocumentService.offscreenApiSupported()) {
|
||||||
return await this.triggerOffscreenReadFromClipboard();
|
return await this.triggerOffscreenReadFromClipboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Location } from "@angular/common";
|
import { Location } from "@angular/common";
|
||||||
import { Component } from "@angular/core";
|
import { Component, NgZone } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -29,22 +28,22 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
|||||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
stateService: StateService,
|
accountService: AccountService,
|
||||||
cipherService: CipherService,
|
cipherService: CipherService,
|
||||||
route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
accountService: AccountService,
|
ngZone: NgZone,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
passwordGenerationService,
|
passwordGenerationService,
|
||||||
usernameGenerationService,
|
usernameGenerationService,
|
||||||
platformUtilsService,
|
platformUtilsService,
|
||||||
stateService,
|
accountService,
|
||||||
i18nService,
|
i18nService,
|
||||||
logService,
|
logService,
|
||||||
route,
|
route,
|
||||||
accountService,
|
ngZone,
|
||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
this.cipherService = cipherService;
|
this.cipherService = cipherService;
|
||||||
|
|||||||
@@ -29,11 +29,6 @@
|
|||||||
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
|
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Router } from "@angular/router";
|
|||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
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 { 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 { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@@ -27,7 +26,6 @@ export class ExportComponent extends BaseExportComponent {
|
|||||||
policyService: PolicyService,
|
policyService: PolicyService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
userVerificationService: UserVerificationService,
|
|
||||||
formBuilder: UntypedFormBuilder,
|
formBuilder: UntypedFormBuilder,
|
||||||
fileDownloadService: FileDownloadService,
|
fileDownloadService: FileDownloadService,
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
@@ -40,7 +38,6 @@ export class ExportComponent extends BaseExportComponent {
|
|||||||
eventCollectionService,
|
eventCollectionService,
|
||||||
policyService,
|
policyService,
|
||||||
logService,
|
logService,
|
||||||
userVerificationService,
|
|
||||||
formBuilder,
|
formBuilder,
|
||||||
fileDownloadService,
|
fileDownloadService,
|
||||||
dialogService,
|
dialogService,
|
||||||
|
|||||||
@@ -103,10 +103,8 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa
|
|||||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||||
import {
|
import { legacyPasswordGenerationServiceFactory } from "@bitwarden/common/tools/generator";
|
||||||
PasswordGenerationService,
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
PasswordGenerationServiceAbstraction,
|
|
||||||
} from "@bitwarden/common/tools/generator/password";
|
|
||||||
import {
|
import {
|
||||||
PasswordStrengthService,
|
PasswordStrengthService,
|
||||||
PasswordStrengthServiceAbstraction,
|
PasswordStrengthServiceAbstraction,
|
||||||
@@ -499,10 +497,12 @@ export class ServiceContainer {
|
|||||||
|
|
||||||
this.passwordStrengthService = new PasswordStrengthService();
|
this.passwordStrengthService = new PasswordStrengthService();
|
||||||
|
|
||||||
this.passwordGenerationService = new PasswordGenerationService(
|
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||||
|
this.encryptService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.policyService,
|
this.policyService,
|
||||||
this.stateService,
|
this.accountService,
|
||||||
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||||
|
|||||||
@@ -411,7 +411,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
this.masterPasswordService.forceSetPasswordReason$(message.userId),
|
this.masterPasswordService.forceSetPasswordReason$(message.userId),
|
||||||
)) != ForceSetPasswordReason.None;
|
)) != ForceSetPasswordReason.None;
|
||||||
if (locked) {
|
if (locked) {
|
||||||
this.messagingService.send("locked", { userId: message.userId });
|
this.modalService.closeAll();
|
||||||
|
await this.router.navigate(["lock"]);
|
||||||
} else if (forcedPasswordReset) {
|
} 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.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
|||||||
@@ -21,11 +21,6 @@
|
|||||||
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
|
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { UntypedFormBuilder } from "@angular/forms";
|
|||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
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 { 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 { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@@ -24,7 +23,6 @@ export class ExportComponent extends BaseExportComponent implements OnInit {
|
|||||||
exportService: VaultExportServiceAbstraction,
|
exportService: VaultExportServiceAbstraction,
|
||||||
eventCollectionService: EventCollectionService,
|
eventCollectionService: EventCollectionService,
|
||||||
policyService: PolicyService,
|
policyService: PolicyService,
|
||||||
userVerificationService: UserVerificationService,
|
|
||||||
formBuilder: UntypedFormBuilder,
|
formBuilder: UntypedFormBuilder,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
fileDownloadService: FileDownloadService,
|
fileDownloadService: FileDownloadService,
|
||||||
@@ -38,7 +36,6 @@ export class ExportComponent extends BaseExportComponent implements OnInit {
|
|||||||
eventCollectionService,
|
eventCollectionService,
|
||||||
policyService,
|
policyService,
|
||||||
logService,
|
logService,
|
||||||
userVerificationService,
|
|
||||||
formBuilder,
|
formBuilder,
|
||||||
fileDownloadService,
|
fileDownloadService,
|
||||||
dialogService,
|
dialogService,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -36,10 +35,6 @@ describe("GeneratorComponent", () => {
|
|||||||
provide: UsernameGenerationServiceAbstraction,
|
provide: UsernameGenerationServiceAbstraction,
|
||||||
useValue: mock<UsernameGenerationServiceAbstraction>(),
|
useValue: mock<UsernameGenerationServiceAbstraction>(),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: StateService,
|
|
||||||
useValue: mock<StateService>(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: PlatformUtilsService,
|
provide: PlatformUtilsService,
|
||||||
useValue: platformUtilsServiceMock,
|
useValue: platformUtilsServiceMock,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, NgZone } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
|
|
||||||
@@ -18,22 +17,22 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
|||||||
constructor(
|
constructor(
|
||||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||||
stateService: StateService,
|
accountService: AccountService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
|
ngZone: NgZone,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
accountService: AccountService,
|
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
passwordGenerationService,
|
passwordGenerationService,
|
||||||
usernameGenerationService,
|
usernameGenerationService,
|
||||||
platformUtilsService,
|
platformUtilsService,
|
||||||
stateService,
|
accountService,
|
||||||
i18nService,
|
i18nService,
|
||||||
logService,
|
logService,
|
||||||
route,
|
route,
|
||||||
accountService,
|
ngZone,
|
||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +1,99 @@
|
|||||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="bulkTitle">
|
<bit-dialog dialogSize="large" [title]="'confirmUsers' | i18n" [loading]="loading">
|
||||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
<ng-container bitDialogContent>
|
||||||
<div class="modal-content">
|
<app-callout type="danger" *ngIf="filteredUsers.length <= 0">
|
||||||
<div class="modal-header">
|
{{ "noSelectedUsersApplicable" | i18n }}
|
||||||
<h1 class="modal-title" id="bulkTitle">
|
</app-callout>
|
||||||
{{ "confirmUsers" | i18n }}
|
<app-callout type="error" *ngIf="error">
|
||||||
</h1>
|
{{ error }}
|
||||||
<button
|
</app-callout>
|
||||||
type="button"
|
<ng-container *ngIf="!loading && !done">
|
||||||
class="close"
|
<p bitTypography="body1">
|
||||||
data-dismiss="modal"
|
{{ "fingerprintEnsureIntegrityVerify" | i18n }}
|
||||||
appA11yTitle="{{ 'close' | i18n }}"
|
<a
|
||||||
|
bitLink
|
||||||
|
href="https://bitwarden.com/help/fingerprint-phrase/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">×</span>
|
{{ "learnMore" | i18n }}</a
|
||||||
</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()"
|
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
</p>
|
||||||
<span>{{ "confirm" | i18n }}</span>
|
<bit-table>
|
||||||
</button>
|
<ng-container header>
|
||||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
<tr>
|
||||||
{{ "close" | i18n }}
|
<th bitCell colspan="2">{{ "user" | i18n }}</th>
|
||||||
</button>
|
<th bitCell>{{ "fingerprint" | i18n }}</th>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
</ng-container>
|
||||||
</div>
|
<ng-template body>
|
||||||
</div>
|
<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 { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { BulkUserDetails } from "./bulk-status.component";
|
import { BulkUserDetails } from "./bulk-status.component";
|
||||||
|
|
||||||
|
type BulkConfirmDialogData = {
|
||||||
|
organizationId: string;
|
||||||
|
users: BulkUserDetails[];
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-bulk-confirm",
|
selector: "app-bulk-confirm",
|
||||||
templateUrl: "bulk-confirm.component.html",
|
templateUrl: "bulk-confirm.component.html",
|
||||||
})
|
})
|
||||||
export class BulkConfirmComponent implements OnInit {
|
export class BulkConfirmComponent implements OnInit {
|
||||||
@Input() organizationId: string;
|
organizationId: string;
|
||||||
@Input() users: BulkUserDetails[];
|
users: BulkUserDetails[];
|
||||||
|
|
||||||
excludedUsers: BulkUserDetails[];
|
excludedUsers: BulkUserDetails[];
|
||||||
filteredUsers: BulkUserDetails[];
|
filteredUsers: BulkUserDetails[];
|
||||||
@@ -30,11 +37,15 @@ export class BulkConfirmComponent implements OnInit {
|
|||||||
error: string;
|
error: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) protected data: BulkConfirmDialogData,
|
||||||
protected cryptoService: CryptoService,
|
protected cryptoService: CryptoService,
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
private organizationUserService: OrganizationUserService,
|
private organizationUserService: OrganizationUserService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
) {}
|
) {
|
||||||
|
this.organizationId = data.organizationId;
|
||||||
|
this.users = data.users;
|
||||||
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.excludedUsers = this.users.filter((u) => !this.isAccepted(u));
|
this.excludedUsers = this.users.filter((u) => !this.isAccepted(u));
|
||||||
@@ -110,4 +121,8 @@ export class BulkConfirmComponent implements OnInit {
|
|||||||
request,
|
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">
|
<bit-dialog dialogSize="large" [title]="'removeUsers' | i18n">
|
||||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
<ng-container bitDialogContent>
|
||||||
<div class="modal-content">
|
<app-callout type="danger" *ngIf="users.length <= 0">
|
||||||
<div class="modal-header">
|
{{ "noSelectedUsersApplicable" | i18n }}
|
||||||
<h1 class="modal-title" id="bulkTitle">
|
</app-callout>
|
||||||
{{ "removeUsers" | i18n }}
|
<app-callout type="error" *ngIf="error">
|
||||||
</h1>
|
{{ error }}
|
||||||
<button
|
</app-callout>
|
||||||
type="button"
|
<ng-container *ngIf="!done">
|
||||||
class="close"
|
<app-callout type="warning" *ngIf="users.length > 0 && !error">
|
||||||
data-dismiss="modal"
|
<p bitTypography="body1">{{ removeUsersWarning }}</p>
|
||||||
appA11yTitle="{{ 'close' | i18n }}"
|
<p *ngIf="this.showNoMasterPasswordWarning" bitTypography="body1">
|
||||||
>
|
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
|
||||||
<span aria-hidden="true">×</span>
|
</p>
|
||||||
</button>
|
</app-callout>
|
||||||
</div>
|
<bit-table>
|
||||||
<div class="modal-body">
|
<ng-container header>
|
||||||
<app-callout type="danger" *ngIf="users.length <= 0">
|
<tr>
|
||||||
{{ "noSelectedUsersApplicable" | i18n }}
|
<th bitCell colspan="2">{{ "user" | i18n }}</th>
|
||||||
</app-callout>
|
<th bitCell *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
|
||||||
<app-callout type="error" *ngIf="error">
|
</tr>
|
||||||
{{ 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>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="done">
|
<ng-template body>
|
||||||
<table class="table table-hover table-list">
|
<tr bitRow *ngFor="let user of users">
|
||||||
<thead>
|
<td bitCell class="tw-w-5">
|
||||||
<tr>
|
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||||
<th colspan="2">{{ "user" | i18n }}</th>
|
</td>
|
||||||
<th>{{ "status" | i18n }}</th>
|
<td bitCell>
|
||||||
</tr>
|
{{ user.email }}
|
||||||
</thead>
|
<small class="tw-text-muted tw-block" *ngIf="user.name">{{ user.name }}</small>
|
||||||
<tr *ngFor="let user of users">
|
</td>
|
||||||
<td width="30">
|
<td bitCell *ngIf="this.showNoMasterPasswordWarning">
|
||||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
<span class="tw-text-muted tw-block tw-lowercase">
|
||||||
</td>
|
<ng-container *ngIf="user.hasMasterPassword === true"> - </ng-container>
|
||||||
<td>
|
<ng-container *ngIf="user.hasMasterPassword === false">
|
||||||
{{ user.email }}
|
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
{{ "noMasterPassword" | i18n }}
|
||||||
</td>
|
</ng-container>
|
||||||
<td *ngIf="statuses.has(user.id)">
|
</span>
|
||||||
{{ statuses.get(user.id) }}
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
<td *ngIf="!statuses.has(user.id)">
|
</ng-template>
|
||||||
{{ "bulkFilteredMessage" | i18n }}
|
</bit-table>
|
||||||
</td>
|
</ng-container>
|
||||||
</tr>
|
<ng-container *ngIf="done">
|
||||||
</table>
|
<bit-table>
|
||||||
|
<ng-container header>
|
||||||
|
<tr>
|
||||||
|
<th bitCell colspan="2">{{ "user" | i18n }}</th>
|
||||||
|
<th bitCell>{{ "status" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
<ng-template body>
|
||||||
<div class="modal-footer">
|
<tr bitRow *ngFor="let user of users">
|
||||||
<button
|
<td bitCell class="tw-w-5">
|
||||||
type="submit"
|
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||||
class="btn btn-primary btn-submit"
|
</td>
|
||||||
*ngIf="!done && users.length > 0"
|
<td bitCell>
|
||||||
[disabled]="loading"
|
{{ user.email }}
|
||||||
(click)="submit()"
|
<small class="tw-text-muted tw-block" *ngIf="user.name">{{ user.name }}</small>
|
||||||
>
|
</td>
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
<td *ngIf="statuses.has(user.id)" bitCell>
|
||||||
<span>{{ "removeUsers" | i18n }}</span>
|
{{ statuses.get(user.id) }}
|
||||||
</button>
|
</td>
|
||||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
<td *ngIf="!statuses.has(user.id)" bitCell>
|
||||||
{{ "close" | i18n }}
|
{{ "bulkFilteredMessage" | i18n }}
|
||||||
</button>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
</ng-template>
|
||||||
</div>
|
</bit-table>
|
||||||
</div>
|
</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 { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { BulkUserDetails } from "./bulk-status.component";
|
import { BulkUserDetails } from "./bulk-status.component";
|
||||||
|
|
||||||
|
type BulkRemoveDialogData = {
|
||||||
|
organizationId: string;
|
||||||
|
users: BulkUserDetails[];
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-bulk-remove",
|
selector: "app-bulk-remove",
|
||||||
templateUrl: "bulk-remove.component.html",
|
templateUrl: "bulk-remove.component.html",
|
||||||
})
|
})
|
||||||
export class BulkRemoveComponent {
|
export class BulkRemoveComponent {
|
||||||
@Input() organizationId: string;
|
organizationId: string;
|
||||||
@Input() set users(value: BulkUserDetails[]) {
|
users: 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[];
|
|
||||||
|
|
||||||
statuses: Map<string, string> = new Map();
|
statuses: Map<string, string> = new Map();
|
||||||
|
|
||||||
@@ -34,12 +30,19 @@ export class BulkRemoveComponent {
|
|||||||
showNoMasterPasswordWarning = false;
|
showNoMasterPasswordWarning = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) protected data: BulkRemoveDialogData,
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
private organizationUserService: OrganizationUserService,
|
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;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await this.deleteUsers();
|
const response = await this.deleteUsers();
|
||||||
@@ -54,7 +57,7 @@ export class BulkRemoveComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
};
|
||||||
|
|
||||||
protected async deleteUsers() {
|
protected async deleteUsers() {
|
||||||
return await this.organizationUserService.deleteManyOrganizationUsers(
|
return await this.organizationUserService.deleteManyOrganizationUsers(
|
||||||
@@ -66,4 +69,8 @@ export class BulkRemoveComponent {
|
|||||||
protected get removeUsersWarning() {
|
protected get removeUsersWarning() {
|
||||||
return this.i18nService.t("removeOrgUsersConfirmation");
|
return this.i18nService.t("removeOrgUsersConfirmation");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static open(dialogService: DialogService, config: DialogConfig<BulkRemoveDialogData>) {
|
||||||
|
return dialogService.open(BulkRemoveComponent, config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
title="{{ 'loading' | i18n }}"
|
title="{{ 'loading' | i18n }}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<bit-tab-group
|
<bit-tab-group
|
||||||
*ngIf="!loading && organization$ | async as organization"
|
*ngIf="!loading && organization$ | async as organization"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
>
|
>
|
||||||
<bit-tab [label]="'role' | i18n">
|
<bit-tab [label]="'role' | i18n">
|
||||||
<ng-container *ngIf="!editMode">
|
<ng-container *ngIf="!editMode">
|
||||||
<p>{{ "inviteUserDesc" | i18n }}</p>
|
<p bitTypography="body1">{{ "inviteUserDesc" | i18n }}</p>
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||||
<input id="emails" type="text" appAutoFocus bitInput formControlName="emails" />
|
<input id="emails" type="text" appAutoFocus bitInput formControlName="emails" />
|
||||||
@@ -32,13 +32,11 @@
|
|||||||
}}</bit-hint>
|
}}</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<fieldset role="radiogroup" aria-labelledby="roleGroupLabel" class="tw-mb-6">
|
<bit-radio-group formControlName="type">
|
||||||
<legend
|
<bit-label>
|
||||||
id="roleGroupLabel"
|
|
||||||
class="tw-mb-2 tw-block tw-text-base tw-font-semibold tw-text-main"
|
|
||||||
>
|
|
||||||
{{ "memberRole" | i18n }}
|
{{ "memberRole" | i18n }}
|
||||||
<a
|
<a
|
||||||
|
bitLink
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||||
@@ -46,112 +44,63 @@
|
|||||||
>
|
>
|
||||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</legend>
|
</bit-label>
|
||||||
<div class="tw-mb-2 tw-flex tw-items-baseline">
|
<bit-radio-button id="userTypeUser" [value]="organizationUserType.User">
|
||||||
<input
|
<bit-label>{{ "user" | i18n }}</bit-label>
|
||||||
type="radio"
|
<bit-hint>{{ "userDesc" | i18n }}</bit-hint>
|
||||||
id="userTypeUser"
|
</bit-radio-button>
|
||||||
[value]="organizationUserType.User"
|
<bit-radio-button
|
||||||
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
|
|
||||||
*ngIf="!organization.flexibleCollections"
|
*ngIf="!organization.flexibleCollections"
|
||||||
class="tw-mb-2 tw-flex tw-items-baseline"
|
id="userTypeManager"
|
||||||
|
[value]="organizationUserType.Manager"
|
||||||
>
|
>
|
||||||
<input
|
<bit-label>{{ "manager" | i18n }}</bit-label>
|
||||||
type="radio"
|
<bit-hint>{{ "managerDesc" | i18n }}</bit-hint>
|
||||||
id="userTypeManager"
|
</bit-radio-button>
|
||||||
[value]="organizationUserType.Manager"
|
<bit-radio-button id="userTypeAdmin" [value]="organizationUserType.Admin">
|
||||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
<bit-label>{{ "admin" | i18n }}</bit-label>
|
||||||
formControlName="type"
|
<bit-hint>{{ "adminDesc" | i18n }}</bit-hint>
|
||||||
name="type"
|
</bit-radio-button>
|
||||||
/>
|
<bit-radio-button id="userTypeOwner" [value]="organizationUserType.Owner">
|
||||||
<label class="tw-m-0" for="userTypeManager">
|
<bit-label>{{ "owner" | i18n }}</bit-label>
|
||||||
{{ "manager" | i18n }}
|
<bit-hint>{{ "ownerDesc" | i18n }}</bit-hint>
|
||||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
</bit-radio-button>
|
||||||
{{ "managerDesc" | i18n }}
|
<bit-radio-button
|
||||||
</div>
|
id="userTypeCustom"
|
||||||
</label>
|
[value]="organizationUserType.Custom"
|
||||||
</div>
|
[disabled]="!organization.useCustomPermissions || null"
|
||||||
<div class="tw-mb-2 tw-flex tw-items-baseline">
|
>
|
||||||
<input
|
<bit-label>{{ "custom" | i18n }}</bit-label>
|
||||||
type="radio"
|
<bit-hint>
|
||||||
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 }}
|
|
||||||
<ng-container *ngIf="!organization.useCustomPermissions; else enterprise">
|
<ng-container *ngIf="!organization.useCustomPermissions; else enterprise">
|
||||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
<p>
|
||||||
{{ "customDescNonEnterpriseStart" | i18n
|
{{ "customDescNonEnterpriseStart" | i18n
|
||||||
}}<a href="https://bitwarden.com/contact/" target="_blank" rel="noreferrer">{{
|
}}<a
|
||||||
"customDescNonEnterpriseLink" | i18n
|
bitLink
|
||||||
}}</a
|
href="https://bitwarden.com/contact/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>{{ "customDescNonEnterpriseLink" | i18n }}</a
|
||||||
>{{ "customDescNonEnterpriseEnd" | i18n }}
|
>{{ "customDescNonEnterpriseEnd" | i18n }}
|
||||||
</div>
|
</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #enterprise>
|
<ng-template #enterprise>
|
||||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
<p>{{ "customDesc" | i18n }}</p>
|
||||||
{{ "customDesc" | i18n }}
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</label>
|
</bit-hint>
|
||||||
</div>
|
</bit-radio-button>
|
||||||
</fieldset>
|
</bit-radio-group>
|
||||||
<ng-container *ngIf="customUserTypeSelected">
|
<ng-container *ngIf="customUserTypeSelected">
|
||||||
<ng-container *ngIf="!organization.flexibleCollections; else customPermissionsFC">
|
<ng-container *ngIf="!organization.flexibleCollections; else customPermissionsFC">
|
||||||
<h3 class="mt-4 d-flex tw-font-semibold">
|
<h3 bitTypography="h3">
|
||||||
{{ "permissions" | i18n }}
|
{{ "permissions" | i18n }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="row" [formGroup]="permissionsGroup">
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
|
||||||
<div class="col-6">
|
<div class="tw-col-span-6">
|
||||||
<div class="mb-3">
|
<div class="tw-mb-3">
|
||||||
<label class="tw-font-semibold">{{ "managerPermissions" | i18n }}</label>
|
<bit-label class="tw-font-semibold">{{
|
||||||
|
"managerPermissions" | i18n
|
||||||
|
}}</bit-label>
|
||||||
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
||||||
<app-nested-checkbox
|
<app-nested-checkbox
|
||||||
parentId="manageAssignedCollections"
|
parentId="manageAssignedCollections"
|
||||||
@@ -160,221 +109,126 @@
|
|||||||
</app-nested-checkbox>
|
</app-nested-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="tw-col-span-6">
|
||||||
<div class="mb-3">
|
<div class="tw-mb-3">
|
||||||
<label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</label>
|
<bit-label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</bit-label>
|
||||||
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
||||||
<div>
|
<bit-form-control>
|
||||||
<input
|
<input type="checkbox" bitCheckbox formControlName="accessEventLogs" />
|
||||||
type="checkbox"
|
<bit-label>{{ "accessEventLogs" | i18n }}</bit-label>
|
||||||
name="accessEventLogs"
|
</bit-form-control>
|
||||||
id="accessEventLogs"
|
<bit-form-control>
|
||||||
formControlName="accessEventLogs"
|
<input type="checkbox" bitCheckbox formControlName="accessImportExport" />
|
||||||
/>
|
<bit-label>{{ "accessImportExport" | i18n }}</bit-label>
|
||||||
<label class="!tw-font-normal" for="accessEventLogs">
|
</bit-form-control>
|
||||||
{{ "accessEventLogs" | i18n }}
|
<bit-form-control>
|
||||||
</label>
|
<input type="checkbox" bitCheckbox formControlName="accessReports" />
|
||||||
</div>
|
<bit-label>{{ "accessReports" | i18n }}</bit-label>
|
||||||
<div>
|
</bit-form-control>
|
||||||
<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>
|
|
||||||
<app-nested-checkbox
|
<app-nested-checkbox
|
||||||
parentId="manageAllCollections"
|
parentId="manageAllCollections"
|
||||||
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
||||||
>
|
>
|
||||||
</app-nested-checkbox>
|
</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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="manageGroups"
|
bitCheckbox
|
||||||
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"
|
|
||||||
formControlName="manageUsers"
|
formControlName="manageUsers"
|
||||||
(change)="handleDependentPermissions()"
|
(change)="handleDependentPermissions()"
|
||||||
/>
|
/>
|
||||||
<label class="!tw-font-normal" for="manageUsers">
|
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
|
||||||
{{ "manageUsers" | i18n }}
|
</bit-form-control>
|
||||||
</label>
|
<bit-form-control>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="manageResetPassword"
|
bitCheckbox
|
||||||
id="manageResetPassword"
|
|
||||||
formControlName="manageResetPassword"
|
formControlName="manageResetPassword"
|
||||||
(change)="handleDependentPermissions()"
|
(change)="handleDependentPermissions()"
|
||||||
/>
|
/>
|
||||||
<label class="!tw-font-normal" for="manageResetPassword">
|
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
|
||||||
{{ "manageAccountRecovery" | i18n }}
|
</bit-form-control>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #customPermissionsFC>
|
<ng-template #customPermissionsFC>
|
||||||
<div class="row" [formGroup]="permissionsGroup">
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
|
||||||
<div class="col-4">
|
<div class="tw-col-span-4">
|
||||||
<div>
|
<bit-form-control>
|
||||||
<input
|
<input type="checkbox" bitCheckbox formControlName="accessEventLogs" />
|
||||||
type="checkbox"
|
<bit-label>{{ "accessEventLogs" | i18n }}</bit-label>
|
||||||
name="accessEventLogs"
|
</bit-form-control>
|
||||||
id="accessEventLogs"
|
<bit-form-control>
|
||||||
formControlName="accessEventLogs"
|
<input type="checkbox" bitCheckbox formControlName="accessImportExport" />
|
||||||
/>
|
<bit-label>{{ "accessImportExport" | i18n }}</bit-label>
|
||||||
<label class="!tw-font-normal" for="accessEventLogs">
|
</bit-form-control>
|
||||||
{{ "accessEventLogs" | i18n }}
|
<bit-form-control>
|
||||||
</label>
|
<input type="checkbox" bitCheckbox formControlName="accessReports" />
|
||||||
</div>
|
<bit-label>{{ "accessReports" | i18n }}</bit-label>
|
||||||
<div>
|
</bit-form-control>
|
||||||
<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>
|
</div>
|
||||||
<div class="col-4">
|
<div class="tw-col-span-4">
|
||||||
<app-nested-checkbox
|
<app-nested-checkbox
|
||||||
parentId="manageAllCollections"
|
parentId="manageAllCollections"
|
||||||
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
||||||
>
|
>
|
||||||
</app-nested-checkbox>
|
</app-nested-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="tw-col-span-4">
|
||||||
<div class="mb-3">
|
<div class="tw-mb-3">
|
||||||
<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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="manageGroups"
|
bitCheckbox
|
||||||
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"
|
|
||||||
formControlName="manageUsers"
|
formControlName="manageUsers"
|
||||||
(change)="handleDependentPermissions()"
|
(change)="handleDependentPermissions()"
|
||||||
/>
|
/>
|
||||||
<label class="!tw-font-normal" for="manageUsers">
|
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
|
||||||
{{ "manageUsers" | i18n }}
|
</bit-form-control>
|
||||||
</label>
|
<bit-form-control>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="manageResetPassword"
|
bitCheckbox
|
||||||
id="manageResetPassword"
|
|
||||||
formControlName="manageResetPassword"
|
formControlName="manageResetPassword"
|
||||||
(change)="handleDependentPermissions()"
|
(change)="handleDependentPermissions()"
|
||||||
/>
|
/>
|
||||||
<label class="!tw-font-normal" for="manageResetPassword">
|
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
|
||||||
{{ "manageAccountRecovery" | i18n }}
|
</bit-form-control>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="organization.useSecretsManager">
|
<ng-container *ngIf="organization.useSecretsManager">
|
||||||
<h3 class="mt-4">
|
<h3 class="tw-mt-4">
|
||||||
{{ "secretsManager" | i18n }}
|
{{ "secretsManager" | i18n }}
|
||||||
<a
|
<a
|
||||||
|
bitLink
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||||
@@ -436,6 +290,7 @@
|
|||||||
<bit-label>
|
<bit-label>
|
||||||
{{ "accessAllCollectionsDesc" | i18n }}
|
{{ "accessAllCollectionsDesc" | i18n }}
|
||||||
<a
|
<a
|
||||||
|
bitLink
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
<div [formGroup]="checkboxes">
|
<div [formGroup]="checkboxes">
|
||||||
<input
|
<bit-form-control>
|
||||||
type="checkbox"
|
<input
|
||||||
[name]="pascalize(parentId)"
|
type="checkbox"
|
||||||
[id]="parentId"
|
bitCheckbox
|
||||||
[formControlName]="parentId"
|
[formControlName]="parentId"
|
||||||
[indeterminate]="parentIndeterminate"
|
[indeterminate]="parentIndeterminate"
|
||||||
/>
|
/>
|
||||||
<label class="!tw-font-normal" [for]="parentId">
|
<bit-label>{{ parentId | i18n }}</bit-label>
|
||||||
{{ parentId | i18n }}
|
</bit-form-control>
|
||||||
</label>
|
<div class="tw-ml-4">
|
||||||
<div class="tw-ml-6">
|
|
||||||
<ng-container *ngFor="let c of checkboxes.controls | keyvalue; trackBy: key">
|
<ng-container *ngFor="let c of checkboxes.controls | keyvalue; trackBy: key">
|
||||||
<div class="" *ngIf="c.key != parentId">
|
<div *ngIf="c.key != parentId">
|
||||||
<input
|
<bit-form-control>
|
||||||
class=""
|
<input type="checkbox" bitCheckbox [formControl]="c.value" (change)="onChildCheck()" />
|
||||||
type="checkbox"
|
<bit-label>{{ c.key | i18n }}</bit-label>
|
||||||
[name]="pascalize(c.key)"
|
</bit-form-control>
|
||||||
[id]="c.key"
|
|
||||||
[formControl]="c.value"
|
|
||||||
(change)="onChildCheck()"
|
|
||||||
/>
|
|
||||||
<label class="!tw-font-normal" [for]="c.key">
|
|
||||||
{{ c.key | i18n }}
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -481,16 +481,13 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [modal] = await this.modalService.openViewRef(
|
const dialogRef = BulkRemoveComponent.open(this.dialogService, {
|
||||||
BulkRemoveComponent,
|
data: {
|
||||||
this.bulkRemoveModalRef,
|
organizationId: this.organization.id,
|
||||||
(comp) => {
|
users: this.getCheckedUsers(),
|
||||||
comp.organizationId = this.organization.id;
|
|
||||||
comp.users = this.getCheckedUsers();
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
await lastValueFrom(dialogRef.closed);
|
||||||
await modal.onClosedPromise();
|
|
||||||
await this.load();
|
await this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,16 +555,14 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [modal] = await this.modalService.openViewRef(
|
const dialogRef = BulkConfirmComponent.open(this.dialogService, {
|
||||||
BulkConfirmComponent,
|
data: {
|
||||||
this.bulkConfirmModalRef,
|
organizationId: this.organization.id,
|
||||||
(comp) => {
|
users: this.getCheckedUsers(),
|
||||||
comp.organizationId = this.organization.id;
|
|
||||||
comp.users = this.getCheckedUsers();
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
await modal.onClosedPromise();
|
await lastValueFrom(dialogRef.closed);
|
||||||
await this.load();
|
await this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,7 @@
|
|||||||
{{ "disableSendExemption" | i18n }}
|
{{ "disableSendExemption" | i18n }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
|
||||||
<div class="form-group">
|
<bit-form-control>
|
||||||
<div class="form-check">
|
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||||
<input
|
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||||
class="form-check-input"
|
</bit-form-control>
|
||||||
type="checkbox"
|
|
||||||
id="enabled"
|
|
||||||
[formControl]="enabled"
|
|
||||||
name="Enabled"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,144 +1,64 @@
|
|||||||
<div [formGroup]="data">
|
<div [formGroup]="data">
|
||||||
<div class="form-group">
|
<bit-form-control>
|
||||||
<div class="form-check">
|
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||||
<input
|
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||||
class="form-check-input"
|
</bit-form-control>
|
||||||
type="checkbox"
|
|
||||||
id="enabled"
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
[formControl]="enabled"
|
<bit-form-field class="tw-col-span-6 tw-mb-0">
|
||||||
name="Enabled"
|
<bit-label>{{ "defaultType" | i18n }}</bit-label>
|
||||||
/>
|
<bit-select formControlName="defaultType" id="defaultType">
|
||||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
<bit-option *ngFor="let o of defaultTypes" [value]="o.value" [label]="o.name"></bit-option>
|
||||||
</div>
|
</bit-select>
|
||||||
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<h3 bitTypography="h3" class="tw-mt-4">{{ "password" | i18n }}</h3>
|
||||||
<div class="col-6 form-group mb-0">
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
<label for="defaultType">{{ "defaultType" | i18n }}</label>
|
<bit-form-field class="tw-col-span-6">
|
||||||
<select
|
<bit-label>{{ "minLength" | i18n }}</bit-label>
|
||||||
id="defaultType"
|
<input bitInput type="number" min="5" max="128" formControlName="minLength" />
|
||||||
name="defaultType"
|
</bit-form-field>
|
||||||
formControlName="defaultType"
|
|
||||||
class="form-control"
|
|
||||||
>
|
|
||||||
<option *ngFor="let o of defaultTypes" [ngValue]="o.value">{{ o.name }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 class="mt-4">{{ "password" | i18n }}</h3>
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
<div class="row">
|
<bit-form-field class="tw-col-span-6">
|
||||||
<div class="col-6 form-group">
|
<bit-label>{{ "minNumbers" | i18n }}</bit-label>
|
||||||
<label for="minLength">{{ "minLength" | i18n }}</label>
|
<input bitInput type="number" min="0" max="9" formControlName="minNumbers" />
|
||||||
<input
|
</bit-form-field>
|
||||||
id="minLength"
|
<bit-form-field class="tw-col-span-6">
|
||||||
class="form-control"
|
<bit-label>{{ "minSpecial" | i18n }}</bit-label>
|
||||||
type="number"
|
<input bitInput type="number" min="0" max="9" formControlName="minSpecial" />
|
||||||
name="minLength"
|
</bit-form-field>
|
||||||
min="5"
|
|
||||||
max="128"
|
|
||||||
formControlName="minLength"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<bit-form-control>
|
||||||
<div class="col-6 form-group">
|
<input type="checkbox" bitCheckbox formControlName="useUpper" id="useUpper" />
|
||||||
<label for="minNumbers">{{ "minNumbers" | i18n }}</label>
|
<bit-label>A-Z</bit-label>
|
||||||
<input
|
</bit-form-control>
|
||||||
id="minNumbers"
|
<bit-form-control>
|
||||||
class="form-control"
|
<input type="checkbox" bitCheckbox formControlName="useLower" id="useLower" />
|
||||||
type="number"
|
<bit-label>a-z</bit-label>
|
||||||
name="minNumbers"
|
</bit-form-control>
|
||||||
min="0"
|
<bit-form-control>
|
||||||
max="9"
|
<input type="checkbox" bitCheckbox formControlName="useNumbers" id="useNumbers" />
|
||||||
formControlName="minNumbers"
|
<bit-label>0-9</bit-label>
|
||||||
/>
|
</bit-form-control>
|
||||||
</div>
|
<bit-form-control>
|
||||||
<div class="col-6 form-group">
|
<input type="checkbox" bitCheckbox formControlName="useSpecial" id="useSpecial" />
|
||||||
<label for="minSpecial">{{ "minSpecial" | i18n }}</label>
|
<bit-label>!@#$%^&*</bit-label>
|
||||||
<input
|
</bit-form-control>
|
||||||
id="minSpecial"
|
<h3 bitTypography="h3" class="tw-mt-4">{{ "passphrase" | i18n }}</h3>
|
||||||
class="form-control"
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
type="number"
|
<bit-form-field class="tw-col-span-6">
|
||||||
name="minSpecial"
|
<bit-label>{{ "minimumNumberOfWords" | i18n }}</bit-label>
|
||||||
min="0"
|
<input bitInput type="number" min="3" max="20" formControlName="minNumberWords" />
|
||||||
max="9"
|
</bit-form-field>
|
||||||
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>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component } from "@angular/core";
|
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 { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -20,14 +20,14 @@ export class PasswordGeneratorPolicy extends BasePolicy {
|
|||||||
export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
|
export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
|
||||||
data = this.formBuilder.group({
|
data = this.formBuilder.group({
|
||||||
defaultType: [null],
|
defaultType: [null],
|
||||||
minLength: [null],
|
minLength: [null, [Validators.min(5), Validators.max(128)]],
|
||||||
useUpper: [null],
|
useUpper: [null],
|
||||||
useLower: [null],
|
useLower: [null],
|
||||||
useNumbers: [null],
|
useNumbers: [null],
|
||||||
useSpecial: [null],
|
useSpecial: [null],
|
||||||
minNumbers: [null],
|
minNumbers: [null, [Validators.min(0), Validators.max(9)]],
|
||||||
minSpecial: [null],
|
minSpecial: [null, [Validators.min(0), Validators.max(9)]],
|
||||||
minNumberWords: [null],
|
minNumberWords: [null, [Validators.min(3), Validators.max(20)]],
|
||||||
capitalize: [null],
|
capitalize: [null],
|
||||||
includeNumber: [null],
|
includeNumber: [null],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,15 +5,7 @@
|
|||||||
{{ "requireSsoExemption" | i18n }}
|
{{ "requireSsoExemption" | i18n }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
|
||||||
<div class="form-group">
|
<bit-form-control>
|
||||||
<div class="form-check">
|
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||||
<input
|
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||||
class="form-check-input"
|
</bit-form-control>
|
||||||
type="checkbox"
|
|
||||||
id="enabled"
|
|
||||||
[formControl]="enabled"
|
|
||||||
name="Enabled"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -2,29 +2,15 @@
|
|||||||
{{ "sendOptionsExemption" | i18n }}
|
{{ "sendOptionsExemption" | i18n }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
|
||||||
<div class="form-group">
|
<bit-form-control>
|
||||||
<div class="form-check">
|
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||||
<input
|
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||||
class="form-check-input"
|
</bit-form-control>
|
||||||
type="checkbox"
|
|
||||||
id="enabled"
|
|
||||||
[formControl]="enabled"
|
|
||||||
name="Enabled"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div [formGroup]="data">
|
<div [formGroup]="data">
|
||||||
<h3 class="mt-4">{{ "options" | i18n }}</h3>
|
<h3 bitTypography="h3" class="tw-mt-4">{{ "options" | i18n }}</h3>
|
||||||
<div class="form-check">
|
<bit-form-control>
|
||||||
<input
|
<input type="checkbox" bitCheckbox formControlName="disableHideEmail" id="disableHideEmail" />
|
||||||
class="form-check-input"
|
<bit-label>{{ "disableHideEmail" | i18n }}</bit-label>
|
||||||
type="checkbox"
|
</bit-form-control>
|
||||||
id="disableHideEmail"
|
|
||||||
name="DisableHideEmail"
|
|
||||||
formControlName="disableHideEmail"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="disableHideEmail">{{ "disableHideEmail" | i18n }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,15 +2,7 @@
|
|||||||
{{ "singleOrgPolicyWarning" | i18n }}
|
{{ "singleOrgPolicyWarning" | i18n }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
|
|
||||||
<div class="form-group">
|
<bit-form-control>
|
||||||
<div class="form-check">
|
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||||
<input
|
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||||
class="form-check-input"
|
</bit-form-control>
|
||||||
type="checkbox"
|
|
||||||
id="enabled"
|
|
||||||
[formControl]="enabled"
|
|
||||||
name="Enabled"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -104,12 +104,11 @@
|
|||||||
<button type="button" bitButton buttonType="danger" (click)="deleteOrganization()">
|
<button type="button" bitButton buttonType="danger" (click)="deleteOrganization()">
|
||||||
{{ "deleteOrganization" | i18n }}
|
{{ "deleteOrganization" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitButton buttonType="danger" (click)="purgeVault()">
|
<button type="button" bitButton buttonType="danger" [bitAction]="purgeVault">
|
||||||
{{ "purgeVault" | i18n }}
|
{{ "purgeVault" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</app-danger-zone>
|
</app-danger-zone>
|
||||||
|
|
||||||
<ng-template #purgeOrganizationTemplate></ng-template>
|
|
||||||
<ng-template #apiKeyTemplate></ng-template>
|
<ng-template #apiKeyTemplate></ng-template>
|
||||||
<ng-template #rotateApiKeyTemplate></ng-template>
|
<ng-template #rotateApiKeyTemplate></ng-template>
|
||||||
</bit-container>
|
</bit-container>
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./
|
|||||||
templateUrl: "account.component.html",
|
templateUrl: "account.component.html",
|
||||||
})
|
})
|
||||||
export class AccountComponent {
|
export class AccountComponent {
|
||||||
@ViewChild("purgeOrganizationTemplate", { read: ViewContainerRef, static: true })
|
|
||||||
purgeModalRef: ViewContainerRef;
|
|
||||||
@ViewChild("apiKeyTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("apiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||||
apiKeyModalRef: ViewContainerRef;
|
apiKeyModalRef: ViewContainerRef;
|
||||||
@ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||||
@@ -232,11 +230,14 @@ export class AccountComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async purgeVault() {
|
purgeVault = async () => {
|
||||||
await this.modalService.openViewRef(PurgeVaultComponent, this.purgeModalRef, (comp) => {
|
const dialogRef = PurgeVaultComponent.open(this.dialogService, {
|
||||||
comp.organizationId = this.organizationId;
|
data: {
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
await lastValueFrom(dialogRef.closed);
|
||||||
|
};
|
||||||
|
|
||||||
async viewApiKey() {
|
async viewApiKey() {
|
||||||
await this.modalService.openViewRef(ApiKeyComponent, this.apiKeyModalRef, (comp) => {
|
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 { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
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 { 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 { EventType } from "@bitwarden/common/enums";
|
||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@@ -30,7 +29,6 @@ export class OrganizationVaultExportComponent extends ExportComponent {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
policyService: PolicyService,
|
policyService: PolicyService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
userVerificationService: UserVerificationService,
|
|
||||||
formBuilder: UntypedFormBuilder,
|
formBuilder: UntypedFormBuilder,
|
||||||
fileDownloadService: FileDownloadService,
|
fileDownloadService: FileDownloadService,
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
@@ -43,7 +41,6 @@ export class OrganizationVaultExportComponent extends ExportComponent {
|
|||||||
eventCollectionService,
|
eventCollectionService,
|
||||||
policyService,
|
policyService,
|
||||||
logService,
|
logService,
|
||||||
userVerificationService,
|
|
||||||
formBuilder,
|
formBuilder,
|
||||||
fileDownloadService,
|
fileDownloadService,
|
||||||
dialogService,
|
dialogService,
|
||||||
|
|||||||
@@ -1,50 +1,36 @@
|
|||||||
<td>
|
<td bitCell>
|
||||||
{{ sponsoringOrg.familySponsorshipFriendlyName }}
|
{{ sponsoringOrg.familySponsorshipFriendlyName }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ sponsoringOrg.name }}</td>
|
<td bitCell>{{ sponsoringOrg.name }}</td>
|
||||||
<td>
|
<td bitCell>
|
||||||
<span [ngClass]="statusClass">{{ statusMessage }}</span>
|
<span [ngClass]="statusClass">{{ statusMessage }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="table-action-right">
|
<td bitCell>
|
||||||
<div class="dropdown" appListDropdown>
|
<button
|
||||||
|
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-ellipsis-v"
|
||||||
|
buttonType="main"
|
||||||
|
[bitMenuTriggerFor]="appListDropdown"
|
||||||
|
appA11yTitle="{{ 'options' | i18n }}"
|
||||||
|
></button>
|
||||||
|
<bit-menu #appListDropdown>
|
||||||
<button
|
<button
|
||||||
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
|
|
||||||
class="btn btn-outline-secondary dropdown-toggle"
|
|
||||||
type="button"
|
type="button"
|
||||||
id="dropdownMenuButton"
|
bitMenuItem
|
||||||
data-toggle="dropdown"
|
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
|
||||||
aria-haspopup="true"
|
(click)="resendEmail()"
|
||||||
aria-expanded="false"
|
[attr.aria-label]="'resendEmailLabel' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||||
appA11yTitle="{{ 'options' | i18n }}"
|
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
{{ "resendEmail" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
bitMenuItem
|
||||||
#resendEmailBtn
|
(click)="revokeSponsorship()"
|
||||||
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
|
[attr.aria-label]="'revokeAccount' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||||
[appApiAction]="resendEmailPromise"
|
>
|
||||||
class="dropdown-item btn-submit"
|
<span class="tw-text-danger">{{ "remove" | i18n }}</span>
|
||||||
[disabled]="$any(resendEmailBtn).loading"
|
</button>
|
||||||
(click)="resendEmail()"
|
</bit-menu>
|
||||||
[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>
|
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ export class SponsoringOrgRowComponent implements OnInit {
|
|||||||
@Output() sponsorshipRemoved = new EventEmitter();
|
@Output() sponsorshipRemoved = new EventEmitter();
|
||||||
|
|
||||||
statusMessage = "loading";
|
statusMessage = "loading";
|
||||||
statusClass: "text-success" | "text-danger" = "text-success";
|
statusClass: "tw-text-success" | "tw-text-danger" = "tw-text-success";
|
||||||
|
|
||||||
revokeSponsorshipPromise: Promise<any>;
|
|
||||||
resendEmailPromise: Promise<any>;
|
|
||||||
|
|
||||||
private locale = "";
|
private locale = "";
|
||||||
|
|
||||||
@@ -48,20 +45,15 @@ export class SponsoringOrgRowComponent implements OnInit {
|
|||||||
|
|
||||||
async revokeSponsorship() {
|
async revokeSponsorship() {
|
||||||
try {
|
try {
|
||||||
this.revokeSponsorshipPromise = this.doRevokeSponsorship();
|
await this.doRevokeSponsorship();
|
||||||
await this.revokeSponsorshipPromise;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.revokeSponsorshipPromise = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async resendEmail() {
|
async resendEmail() {
|
||||||
this.resendEmailPromise = this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
|
await this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
|
||||||
await this.resendEmailPromise;
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("emailSent"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("emailSent"));
|
||||||
this.resendEmailPromise = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isSentAwaitingSync() {
|
get isSentAwaitingSync() {
|
||||||
@@ -106,31 +98,31 @@ export class SponsoringOrgRowComponent implements OnInit {
|
|||||||
"revokeWhenExpired",
|
"revokeWhenExpired",
|
||||||
formatDate(validUntil, "MM/dd/yyyy", this.locale),
|
formatDate(validUntil, "MM/dd/yyyy", this.locale),
|
||||||
);
|
);
|
||||||
this.statusClass = "text-danger";
|
this.statusClass = "tw-text-danger";
|
||||||
} else if (toDelete) {
|
} else if (toDelete) {
|
||||||
// They want to delete and we don't have a valid until date so we can
|
// 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 should only happen on a self-hosted install
|
||||||
this.statusMessage = this.i18nService.t("requestRemoved");
|
this.statusMessage = this.i18nService.t("requestRemoved");
|
||||||
this.statusClass = "text-danger";
|
this.statusClass = "tw-text-danger";
|
||||||
} else if (validUntil) {
|
} else if (validUntil) {
|
||||||
// They don't want to delete and they have a valid until date
|
// They don't want to delete and they have a valid until date
|
||||||
// that means they are actively sponsoring someone
|
// that means they are actively sponsoring someone
|
||||||
this.statusMessage = this.i18nService.t("active");
|
this.statusMessage = this.i18nService.t("active");
|
||||||
this.statusClass = "text-success";
|
this.statusClass = "tw-text-success";
|
||||||
} else if (selfHosted && lastSyncDate) {
|
} else if (selfHosted && lastSyncDate) {
|
||||||
// We are on a self-hosted install and it has been synced but we have not gotten
|
// 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
|
// a valid until date so we can't know if they are actively sponsoring someone
|
||||||
this.statusMessage = this.i18nService.t("sent");
|
this.statusMessage = this.i18nService.t("sent");
|
||||||
this.statusClass = "text-success";
|
this.statusClass = "tw-text-success";
|
||||||
} else if (!selfHosted) {
|
} else if (!selfHosted) {
|
||||||
// We are in cloud and all other status checks have been false therefore we have
|
// 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
|
// sent the request but it hasn't been accepted yet
|
||||||
this.statusMessage = this.i18nService.t("sent");
|
this.statusMessage = this.i18nService.t("sent");
|
||||||
this.statusClass = "text-success";
|
this.statusClass = "tw-text-success";
|
||||||
} else {
|
} else {
|
||||||
// We are on a self-hosted install and we have not synced yet
|
// We are on a self-hosted install and we have not synced yet
|
||||||
this.statusMessage = this.i18nService.t("requested");
|
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()">
|
<button type="button" bitButton buttonType="danger" (click)="deauthorizeSessions()">
|
||||||
{{ "deauthorizeSessions" | i18n }}
|
{{ "deauthorizeSessions" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitButton buttonType="danger" (click)="purgeVault()">
|
<button type="button" bitButton buttonType="danger" [bitAction]="purgeVault">
|
||||||
{{ "purgeVault" | i18n }}
|
{{ "purgeVault" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" bitButton buttonType="danger" (click)="deleteAccount()">
|
<button type="button" bitButton buttonType="danger" (click)="deleteAccount()">
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
</app-danger-zone>
|
</app-danger-zone>
|
||||||
|
|
||||||
<ng-template #deauthorizeSessionsTemplate></ng-template>
|
<ng-template #deauthorizeSessionsTemplate></ng-template>
|
||||||
<ng-template #purgeVaultTemplate></ng-template>
|
|
||||||
<ng-template #deleteAccountTemplate></ng-template>
|
<ng-template #deleteAccountTemplate></ng-template>
|
||||||
<ng-template #viewUserApiKeyTemplate></ng-template>
|
<ng-template #viewUserApiKeyTemplate></ng-template>
|
||||||
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
|
import { lastValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.component";
|
import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.component";
|
||||||
|
|
||||||
@@ -15,8 +17,6 @@ import { DeleteAccountComponent } from "./delete-account.component";
|
|||||||
export class AccountComponent {
|
export class AccountComponent {
|
||||||
@ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true })
|
||||||
deauthModalRef: ViewContainerRef;
|
deauthModalRef: ViewContainerRef;
|
||||||
@ViewChild("purgeVaultTemplate", { read: ViewContainerRef, static: true })
|
|
||||||
purgeModalRef: ViewContainerRef;
|
|
||||||
@ViewChild("deleteAccountTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("deleteAccountTemplate", { read: ViewContainerRef, static: true })
|
||||||
deleteModalRef: ViewContainerRef;
|
deleteModalRef: ViewContainerRef;
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ export class AccountComponent {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
|
private dialogService: DialogService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -35,9 +36,10 @@ export class AccountComponent {
|
|||||||
await this.modalService.openViewRef(DeauthorizeSessionsComponent, this.deauthModalRef);
|
await this.modalService.openViewRef(DeauthorizeSessionsComponent, this.deauthModalRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
async purgeVault() {
|
purgeVault = async () => {
|
||||||
await this.modalService.openViewRef(PurgeVaultComponent, this.purgeModalRef);
|
const dialogRef = PurgeVaultComponent.open(this.dialogService);
|
||||||
}
|
await lastValueFrom(dialogRef.closed);
|
||||||
|
};
|
||||||
|
|
||||||
async deleteAccount() {
|
async deleteAccount() {
|
||||||
await this.modalService.openViewRef(DeleteAccountComponent, this.deleteModalRef);
|
await this.modalService.openViewRef(DeleteAccountComponent, this.deleteModalRef);
|
||||||
|
|||||||
@@ -1,46 +1,35 @@
|
|||||||
<div *ngIf="loading">
|
<div *ngIf="loading">
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||||
title="{{ 'loading' | i18n }}"
|
title="{{ 'loading' | i18n }}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
<form
|
<form *ngIf="profile && !loading" [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
*ngIf="profile && !loading"
|
<div class="tw-grid tw-grid-cols-12 tw-gap-6">
|
||||||
#form
|
<div class="tw-col-span-6">
|
||||||
(ngSubmit)="submit()"
|
<bit-form-field>
|
||||||
[appApiAction]="formPromise"
|
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||||
ngNativeValidate
|
<input bitInput formControlName="name" />
|
||||||
>
|
</bit-form-field>
|
||||||
<div class="row">
|
<bit-form-field>
|
||||||
<div class="col-6">
|
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||||
<div class="form-group">
|
<input bitInput formControlName="email" readonly />
|
||||||
<label for="name">{{ "name" | i18n }}</label>
|
</bit-form-field>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="tw-col-span-6">
|
||||||
<div class="mb-3">
|
<div class="tw-mb-3">
|
||||||
<dynamic-avatar text="{{ profile | userName }}" [id]="profile.id" [size]="'large'">
|
<dynamic-avatar text="{{ profile | userName }}" [id]="profile.id" [size]="'large'">
|
||||||
</dynamic-avatar>
|
</dynamic-avatar>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-secondary tw-ml-3.5"
|
buttonType="secondary"
|
||||||
|
bitButton
|
||||||
|
bitFormButton
|
||||||
appStopClick
|
appStopClick
|
||||||
appStopProp
|
appStopProp
|
||||||
(click)="openChangeAvatar()"
|
[bitAction]="openChangeAvatar"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-lg bwi-pencil-square" aria-hidden="true"></i>
|
<i class="bwi bwi-lg bwi-pencil-square" aria-hidden="true"></i>
|
||||||
Customize
|
Customize
|
||||||
@@ -53,9 +42,6 @@
|
|||||||
</app-account-fingerprint>
|
</app-account-fingerprint>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
<button bitButton bitFormButton type="submit" buttonType="primary">{{ "save" | i18n }}</button>
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
||||||
<span>{{ "save" | i18n }}</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
<ng-template #avatarModalTemplate></ng-template>
|
<ng-template #avatarModalTemplate></ng-template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ViewChild, ViewContainerRef, Component, OnDestroy, OnInit } from "@angular/core";
|
import { ViewChild, ViewContainerRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { FormControl, FormGroup } from "@angular/forms";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
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 { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
|
||||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
|
||||||
@@ -21,16 +21,19 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
profile: ProfileResponse;
|
profile: ProfileResponse;
|
||||||
fingerprintMaterial: string;
|
fingerprintMaterial: string;
|
||||||
|
|
||||||
formPromise: Promise<any>;
|
|
||||||
@ViewChild("avatarModalTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("avatarModalTemplate", { read: ViewContainerRef, static: true })
|
||||||
avatarModalRef: ViewContainerRef;
|
avatarModalRef: ViewContainerRef;
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
protected formGroup = new FormGroup({
|
||||||
|
name: new FormControl(null),
|
||||||
|
email: new FormControl(null),
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private logService: LogService,
|
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
) {}
|
) {}
|
||||||
@@ -39,6 +42,15 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
this.profile = await this.apiService.getProfile();
|
this.profile = await this.apiService.getProfile();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.fingerprintMaterial = await this.stateService.getUserId();
|
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() {
|
async ngOnDestroy() {
|
||||||
@@ -46,7 +58,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
async openChangeAvatar() {
|
openChangeAvatar = async () => {
|
||||||
const modalOpened = await this.modalService.openViewRef(
|
const modalOpened = await this.modalService.openViewRef(
|
||||||
ChangeAvatarComponent,
|
ChangeAvatarComponent,
|
||||||
this.avatarModalRef,
|
this.avatarModalRef,
|
||||||
@@ -57,16 +69,14 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
async submit() {
|
submit = async () => {
|
||||||
try {
|
const request = new UpdateProfileRequest(
|
||||||
const request = new UpdateProfileRequest(this.profile.name, this.profile.masterPasswordHint);
|
this.formGroup.get("name").value,
|
||||||
this.formPromise = this.apiService.putProfile(request);
|
this.profile.masterPasswordHint,
|
||||||
await this.formPromise;
|
);
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("accountUpdated"));
|
await this.apiService.putProfile(request);
|
||||||
} catch (e) {
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("accountUpdated"));
|
||||||
this.logService.error(e);
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<div class="tw-rounded tw-border tw-border-solid tw-border-warning-600 tw-bg-background">
|
<bit-banner bannerType="warning" (onClose)="onDismiss.emit()">
|
||||||
<div class="tw-bg-warning-600 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast">
|
{{ "verifyEmailDesc" | i18n }}
|
||||||
<i class="bwi bwi-envelope bwi-fw" aria-hidden="true"></i> {{ "verifyEmail" | i18n }}
|
<button
|
||||||
</div>
|
id="sendBtn"
|
||||||
<div class="tw-p-5">
|
bitLink
|
||||||
<p>{{ "verifyEmailDesc" | i18n }}</p>
|
linkType="contrast"
|
||||||
<button id="sendBtn" bitButton type="button" block [bitAction]="send">
|
bitButton
|
||||||
{{ "sendEmail" | i18n }}
|
type="button"
|
||||||
</button>
|
buttonType="unstyled"
|
||||||
</div>
|
[bitAction]="send"
|
||||||
</div>
|
>
|
||||||
|
{{ "sendEmail" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-banner>
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, EventEmitter, Output } from "@angular/core";
|
import { Component, EventEmitter, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { AsyncActionsModule, BannerModule, ButtonModule, LinkModule } from "@bitwarden/components";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
standalone: true,
|
||||||
selector: "app-verify-email",
|
selector: "app-verify-email",
|
||||||
templateUrl: "verify-email.component.html",
|
templateUrl: "verify-email.component.html",
|
||||||
|
imports: [AsyncActionsModule, BannerModule, ButtonModule, CommonModule, JslibModule, LinkModule],
|
||||||
})
|
})
|
||||||
export class VerifyEmailComponent {
|
export class VerifyEmailComponent {
|
||||||
actionPromise: Promise<unknown>;
|
actionPromise: Promise<unknown>;
|
||||||
|
|
||||||
@Output() onVerified = new EventEmitter<boolean>();
|
@Output() onVerified = new EventEmitter<boolean>();
|
||||||
|
@Output() onDismiss = new EventEmitter<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private logService: LogService,
|
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -51,8 +51,8 @@
|
|||||||
</bit-section>
|
</bit-section>
|
||||||
<bit-section>
|
<bit-section>
|
||||||
<h2 bitTypography="h2">{{ "chooseYourPlan" | i18n }}</h2>
|
<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-radio-button [value]="selectableProduct.product" (change)="changedProduct()">
|
||||||
<bit-label>{{ selectableProduct.nameLocalizationKey | i18n }}</bit-label>
|
<bit-label>{{ selectableProduct.nameLocalizationKey | i18n }}</bit-label>
|
||||||
<bit-hint class="tw-text-sm"
|
<bit-hint class="tw-text-sm"
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-radio-button>
|
</bit-radio-button>
|
||||||
<span *ngIf="selectableProduct.product != productTypes.Free">
|
<span *ngIf="selectableProduct.product != productTypes.Free" class="tw-pl-4">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship"
|
*ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship"
|
||||||
>
|
>
|
||||||
@@ -176,6 +176,7 @@
|
|||||||
!selectableProduct.PasswordManager.basePrice &&
|
!selectableProduct.PasswordManager.basePrice &&
|
||||||
selectableProduct.PasswordManager.hasAdditionalSeatsOption
|
selectableProduct.PasswordManager.hasAdditionalSeatsOption
|
||||||
"
|
"
|
||||||
|
class="tw-pl-4"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
"costPerUser"
|
"costPerUser"
|
||||||
@@ -188,11 +189,11 @@
|
|||||||
}}
|
}}
|
||||||
/{{ "month" | i18n }}
|
/{{ "month" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
<span *ngIf="selectableProduct.product == productTypes.Free">{{
|
<span *ngIf="selectableProduct.product == productTypes.Free" class="tw-pl-4">{{
|
||||||
"freeForever" | i18n
|
"freeForever" | i18n
|
||||||
}}</span>
|
}}</span>
|
||||||
</bit-radio-group>
|
</div>
|
||||||
</div>
|
</bit-radio-group>
|
||||||
</bit-section>
|
</bit-section>
|
||||||
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
|
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
|
||||||
<bit-section
|
<bit-section
|
||||||
@@ -277,126 +278,128 @@
|
|||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
</div>
|
</div>
|
||||||
</bit-section>
|
</bit-section>
|
||||||
<bit-section *ngFor="let selectablePlan of selectablePlans">
|
<bit-section>
|
||||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||||
<bit-radio-group formControlName="plan">
|
<bit-radio-group formControlName="plan">
|
||||||
<bit-radio-button
|
<div *ngFor="let selectablePlan of selectablePlans">
|
||||||
type="radio"
|
<bit-radio-button
|
||||||
id="interval{{ selectablePlan.type }}"
|
type="radio"
|
||||||
[value]="selectablePlan.type"
|
id="interval{{ selectablePlan.type }}"
|
||||||
>
|
[value]="selectablePlan.type"
|
||||||
<bit-label>{{ (selectablePlan.isAnnual ? "annually" : "monthly") | i18n }}</bit-label>
|
>
|
||||||
<bit-hint *ngIf="selectablePlan.isAnnual">
|
<bit-label>{{ (selectablePlan.isAnnual ? "annually" : "monthly") | i18n }}</bit-label>
|
||||||
<p
|
<bit-hint *ngIf="selectablePlan.isAnnual">
|
||||||
class="tw-mb-0"
|
<p
|
||||||
bitTypography="body2"
|
class="tw-mb-0"
|
||||||
*ngIf="selectablePlan.PasswordManager.basePrice"
|
bitTypography="body2"
|
||||||
>
|
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||||
{{ "basePrice" | i18n }}:
|
>
|
||||||
{{
|
{{ "basePrice" | i18n }}:
|
||||||
(selectablePlan.isAnnual
|
{{
|
||||||
? selectablePlan.PasswordManager.basePrice / 12
|
(selectablePlan.isAnnual
|
||||||
: selectablePlan.PasswordManager.basePrice
|
? selectablePlan.PasswordManager.basePrice / 12
|
||||||
) | currency: "$"
|
: selectablePlan.PasswordManager.basePrice
|
||||||
}}
|
) | currency: "$"
|
||||||
× 12
|
}}
|
||||||
{{ "monthAbbr" | i18n }}
|
× 12
|
||||||
=
|
{{ "monthAbbr" | i18n }}
|
||||||
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
=
|
||||||
<span class="tw-line-through">{{
|
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
||||||
selectablePlan.PasswordManager.basePrice | currency: "$"
|
<span class="tw-line-through">{{
|
||||||
}}</span>
|
selectablePlan.PasswordManager.basePrice | currency: "$"
|
||||||
{{ "freeWithSponsorship" | i18n }}
|
}}</span>
|
||||||
</ng-container>
|
{{ "freeWithSponsorship" | i18n }}
|
||||||
<ng-template #notAcceptingSponsorship>
|
</ng-container>
|
||||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
<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 }}
|
/{{ "year" | i18n }}
|
||||||
</ng-template>
|
</p>
|
||||||
</p>
|
<p
|
||||||
<p
|
class="tw-mb-0"
|
||||||
class="tw-mb-0"
|
bitTypography="body2"
|
||||||
bitTypography="body2"
|
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
|
||||||
>
|
|
||||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
|
||||||
>{{ "additionalUsers" | i18n }}:</span
|
|
||||||
>
|
>
|
||||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
{{ "additionalStorageGb" | i18n }}:
|
||||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||||
{{
|
{{
|
||||||
(selectablePlan.isAnnual
|
(selectablePlan.isAnnual
|
||||||
? selectablePlan.PasswordManager.seatPrice / 12
|
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
|
||||||
: selectablePlan.PasswordManager.seatPrice
|
: selectablePlan.PasswordManager.additionalStoragePricePerGb
|
||||||
) | currency: "$"
|
) | currency: "$"
|
||||||
}}
|
}}
|
||||||
× 12 {{ "monthAbbr" | i18n }} =
|
× 12 {{ "monthAbbr" | i18n }} =
|
||||||
{{
|
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
|
||||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
</p>
|
||||||
| currency: "$"
|
</bit-hint>
|
||||||
}}
|
<bit-hint *ngIf="!selectablePlan.isAnnual">
|
||||||
/{{ "year" | i18n }}
|
<p
|
||||||
</p>
|
class="tw-mb-0"
|
||||||
<p
|
bitTypography="body2"
|
||||||
class="tw-mb-0"
|
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||||
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
|
|
||||||
>
|
>
|
||||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
{{ "basePrice" | i18n }}:
|
||||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||||
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
|
{{ "monthAbbr" | i18n }}
|
||||||
{{ "monthAbbr" | i18n }} =
|
=
|
||||||
{{
|
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
/{{ "month" | i18n }}
|
||||||
| currency: "$"
|
</p>
|
||||||
}}
|
<p
|
||||||
/{{ "month" | i18n }}
|
class="tw-mb-0"
|
||||||
</p>
|
bitTypography="body2"
|
||||||
<p
|
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||||
class="tw-mb-0"
|
>
|
||||||
bitTypography="body2"
|
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
>{{ "additionalUsers" | i18n }}:</span
|
||||||
>
|
>
|
||||||
{{ "additionalStorageGb" | i18n }}:
|
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||||
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
|
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
|
||||||
{{ "monthAbbr" | i18n }} =
|
{{ "monthAbbr" | i18n }} =
|
||||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
|
{{
|
||||||
</p>
|
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||||
</bit-hint>
|
| currency: "$"
|
||||||
</bit-radio-button>
|
}}
|
||||||
|
/{{ "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-radio-group>
|
||||||
</bit-section>
|
</bit-section>
|
||||||
</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 {
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
Component,
|
import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core";
|
||||||
ElementRef,
|
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||||
EventEmitter,
|
|
||||||
Input,
|
|
||||||
OnInit,
|
|
||||||
Output,
|
|
||||||
ViewChild,
|
|
||||||
} from "@angular/core";
|
|
||||||
import { firstValueFrom, map } from "rxjs";
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
export interface AddCreditDialogData {
|
||||||
|
organizationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AddCreditDialogResult {
|
||||||
|
Added = "added",
|
||||||
|
Cancelled = "cancelled",
|
||||||
|
}
|
||||||
|
|
||||||
export type PayPalConfig = {
|
export type PayPalConfig = {
|
||||||
businessId?: string;
|
businessId?: string;
|
||||||
@@ -24,17 +28,9 @@ export type PayPalConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-add-credit",
|
templateUrl: "add-credit-dialog.component.html",
|
||||||
templateUrl: "add-credit.component.html",
|
|
||||||
})
|
})
|
||||||
export class AddCreditComponent implements OnInit {
|
export class AddCreditDialogComponent implements OnInit {
|
||||||
@Input() creditAmount: string;
|
|
||||||
@Input() showOptions = true;
|
|
||||||
@Input() method = PaymentMethodType.PayPal;
|
|
||||||
@Input() organizationId: string;
|
|
||||||
@Output() onAdded = new EventEmitter();
|
|
||||||
@Output() onCanceled = new EventEmitter();
|
|
||||||
|
|
||||||
@ViewChild("ppButtonForm", { read: ElementRef, static: true }) ppButtonFormRef: ElementRef;
|
@ViewChild("ppButtonForm", { read: ElementRef, static: true }) ppButtonFormRef: ElementRef;
|
||||||
|
|
||||||
paymentMethodType = PaymentMethodType;
|
paymentMethodType = PaymentMethodType;
|
||||||
@@ -44,14 +40,22 @@ export class AddCreditComponent implements OnInit {
|
|||||||
ppLoading = false;
|
ppLoading = false;
|
||||||
subject: string;
|
subject: string;
|
||||||
returnUrl: string;
|
returnUrl: string;
|
||||||
formPromise: Promise<any>;
|
organizationId: string;
|
||||||
|
|
||||||
private userId: string;
|
private userId: string;
|
||||||
private name: string;
|
private name: string;
|
||||||
private email: string;
|
private email: string;
|
||||||
private region: string;
|
private region: string;
|
||||||
|
|
||||||
|
protected DialogResult = AddCreditDialogResult;
|
||||||
|
protected formGroup = new FormGroup({
|
||||||
|
method: new FormControl(PaymentMethodType.PayPal),
|
||||||
|
creditAmount: new FormControl(null, [Validators.required]),
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private dialogRef: DialogRef,
|
||||||
|
@Inject(DIALOG_DATA) protected data: AddCreditDialogData,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
@@ -59,6 +63,7 @@ export class AddCreditComponent implements OnInit {
|
|||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
|
this.organizationId = data.organizationId;
|
||||||
const payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig;
|
const payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig;
|
||||||
this.ppButtonFormAction = payPalConfig.buttonAction;
|
this.ppButtonFormAction = payPalConfig.buttonAction;
|
||||||
this.ppButtonBusinessId = payPalConfig.businessId;
|
this.ppButtonBusinessId = payPalConfig.businessId;
|
||||||
@@ -93,7 +98,18 @@ export class AddCreditComponent implements OnInit {
|
|||||||
this.returnUrl = window.location.href;
|
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 === "") {
|
if (this.creditAmount == null || this.creditAmount === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -104,33 +120,20 @@ export class AddCreditComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.method === PaymentMethodType.BitPay) {
|
if (this.method === PaymentMethodType.BitPay) {
|
||||||
try {
|
const req = new BitPayInvoiceRequest();
|
||||||
const req = new BitPayInvoiceRequest();
|
req.email = this.email;
|
||||||
req.email = this.email;
|
req.name = this.name;
|
||||||
req.name = this.name;
|
req.credit = true;
|
||||||
req.credit = true;
|
req.amount = this.creditAmountNumber;
|
||||||
req.amount = this.creditAmountNumber;
|
req.organizationId = this.organizationId;
|
||||||
req.organizationId = this.organizationId;
|
req.userId = this.userId;
|
||||||
req.userId = this.userId;
|
req.returnUrl = this.returnUrl;
|
||||||
req.returnUrl = this.returnUrl;
|
const bitPayUrl: string = await this.apiService.postBitPayInvoice(req);
|
||||||
this.formPromise = this.apiService.postBitPayInvoice(req);
|
this.platformUtilsService.launchUri(bitPayUrl);
|
||||||
const bitPayUrl: string = await this.formPromise;
|
|
||||||
this.platformUtilsService.launchUri(bitPayUrl);
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
this.dialogRef.close(AddCreditDialogResult.Added);
|
||||||
this.onAdded.emit();
|
};
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.onCanceled.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
formatAmount() {
|
formatAmount() {
|
||||||
try {
|
try {
|
||||||
@@ -160,3 +163,15 @@ export class AddCreditComponent implements OnInit {
|
|||||||
return null;
|
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 { HeaderModule } from "../../layouts/header/header.module";
|
||||||
import { SharedModule } from "../../shared";
|
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 { AdjustPaymentDialogComponent } from "./adjust-payment-dialog.component";
|
||||||
import { AdjustStorageComponent } from "./adjust-storage.component";
|
import { AdjustStorageComponent } from "./adjust-storage.component";
|
||||||
import { BillingHistoryComponent } from "./billing-history.component";
|
import { BillingHistoryComponent } from "./billing-history.component";
|
||||||
@@ -17,7 +17,7 @@ import { UpdateLicenseComponent } from "./update-license.component";
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule],
|
imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
AddCreditComponent,
|
AddCreditDialogComponent,
|
||||||
AdjustPaymentDialogComponent,
|
AdjustPaymentDialogComponent,
|
||||||
AdjustStorageComponent,
|
AdjustStorageComponent,
|
||||||
BillingHistoryComponent,
|
BillingHistoryComponent,
|
||||||
|
|||||||
@@ -33,22 +33,9 @@
|
|||||||
<strong>{{ creditOrBalance | currency: "$" }}</strong>
|
<strong>{{ creditOrBalance | currency: "$" }}</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>{{ "creditAppliedDesc" | i18n }}</p>
|
<p>{{ "creditAppliedDesc" | i18n }}</p>
|
||||||
<button
|
<button type="button" bitButton buttonType="secondary" [bitAction]="addCredit">
|
||||||
type="button"
|
|
||||||
bitButton
|
|
||||||
buttonType="secondary"
|
|
||||||
(click)="addCredit()"
|
|
||||||
*ngIf="!showAddCredit"
|
|
||||||
>
|
|
||||||
{{ "addCredit" | i18n }}
|
{{ "addCredit" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<app-add-credit
|
|
||||||
[organizationId]="organizationId"
|
|
||||||
(onAdded)="closeAddCredit(true)"
|
|
||||||
(onCanceled)="closeAddCredit(false)"
|
|
||||||
*ngIf="showAddCredit"
|
|
||||||
>
|
|
||||||
</app-add-credit>
|
|
||||||
<h2 class="spaced-header">{{ "paymentMethod" | i18n }}</h2>
|
<h2 class="spaced-header">{{ "paymentMethod" | i18n }}</h2>
|
||||||
<p *ngIf="!paymentSource">{{ "noPaymentMethod" | i18n }}</p>
|
<p *ngIf="!paymentSource">{{ "noPaymentMethod" | i18n }}</p>
|
||||||
<ng-container *ngIf="paymentSource">
|
<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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component";
|
||||||
import {
|
import {
|
||||||
AdjustPaymentDialogResult,
|
AdjustPaymentDialogResult,
|
||||||
openAdjustPaymentDialog,
|
openAdjustPaymentDialog,
|
||||||
@@ -30,7 +31,6 @@ export class PaymentMethodComponent implements OnInit {
|
|||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
firstLoaded = false;
|
firstLoaded = false;
|
||||||
showAddCredit = false;
|
|
||||||
billing: BillingPaymentResponse;
|
billing: BillingPaymentResponse;
|
||||||
org: OrganizationSubscriptionResponse;
|
org: OrganizationSubscriptionResponse;
|
||||||
sub: SubscriptionResponse;
|
sub: SubscriptionResponse;
|
||||||
@@ -111,18 +111,17 @@ export class PaymentMethodComponent implements OnInit {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
addCredit() {
|
addCredit = async () => {
|
||||||
this.showAddCredit = true;
|
const dialogRef = openAddCreditDialog(this.dialogService, {
|
||||||
}
|
data: {
|
||||||
|
organizationId: this.organizationId,
|
||||||
closeAddCredit(load: boolean) {
|
},
|
||||||
this.showAddCredit = false;
|
});
|
||||||
if (load) {
|
const result = await lastValueFrom(dialogRef.closed);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
if (result === AddCreditDialogResult.Added) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await this.load();
|
||||||
this.load();
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
changePayment = async () => {
|
changePayment = async () => {
|
||||||
const dialogRef = openAdjustPaymentDialog(this.dialogService, {
|
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 { TwoFactorVerifyComponent } from "../auth/settings/two-factor-verify.component";
|
||||||
import { TwoFactorWebAuthnComponent } from "../auth/settings/two-factor-webauthn.component";
|
import { TwoFactorWebAuthnComponent } from "../auth/settings/two-factor-webauthn.component";
|
||||||
import { TwoFactorYubiKeyComponent } from "../auth/settings/two-factor-yubikey.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 { UserVerificationModule } from "../auth/shared/components/user-verification";
|
||||||
import { SsoComponent } from "../auth/sso.component";
|
import { SsoComponent } from "../auth/sso.component";
|
||||||
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.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 { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module";
|
||||||
import { UserLayoutComponent } from "../layouts/user-layout.component";
|
import { UserLayoutComponent } from "../layouts/user-layout.component";
|
||||||
import { DomainRulesComponent } from "../settings/domain-rules.component";
|
import { DomainRulesComponent } from "../settings/domain-rules.component";
|
||||||
import { LowKdfComponent } from "../settings/low-kdf.component";
|
|
||||||
import { PreferencesComponent } from "../settings/preferences.component";
|
import { PreferencesComponent } from "../settings/preferences.component";
|
||||||
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
||||||
import { GeneratorComponent } from "../tools/generator.component";
|
import { GeneratorComponent } from "../tools/generator.component";
|
||||||
@@ -186,11 +184,9 @@ import { SharedModule } from "./shared.module";
|
|||||||
UpdatePasswordComponent,
|
UpdatePasswordComponent,
|
||||||
UpdateTempPasswordComponent,
|
UpdateTempPasswordComponent,
|
||||||
VaultTimeoutInputComponent,
|
VaultTimeoutInputComponent,
|
||||||
VerifyEmailComponent,
|
|
||||||
VerifyEmailTokenComponent,
|
VerifyEmailTokenComponent,
|
||||||
VerifyRecoverDeleteComponent,
|
VerifyRecoverDeleteComponent,
|
||||||
VerifyRecoverDeleteProviderComponent,
|
VerifyRecoverDeleteProviderComponent,
|
||||||
LowKdfComponent,
|
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
UserVerificationModule,
|
UserVerificationModule,
|
||||||
@@ -264,11 +260,9 @@ import { SharedModule } from "./shared.module";
|
|||||||
UpdateTempPasswordComponent,
|
UpdateTempPasswordComponent,
|
||||||
UserLayoutComponent,
|
UserLayoutComponent,
|
||||||
VaultTimeoutInputComponent,
|
VaultTimeoutInputComponent,
|
||||||
VerifyEmailComponent,
|
|
||||||
VerifyEmailTokenComponent,
|
VerifyEmailTokenComponent,
|
||||||
VerifyRecoverDeleteComponent,
|
VerifyRecoverDeleteComponent,
|
||||||
VerifyRecoverDeleteProviderComponent,
|
VerifyRecoverDeleteProviderComponent,
|
||||||
LowKdfComponent,
|
|
||||||
HeaderModule,
|
HeaderModule,
|
||||||
DangerZoneComponent,
|
DangerZoneComponent,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component, NgZone } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
|
||||||
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
@@ -21,23 +20,23 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
|||||||
constructor(
|
constructor(
|
||||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||||
stateService: StateService,
|
accountService: AccountService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
|
ngZone: NgZone,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
accountService: AccountService,
|
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
passwordGenerationService,
|
passwordGenerationService,
|
||||||
usernameGenerationService,
|
usernameGenerationService,
|
||||||
platformUtilsService,
|
platformUtilsService,
|
||||||
stateService,
|
accountService,
|
||||||
i18nService,
|
i18nService,
|
||||||
logService,
|
logService,
|
||||||
route,
|
route,
|
||||||
accountService,
|
ngZone,
|
||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
if (platformUtilsService.isSelfHost()) {
|
if (platformUtilsService.isSelfHost()) {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { UntypedFormBuilder } from "@angular/forms";
|
import { UntypedFormBuilder } from "@angular/forms";
|
||||||
|
|
||||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
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 { 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 { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@@ -26,7 +24,6 @@ export class ExportComponent extends BaseExportComponent {
|
|||||||
eventCollectionService: EventCollectionService,
|
eventCollectionService: EventCollectionService,
|
||||||
policyService: PolicyService,
|
policyService: PolicyService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
userVerificationService: UserVerificationService,
|
|
||||||
formBuilder: UntypedFormBuilder,
|
formBuilder: UntypedFormBuilder,
|
||||||
fileDownloadService: FileDownloadService,
|
fileDownloadService: FileDownloadService,
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
@@ -39,84 +36,10 @@ export class ExportComponent extends BaseExportComponent {
|
|||||||
eventCollectionService,
|
eventCollectionService,
|
||||||
policyService,
|
policyService,
|
||||||
logService,
|
logService,
|
||||||
userVerificationService,
|
|
||||||
formBuilder,
|
formBuilder,
|
||||||
fileDownloadService,
|
fileDownloadService,
|
||||||
dialogService,
|
dialogService,
|
||||||
organizationService,
|
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
|
* Whether the user can modify user access to this collection
|
||||||
*/
|
*/
|
||||||
canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
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
|
* Whether the user can modify group access to this collection
|
||||||
*/
|
*/
|
||||||
canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
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
|
<app-vault-header
|
||||||
[filter]="filter"
|
[filter]="filter"
|
||||||
[loading]="refreshing && !performingInitialLoad"
|
[loading]="refreshing && !performingInitialLoad"
|
||||||
@@ -14,8 +16,8 @@
|
|||||||
<app-vault-onboarding [ciphers]="ciphers" [orgs]="allOrganizations" (onAddCipher)="addCipher()">
|
<app-vault-onboarding [ciphers]="ciphers" [orgs]="allOrganizations" (onAddCipher)="addCipher()">
|
||||||
</app-vault-onboarding>
|
</app-vault-onboarding>
|
||||||
|
|
||||||
<div class="row">
|
<div class="tw-flex tw-flex-row -tw-mx-2.5">
|
||||||
<div class="col-3">
|
<div class="tw-basis-1/4 tw-max-w-1/4 tw-px-2.5">
|
||||||
<div class="groupings">
|
<div class="groupings">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="inner-content">
|
<div class="inner-content">
|
||||||
@@ -30,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
||||||
{{ trashCleanupWarning }}
|
{{ trashCleanupWarning }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
@@ -81,44 +83,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<ng-template #attachments></ng-template>
|
<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 { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.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 })
|
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
|
||||||
collectionsModalRef: ViewContainerRef;
|
collectionsModalRef: ViewContainerRef;
|
||||||
|
|
||||||
showVerifyEmail = false;
|
|
||||||
showBrowserOutdated = false;
|
|
||||||
showPremiumCallout = false;
|
|
||||||
showLowKdf = false;
|
|
||||||
trashCleanupWarning: string = null;
|
trashCleanupWarning: string = null;
|
||||||
kdfIterations: number;
|
kdfIterations: number;
|
||||||
activeFilter: VaultFilter = new VaultFilter();
|
activeFilter: VaultFilter = new VaultFilter();
|
||||||
@@ -161,7 +153,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private tokenService: TokenService,
|
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
@@ -180,14 +171,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private searchPipe: SearchPipe,
|
private searchPipe: SearchPipe,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private userVerificationService: UserVerificationService,
|
|
||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
protected kdfConfigService: KdfConfigService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.showBrowserOutdated = window.navigator.userAgent.indexOf("MSIE") !== -1;
|
|
||||||
this.trashCleanupWarning = this.i18nService.t(
|
this.trashCleanupWarning = this.i18nService.t(
|
||||||
this.platformUtilsService.isSelfHost()
|
this.platformUtilsService.isSelfHost()
|
||||||
? "trashCleanupWarningSelfHosted"
|
? "trashCleanupWarningSelfHosted"
|
||||||
@@ -197,18 +185,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
const firstSetup$ = this.route.queryParams.pipe(
|
const firstSetup$ = this.route.queryParams.pipe(
|
||||||
first(),
|
first(),
|
||||||
switchMap(async (params: Params) => {
|
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);
|
await this.syncService.fullSync(false);
|
||||||
|
|
||||||
const canAccessPremium = await firstValueFrom(
|
|
||||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
|
|
||||||
);
|
|
||||||
this.showPremiumCallout =
|
|
||||||
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
|
||||||
|
|
||||||
const cipherId = getCipherIdFromParams(params);
|
const cipherId = getCipherIdFromParams(params);
|
||||||
if (!cipherId) {
|
if (!cipherId) {
|
||||||
return;
|
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() {
|
ngOnDestroy() {
|
||||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
@@ -1005,14 +973,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
: this.cipherService.softDeleteWithServer(id);
|
: 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[]) {
|
protected async repromptCipher(ciphers: CipherView[]) {
|
||||||
const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None);
|
const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { NgModule } from "@angular/core";
|
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 { LooseComponentsModule, SharedModule } from "../../shared";
|
||||||
import { CollectionDialogModule } from "../components/collection-dialog";
|
import { CollectionDialogModule } from "../components/collection-dialog";
|
||||||
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
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 { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module";
|
||||||
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
||||||
import { PipesModule } from "./pipes/pipes.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 { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||||
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./vault-onboarding/services/abstraction/vault-onboarding.service";
|
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./vault-onboarding/services/abstraction/vault-onboarding.service";
|
||||||
@@ -34,10 +37,13 @@ import { VaultComponent } from "./vault.component";
|
|||||||
VaultItemsModule,
|
VaultItemsModule,
|
||||||
CollectionDialogModule,
|
CollectionDialogModule,
|
||||||
VaultOnboardingComponent,
|
VaultOnboardingComponent,
|
||||||
|
BannerModule,
|
||||||
|
VerifyEmailComponent,
|
||||||
],
|
],
|
||||||
declarations: [VaultComponent, VaultHeaderComponent],
|
declarations: [VaultComponent, VaultHeaderComponent, VaultBannersComponent],
|
||||||
exports: [VaultComponent],
|
exports: [VaultComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
VaultBannersService,
|
||||||
{
|
{
|
||||||
provide: VaultOnboardingServiceAbstraction,
|
provide: VaultOnboardingServiceAbstraction,
|
||||||
useClass: VaultOnboardingService,
|
useClass: VaultOnboardingService,
|
||||||
|
|||||||
@@ -1,38 +1,19 @@
|
|||||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="purgeVaultTitle">
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
<bit-dialog dialogSize="default" [title]="'purgeVault' | i18n">
|
||||||
<form
|
<ng-container bitDialogContent>
|
||||||
class="modal-content"
|
<p bitTypography="body1">
|
||||||
#form
|
{{ (organizationId ? "purgeOrgVaultDesc" : "purgeVaultDesc") | i18n }}
|
||||||
(ngSubmit)="submit()"
|
</p>
|
||||||
[appApiAction]="formPromise"
|
<app-callout type="warning">{{ "purgeVaultWarning" | i18n }}</app-callout>
|
||||||
ngNativeValidate
|
<app-user-verification formControlName="masterPassword"></app-user-verification>
|
||||||
>
|
</ng-container>
|
||||||
<div class="modal-header">
|
<ng-container bitDialogFooter>
|
||||||
<h1 class="modal-title" id="purgeVaultTitle">{{ "purgeVault" | i18n }}</h1>
|
<button bitButton bitFormButton type="submit" buttonType="danger">
|
||||||
<button
|
{{ "purgeVault" | i18n }}
|
||||||
type="button"
|
</button>
|
||||||
class="close"
|
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
|
||||||
data-dismiss="modal"
|
{{ "close" | i18n }}
|
||||||
appA11yTitle="{{ 'close' | i18n }}"
|
</button>
|
||||||
>
|
</ng-container>
|
||||||
<span aria-hidden="true">×</span>
|
</bit-dialog>
|
||||||
</button>
|
</form>
|
||||||
</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>
|
|
||||||
|
|||||||
@@ -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 { Router } from "@angular/router";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
export interface PurgeVaultDialogData {
|
||||||
|
organizationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-purge-vault",
|
selector: "app-purge-vault",
|
||||||
templateUrl: "purge-vault.component.html",
|
templateUrl: "purge-vault.component.html",
|
||||||
})
|
})
|
||||||
export class PurgeVaultComponent {
|
export class PurgeVaultComponent {
|
||||||
@Input() organizationId?: string = null;
|
organizationId: string = null;
|
||||||
|
|
||||||
masterPassword: Verification;
|
formGroup = new FormGroup({
|
||||||
formPromise: Promise<unknown>;
|
masterPassword: new FormControl<Verification>(null),
|
||||||
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) protected data: PurgeVaultDialogData,
|
||||||
|
private dialogRef: DialogRef,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private logService: LogService,
|
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
) {}
|
) {
|
||||||
|
this.organizationId = data && data.organizationId ? data.organizationId : null;
|
||||||
|
}
|
||||||
|
|
||||||
async submit() {
|
submit = async () => {
|
||||||
try {
|
const response = this.userVerificationService
|
||||||
this.formPromise = this.userVerificationService
|
.buildRequest(this.formGroup.value.masterPassword)
|
||||||
.buildRequest(this.masterPassword)
|
.then((request) => this.apiService.postPurgeCiphers(request, this.organizationId));
|
||||||
.then((request) => this.apiService.postPurgeCiphers(request, this.organizationId));
|
await response;
|
||||||
await this.formPromise;
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("vaultPurged"));
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("vaultPurged"));
|
await this.syncService.fullSync(true);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
if (this.organizationId != null) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
await this.router.navigate(["organizations", this.organizationId, "vault"]);
|
||||||
this.syncService.fullSync(true);
|
} else {
|
||||||
if (this.organizationId != null) {
|
await this.router.navigate(["vault"]);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
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": {
|
"secureYourInfrastructure": {
|
||||||
"message": "Secure your infrastructure"
|
"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 { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||||
import {
|
import {
|
||||||
PasswordGenerationService,
|
legacyPasswordGenerationServiceFactory,
|
||||||
PasswordGenerationServiceAbstraction,
|
legacyUsernameGenerationServiceFactory,
|
||||||
} from "@bitwarden/common/tools/generator/password";
|
} from "@bitwarden/common/tools/generator";
|
||||||
import {
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||||
UsernameGenerationService,
|
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||||
UsernameGenerationServiceAbstraction,
|
|
||||||
} from "@bitwarden/common/tools/generator/username";
|
|
||||||
import {
|
import {
|
||||||
PasswordStrengthService,
|
PasswordStrengthService,
|
||||||
PasswordStrengthServiceAbstraction,
|
PasswordStrengthServiceAbstraction,
|
||||||
@@ -559,13 +557,27 @@ const safeProviders: SafeProvider[] = [
|
|||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: PasswordGenerationServiceAbstraction,
|
provide: PasswordGenerationServiceAbstraction,
|
||||||
useClass: PasswordGenerationService,
|
useFactory: legacyPasswordGenerationServiceFactory,
|
||||||
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
|
deps: [
|
||||||
|
EncryptService,
|
||||||
|
CryptoServiceAbstraction,
|
||||||
|
PolicyServiceAbstraction,
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
StateProvider,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: UsernameGenerationServiceAbstraction,
|
provide: UsernameGenerationServiceAbstraction,
|
||||||
useClass: UsernameGenerationService,
|
useFactory: legacyUsernameGenerationServiceFactory,
|
||||||
deps: [CryptoServiceAbstraction, StateServiceAbstraction, ApiServiceAbstraction],
|
deps: [
|
||||||
|
ApiServiceAbstraction,
|
||||||
|
I18nServiceAbstraction,
|
||||||
|
CryptoServiceAbstraction,
|
||||||
|
EncryptService,
|
||||||
|
PolicyServiceAbstraction,
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
StateProvider,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: ApiServiceAbstraction,
|
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 { ActivatedRoute } from "@angular/router";
|
||||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
import { BehaviorSubject, combineLatest, firstValueFrom, Subject } from "rxjs";
|
||||||
import { debounceTime, first, map } from "rxjs/operators";
|
import { debounceTime, first, map, skipWhile, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
|
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { GeneratorType } from "@bitwarden/common/tools/generator/generator-type";
|
||||||
import { GeneratorOptions } from "@bitwarden/common/tools/generator/generator-options";
|
|
||||||
import {
|
import {
|
||||||
PasswordGenerationServiceAbstraction,
|
PasswordGenerationServiceAbstraction,
|
||||||
PasswordGeneratorOptions,
|
PasswordGeneratorOptions,
|
||||||
@@ -22,9 +21,9 @@ import {
|
|||||||
import { EmailForwarderOptions } from "@bitwarden/common/tools/models/domain/email-forwarder-options";
|
import { EmailForwarderOptions } from "@bitwarden/common/tools/models/domain/email-forwarder-options";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class GeneratorComponent implements OnInit {
|
export class GeneratorComponent implements OnInit, OnDestroy {
|
||||||
@Input() comingFromAddEdit = false;
|
@Input() comingFromAddEdit = false;
|
||||||
@Input() type: string;
|
@Input() type: GeneratorType | "";
|
||||||
@Output() onSelected = new EventEmitter<string>();
|
@Output() onSelected = new EventEmitter<string>();
|
||||||
|
|
||||||
usernameGeneratingPromise: Promise<string>;
|
usernameGeneratingPromise: Promise<string>;
|
||||||
@@ -43,6 +42,9 @@ export class GeneratorComponent implements OnInit {
|
|||||||
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
|
||||||
usernameWebsite: string = null;
|
usernameWebsite: string = null;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private isInitialized$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
// update screen reader minimum password length with 500ms debounce
|
// update screen reader minimum password length with 500ms debounce
|
||||||
// so that the user isn't flooded with status updates
|
// so that the user isn't flooded with status updates
|
||||||
private _passwordOptionsMinLengthForReader = new BehaviorSubject<number>(
|
private _passwordOptionsMinLengthForReader = new BehaviorSubject<number>(
|
||||||
@@ -53,15 +55,17 @@ export class GeneratorComponent implements OnInit {
|
|||||||
debounceTime(500),
|
debounceTime(500),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _password = new BehaviorSubject<string>("-");
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||||
protected usernameGenerationService: UsernameGenerationServiceAbstraction,
|
protected usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||||
protected platformUtilsService: PlatformUtilsService,
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
protected stateService: StateService,
|
protected accountService: AccountService,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
protected logService: LogService,
|
protected logService: LogService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected accountService: AccountService,
|
protected ngZone: NgZone,
|
||||||
private win: Window,
|
private win: Window,
|
||||||
) {
|
) {
|
||||||
this.typeOptions = [
|
this.typeOptions = [
|
||||||
@@ -92,61 +96,115 @@ export class GeneratorComponent implements OnInit {
|
|||||||
];
|
];
|
||||||
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
|
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
|
||||||
this.catchallOptions = [{ 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() {
|
this.forwardOptions = [
|
||||||
// eslint-disable-next-line rxjs/no-async-subscribe
|
{ name: "", value: "", validForSelfHosted: false },
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
{ name: "addy.io", value: "anonaddy", validForSelfHosted: true },
|
||||||
const passwordOptionsResponse = await this.passwordGenerationService.getOptions();
|
{ name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false },
|
||||||
this.passwordOptions = passwordOptionsResponse[0];
|
{ name: "Fastmail", value: "fastmail", validForSelfHosted: true },
|
||||||
this.enforcedPasswordPolicyOptions = passwordOptionsResponse[1];
|
{ name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },
|
||||||
this.avoidAmbiguous = !this.passwordOptions.ambiguous;
|
{ name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true },
|
||||||
this.passwordOptions.type =
|
{ name: "Forward Email", value: "forwardemail", validForSelfHosted: true },
|
||||||
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
this.usernameOptions = await this.usernameGenerationService.getOptions();
|
this._password.pipe(debounceTime(250)).subscribe((password) => {
|
||||||
if (this.usernameOptions.type == null) {
|
ngZone.run(() => {
|
||||||
this.usernameOptions.type = "word";
|
this.password = password;
|
||||||
}
|
});
|
||||||
if (
|
this.passwordGenerationService.addHistory(this.password).catch((e) => {
|
||||||
this.usernameOptions.subaddressEmail == null ||
|
this.logService.error(e);
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async typeChanged() {
|
cascadeOptions(navigationType: GeneratorType = undefined, accountEmail: string) {
|
||||||
await this.stateService.setGeneratorOptions({ type: this.type } as GeneratorOptions);
|
this.avoidAmbiguous = !this.passwordOptions.ambiguous;
|
||||||
if (this.regenerateWithoutButtonPress()) {
|
|
||||||
await this.regenerate();
|
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() {
|
async regenerate() {
|
||||||
@@ -160,7 +218,7 @@ export class GeneratorComponent implements OnInit {
|
|||||||
async sliderChanged() {
|
async sliderChanged() {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.savePasswordOptions(false);
|
this.savePasswordOptions();
|
||||||
await this.passwordGenerationService.addHistory(this.password);
|
await this.passwordGenerationService.addHistory(this.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,31 +262,34 @@ export class GeneratorComponent implements OnInit {
|
|||||||
|
|
||||||
async sliderInput() {
|
async sliderInput() {
|
||||||
await this.normalizePasswordOptions();
|
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.normalizePasswordOptions();
|
||||||
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
await this.passwordGenerationService.saveOptions(this.passwordOptions);
|
||||||
|
|
||||||
if (regenerate && this.regenerateWithoutButtonPress()) {
|
// restore the original format
|
||||||
await this.regeneratePassword();
|
this.passwordOptions.type = restoreType;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveUsernameOptions(regenerate = true) {
|
async saveUsernameOptions() {
|
||||||
await this.usernameGenerationService.saveOptions(this.usernameOptions);
|
await this.usernameGenerationService.saveOptions(this.usernameOptions);
|
||||||
if (this.usernameOptions.type === "forwarded") {
|
if (this.usernameOptions.type === "forwarded") {
|
||||||
this.username = "-";
|
this.username = "-";
|
||||||
}
|
}
|
||||||
if (regenerate && this.regenerateWithoutButtonPress()) {
|
|
||||||
await this.regenerateUsername();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async regeneratePassword() {
|
async regeneratePassword() {
|
||||||
this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions);
|
this._password.next(
|
||||||
await this.passwordGenerationService.addHistory(this.password);
|
await this.passwordGenerationService.generatePassword(this.passwordOptions),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
regenerateUsername() {
|
regenerateUsername() {
|
||||||
@@ -297,28 +358,5 @@ export class GeneratorComponent implements OnInit {
|
|||||||
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(
|
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(
|
||||||
this.passwordOptions,
|
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 () => {
|
clear = async () => {
|
||||||
this.history = [];
|
this.history = await this.passwordGenerationService.clear();
|
||||||
await this.passwordGenerationService.clear();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
copy(password: string) {
|
copy(password: string) {
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import { AnonLayoutComponent } from "@bitwarden/auth/angular";
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { Icon } from "@bitwarden/components";
|
import { Icon } from "@bitwarden/components";
|
||||||
|
|
||||||
|
export interface AnonLayoutWrapperData {
|
||||||
|
pageTitle?: string;
|
||||||
|
pageSubtitle?: string;
|
||||||
|
pageIcon?: Icon;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: "anon-layout-wrapper.component.html",
|
templateUrl: "anon-layout-wrapper.component.html",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p>
|
<p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p>
|
||||||
</div>
|
</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
|
<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"
|
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
|
Instead the AnonLayoutComponent is implemented solely in the router via routable composition, which
|
||||||
gives us the advantages of nested routes in Angular.
|
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
|
To allow for routable composition, Auth also provides an AnonLayout**Wrapper**Component which embeds
|
||||||
AnonLayout**Wrapper**Component.
|
the AnonLayoutComponent.
|
||||||
|
|
||||||
For clarity:
|
For clarity:
|
||||||
|
|
||||||
- AnonLayoutComponent = the Auth-owned library component - `<auth-anon-layout>`
|
- AnonLayoutComponent = the base, Auth-owned library component - `<auth-anon-layout>`
|
||||||
- AnonLayout**Wrapper**Component = the client-specific wrapper component to be used in a client
|
- AnonLayout**Wrapper**Component = the wrapper to be used in client routing modules
|
||||||
routing module
|
|
||||||
|
|
||||||
The AnonLayout**Wrapper**Component embeds the AnonLayoutComponent along with the router outlets:
|
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
|
pageTitle: "logIn", // example of a translation key from messages.json
|
||||||
pageSubtitle: "loginWithMasterPassword", // 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
|
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.
|
All 3 of these properties are optional.
|
||||||
|
|
||||||
```javascript
|
```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",
|
pageTitle: "logIn",
|
||||||
pageSubtitle: "loginWithMasterPassword",
|
pageSubtitle: "loginWithMasterPassword",
|
||||||
pageIcon: LockIcon,
|
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.
|
// 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">
|
<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 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>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>
|
||||||
@@ -58,12 +58,12 @@ export const WithSecondaryContent: Story = {
|
|||||||
// Notice that slot="secondary" is requred to project any secondary content.
|
// Notice that slot="secondary" is requred to project any secondary content.
|
||||||
`
|
`
|
||||||
<auth-anon-layout [title]="title" [subtitle]="subtitle">
|
<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 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>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>
|
||||||
|
|
||||||
<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>
|
<div class="tw-font-bold tw-mb-2">Secondary Projected Content (optional)</div>
|
||||||
<button bitButton>Perform Action</button>
|
<button bitButton>Perform Action</button>
|
||||||
</div>
|
</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.
|
// 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?">
|
<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 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>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>
|
||||||
|
|
||||||
<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>
|
<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>
|
<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>
|
<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.
|
// 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">
|
<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 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>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>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
export * from "./icons";
|
export * from "./icons";
|
||||||
|
|
||||||
export * from "./anon-layout/anon-layout.component";
|
export * from "./anon-layout/anon-layout.component";
|
||||||
|
export * from "./anon-layout/anon-layout-wrapper.component";
|
||||||
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
||||||
export * from "./password-callout/password-callout.component";
|
export * from "./password-callout/password-callout.component";
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ describe("AuthRequestLoginStrategy", () => {
|
|||||||
|
|
||||||
authRequestLoginStrategy = new AuthRequestLoginStrategy(
|
authRequestLoginStrategy = new AuthRequestLoginStrategy(
|
||||||
cache,
|
cache,
|
||||||
|
deviceTrustService,
|
||||||
accountService,
|
accountService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
@@ -109,7 +110,6 @@ describe("AuthRequestLoginStrategy", () => {
|
|||||||
stateService,
|
stateService,
|
||||||
twoFactorService,
|
twoFactorService,
|
||||||
userDecryptionOptions,
|
userDecryptionOptions,
|
||||||
deviceTrustService,
|
|
||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
vaultTimeoutSettingsService,
|
vaultTimeoutSettingsService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
|
|||||||
@@ -1,28 +1,13 @@
|
|||||||
import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs";
|
import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||||
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 { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
|
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 { 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 { 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 { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
|
||||||
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
||||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||||
|
|
||||||
@@ -51,40 +36,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
data: AuthRequestLoginStrategyData,
|
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,
|
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
|
||||||
kdfConfigService: KdfConfigService,
|
|
||||||
) {
|
) {
|
||||||
super(
|
super(...sharedDeps);
|
||||||
accountService,
|
|
||||||
masterPasswordService,
|
|
||||||
cryptoService,
|
|
||||||
apiService,
|
|
||||||
tokenService,
|
|
||||||
appIdService,
|
|
||||||
platformUtilsService,
|
|
||||||
messagingService,
|
|
||||||
logService,
|
|
||||||
stateService,
|
|
||||||
twoFactorService,
|
|
||||||
userDecryptionOptionsService,
|
|
||||||
billingAccountProfileStateService,
|
|
||||||
vaultTimeoutSettingsService,
|
|
||||||
kdfConfigService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.cache = new BehaviorSubject(data);
|
this.cache = new BehaviorSubject(data);
|
||||||
this.email$ = this.cache.pipe(map((data) => data.tokenRequest.email));
|
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
|
// The base class is abstract so we test it via PasswordLoginStrategy
|
||||||
passwordLoginStrategy = new PasswordLoginStrategy(
|
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||||
cache,
|
cache,
|
||||||
|
passwordStrengthService,
|
||||||
|
policyService,
|
||||||
|
loginStrategyService,
|
||||||
accountService,
|
accountService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
@@ -162,9 +165,6 @@ describe("LoginStrategy", () => {
|
|||||||
stateService,
|
stateService,
|
||||||
twoFactorService,
|
twoFactorService,
|
||||||
userDecryptionOptionsService,
|
userDecryptionOptionsService,
|
||||||
passwordStrengthService,
|
|
||||||
policyService,
|
|
||||||
loginStrategyService,
|
|
||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
vaultTimeoutSettingsService,
|
vaultTimeoutSettingsService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
@@ -461,6 +461,9 @@ describe("LoginStrategy", () => {
|
|||||||
|
|
||||||
passwordLoginStrategy = new PasswordLoginStrategy(
|
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||||
cache,
|
cache,
|
||||||
|
passwordStrengthService,
|
||||||
|
policyService,
|
||||||
|
loginStrategyService,
|
||||||
accountService,
|
accountService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
@@ -473,9 +476,6 @@ describe("LoginStrategy", () => {
|
|||||||
stateService,
|
stateService,
|
||||||
twoFactorService,
|
twoFactorService,
|
||||||
userDecryptionOptionsService,
|
userDecryptionOptionsService,
|
||||||
passwordStrengthService,
|
|
||||||
policyService,
|
|
||||||
loginStrategyService,
|
|
||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
vaultTimeoutSettingsService,
|
vaultTimeoutSettingsService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ describe("PasswordLoginStrategy", () => {
|
|||||||
|
|
||||||
passwordLoginStrategy = new PasswordLoginStrategy(
|
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||||
cache,
|
cache,
|
||||||
|
passwordStrengthService,
|
||||||
|
policyService,
|
||||||
|
loginStrategyService,
|
||||||
accountService,
|
accountService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
@@ -133,9 +136,6 @@ describe("PasswordLoginStrategy", () => {
|
|||||||
stateService,
|
stateService,
|
||||||
twoFactorService,
|
twoFactorService,
|
||||||
userDecryptionOptionsService,
|
userDecryptionOptionsService,
|
||||||
passwordStrengthService,
|
|
||||||
policyService,
|
|
||||||
loginStrategyService,
|
|
||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
vaultTimeoutSettingsService,
|
vaultTimeoutSettingsService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
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 { 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 { 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 { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
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";
|
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 { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response";
|
||||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.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 { 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 { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
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 { MasterKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
|
||||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||||
|
|
||||||
@@ -75,42 +60,12 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
data: PasswordLoginStrategyData,
|
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 passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
|
||||||
kdfConfigService: KdfConfigService,
|
|
||||||
) {
|
) {
|
||||||
super(
|
super(...sharedDeps);
|
||||||
accountService,
|
|
||||||
masterPasswordService,
|
|
||||||
cryptoService,
|
|
||||||
apiService,
|
|
||||||
tokenService,
|
|
||||||
appIdService,
|
|
||||||
platformUtilsService,
|
|
||||||
messagingService,
|
|
||||||
logService,
|
|
||||||
stateService,
|
|
||||||
twoFactorService,
|
|
||||||
userDecryptionOptionsService,
|
|
||||||
billingAccountProfileStateService,
|
|
||||||
vaultTimeoutSettingsService,
|
|
||||||
kdfConfigService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.cache = new BehaviorSubject(data);
|
this.cache = new BehaviorSubject(data);
|
||||||
this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email));
|
this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email));
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ describe("SsoLoginStrategy", () => {
|
|||||||
|
|
||||||
ssoLoginStrategy = new SsoLoginStrategy(
|
ssoLoginStrategy = new SsoLoginStrategy(
|
||||||
null,
|
null,
|
||||||
|
keyConnectorService,
|
||||||
|
deviceTrustService,
|
||||||
|
authRequestService,
|
||||||
|
i18nService,
|
||||||
accountService,
|
accountService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
@@ -130,10 +134,6 @@ describe("SsoLoginStrategy", () => {
|
|||||||
stateService,
|
stateService,
|
||||||
twoFactorService,
|
twoFactorService,
|
||||||
userDecryptionOptionsService,
|
userDecryptionOptionsService,
|
||||||
keyConnectorService,
|
|
||||||
deviceTrustService,
|
|
||||||
authRequestService,
|
|
||||||
i18nService,
|
|
||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
vaultTimeoutSettingsService,
|
vaultTimeoutSettingsService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
|
|||||||
@@ -1,36 +1,19 @@
|
|||||||
import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs";
|
import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||||
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 { 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 { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
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 { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
|
||||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.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 { HttpStatusCode } from "@bitwarden/common/enums";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
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 { 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 { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import {
|
import { AuthRequestServiceAbstraction } from "../abstractions";
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
|
||||||
AuthRequestServiceAbstraction,
|
|
||||||
} from "../abstractions";
|
|
||||||
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
||||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||||
|
|
||||||
@@ -84,43 +67,13 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
data: SsoLoginStrategyData,
|
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 keyConnectorService: KeyConnectorService,
|
||||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||||
private authRequestService: AuthRequestServiceAbstraction,
|
private authRequestService: AuthRequestServiceAbstraction,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
|
||||||
kdfConfigService: KdfConfigService,
|
|
||||||
) {
|
) {
|
||||||
super(
|
super(...sharedDeps);
|
||||||
accountService,
|
|
||||||
masterPasswordService,
|
|
||||||
cryptoService,
|
|
||||||
apiService,
|
|
||||||
tokenService,
|
|
||||||
appIdService,
|
|
||||||
platformUtilsService,
|
|
||||||
messagingService,
|
|
||||||
logService,
|
|
||||||
stateService,
|
|
||||||
twoFactorService,
|
|
||||||
userDecryptionOptionsService,
|
|
||||||
billingAccountProfileStateService,
|
|
||||||
vaultTimeoutSettingsService,
|
|
||||||
kdfConfigService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.cache = new BehaviorSubject(data);
|
this.cache = new BehaviorSubject(data);
|
||||||
this.email$ = this.cache.pipe(map((state) => state.email));
|
this.email$ = this.cache.pipe(map((state) => state.email));
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ describe("UserApiLoginStrategy", () => {
|
|||||||
|
|
||||||
apiLogInStrategy = new UserApiLoginStrategy(
|
apiLogInStrategy = new UserApiLoginStrategy(
|
||||||
cache,
|
cache,
|
||||||
|
environmentService,
|
||||||
|
keyConnectorService,
|
||||||
accountService,
|
accountService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
@@ -106,8 +108,6 @@ describe("UserApiLoginStrategy", () => {
|
|||||||
stateService,
|
stateService,
|
||||||
twoFactorService,
|
twoFactorService,
|
||||||
userDecryptionOptionsService,
|
userDecryptionOptionsService,
|
||||||
environmentService,
|
|
||||||
keyConnectorService,
|
|
||||||
billingAccountProfileStateService,
|
billingAccountProfileStateService,
|
||||||
vaultTimeoutSettingsService,
|
vaultTimeoutSettingsService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
|
|||||||
@@ -1,28 +1,13 @@
|
|||||||
import { firstValueFrom, BehaviorSubject } from "rxjs";
|
import { firstValueFrom, BehaviorSubject } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
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 { 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 { 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 { 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 { 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 { 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 { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
|
|
||||||
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
||||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||||
|
|
||||||
@@ -44,41 +29,12 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
data: UserApiLoginStrategyData,
|
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 environmentService: EnvironmentService,
|
||||||
private keyConnectorService: KeyConnectorService,
|
private keyConnectorService: KeyConnectorService,
|
||||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
|
||||||
protected kdfConfigService: KdfConfigService,
|
|
||||||
) {
|
) {
|
||||||
super(
|
super(...sharedDeps);
|
||||||
accountService,
|
|
||||||
masterPasswordService,
|
|
||||||
cryptoService,
|
|
||||||
apiService,
|
|
||||||
tokenService,
|
|
||||||
appIdService,
|
|
||||||
platformUtilsService,
|
|
||||||
messagingService,
|
|
||||||
logService,
|
|
||||||
stateService,
|
|
||||||
twoFactorService,
|
|
||||||
userDecryptionOptionsService,
|
|
||||||
billingAccountProfileStateService,
|
|
||||||
vaultTimeoutSettingsService,
|
|
||||||
kdfConfigService,
|
|
||||||
);
|
|
||||||
this.cache = new BehaviorSubject(data);
|
this.cache = new BehaviorSubject(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,13 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
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 { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
|
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 { 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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions";
|
|
||||||
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
||||||
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||||
|
|
||||||
@@ -46,39 +31,9 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
data: WebAuthnLoginStrategyData,
|
data: WebAuthnLoginStrategyData,
|
||||||
accountService: AccountService,
|
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||||
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,
|
|
||||||
) {
|
) {
|
||||||
super(
|
super(...sharedDeps);
|
||||||
accountService,
|
|
||||||
masterPasswordService,
|
|
||||||
cryptoService,
|
|
||||||
apiService,
|
|
||||||
tokenService,
|
|
||||||
appIdService,
|
|
||||||
platformUtilsService,
|
|
||||||
messagingService,
|
|
||||||
logService,
|
|
||||||
stateService,
|
|
||||||
twoFactorService,
|
|
||||||
userDecryptionOptionsService,
|
|
||||||
billingAccountProfileStateService,
|
|
||||||
vaultTimeoutSettingsService,
|
|
||||||
kdfConfigService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.cache = new BehaviorSubject(data);
|
this.cache = new BehaviorSubject(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { MasterKey } from "@bitwarden/common/types/key";
|
|||||||
import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions";
|
import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions";
|
||||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
|
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
|
||||||
import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy";
|
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 { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy";
|
||||||
import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy";
|
import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy";
|
||||||
import { UserApiLoginStrategy } from "../../login-strategies/user-api-login.strategy";
|
import { UserApiLoginStrategy } from "../../login-strategies/user-api-login.strategy";
|
||||||
@@ -338,6 +339,24 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||||||
private initializeLoginStrategy(
|
private initializeLoginStrategy(
|
||||||
source: Observable<[AuthenticationType | null, CacheData | null]>,
|
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(
|
return source.pipe(
|
||||||
map(([strategy, data]) => {
|
map(([strategy, data]) => {
|
||||||
if (strategy == null) {
|
if (strategy == null) {
|
||||||
@@ -347,108 +366,35 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||||||
case AuthenticationType.Password:
|
case AuthenticationType.Password:
|
||||||
return new PasswordLoginStrategy(
|
return new PasswordLoginStrategy(
|
||||||
data?.password,
|
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.passwordStrengthService,
|
||||||
this.policyService,
|
this.policyService,
|
||||||
this,
|
this,
|
||||||
this.billingAccountProfileStateService,
|
...sharedDeps,
|
||||||
this.vaultTimeoutSettingsService,
|
|
||||||
this.kdfConfigService,
|
|
||||||
);
|
);
|
||||||
case AuthenticationType.Sso:
|
case AuthenticationType.Sso:
|
||||||
return new SsoLoginStrategy(
|
return new SsoLoginStrategy(
|
||||||
data?.sso,
|
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.keyConnectorService,
|
||||||
this.deviceTrustService,
|
this.deviceTrustService,
|
||||||
this.authRequestService,
|
this.authRequestService,
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.billingAccountProfileStateService,
|
...sharedDeps,
|
||||||
this.vaultTimeoutSettingsService,
|
|
||||||
this.kdfConfigService,
|
|
||||||
);
|
);
|
||||||
case AuthenticationType.UserApiKey:
|
case AuthenticationType.UserApiKey:
|
||||||
return new UserApiLoginStrategy(
|
return new UserApiLoginStrategy(
|
||||||
data?.userApiKey,
|
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.environmentService,
|
||||||
this.keyConnectorService,
|
this.keyConnectorService,
|
||||||
this.billingAccountProfileStateService,
|
...sharedDeps,
|
||||||
this.vaultTimeoutSettingsService,
|
|
||||||
this.kdfConfigService,
|
|
||||||
);
|
);
|
||||||
case AuthenticationType.AuthRequest:
|
case AuthenticationType.AuthRequest:
|
||||||
return new AuthRequestLoginStrategy(
|
return new AuthRequestLoginStrategy(
|
||||||
data?.authRequest,
|
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.deviceTrustService,
|
||||||
this.billingAccountProfileStateService,
|
...sharedDeps,
|
||||||
this.vaultTimeoutSettingsService,
|
|
||||||
this.kdfConfigService,
|
|
||||||
);
|
);
|
||||||
case AuthenticationType.WebAuthn:
|
case AuthenticationType.WebAuthn:
|
||||||
return new WebAuthnLoginStrategy(
|
return new WebAuthnLoginStrategy(data?.webAuthn, ...sharedDeps);
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export class PasswordGeneratorPolicyOptions extends Domain {
|
|||||||
*/
|
*/
|
||||||
inEffect() {
|
inEffect() {
|
||||||
return (
|
return (
|
||||||
this.defaultType !== "" ||
|
this.defaultType ||
|
||||||
this.minLength > 0 ||
|
this.minLength > 0 ||
|
||||||
this.numberCount > 0 ||
|
this.numberCount > 0 ||
|
||||||
this.specialCount > 0 ||
|
this.specialCount > 0 ||
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||||
masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
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
|
// 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> {
|
async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise<UserKey> {
|
||||||
|
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||||
const userKey = await this.getKeyFromStorage(keySuffix, userId);
|
const userKey = await this.getKeyFromStorage(keySuffix, userId);
|
||||||
if (userKey) {
|
if (userKey) {
|
||||||
if (!(await this.validateUserKey(userKey))) {
|
if (!(await this.validateUserKey(userKey, userId))) {
|
||||||
this.logService.warning("Invalid key, throwing away stored keys");
|
this.logService.warning("Invalid key, throwing away stored keys");
|
||||||
await this.clearAllStoredUserKeys(userId);
|
await this.clearAllStoredUserKeys(userId);
|
||||||
}
|
}
|
||||||
@@ -663,13 +664,15 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---HELPERS---
|
// ---HELPERS---
|
||||||
protected async validateUserKey(key: UserKey): Promise<boolean> {
|
protected async validateUserKey(key: UserKey, userId: UserId): Promise<boolean> {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const encPrivateKey = await firstValueFrom(this.activeUserEncryptedPrivateKeyState.state$);
|
const encPrivateKey = await firstValueFrom(
|
||||||
|
this.stateProvider.getUserState$(USER_ENCRYPTED_PRIVATE_KEY, userId),
|
||||||
|
);
|
||||||
if (encPrivateKey == null) {
|
if (encPrivateKey == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,3 +160,7 @@ export const CIPHERS_DISK_LOCAL = new StateDefinition("ciphersLocal", "disk", {
|
|||||||
export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory", {
|
export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory", {
|
||||||
browser: "memory-large-object",
|
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 { KnownAccountsMigrator } from "./migrations/60-known-accounts";
|
||||||
import { PinStateMigrator } from "./migrations/61-move-pin-state-to-providers";
|
import { PinStateMigrator } from "./migrations/61-move-pin-state-to-providers";
|
||||||
import { VaultTimeoutSettingsServiceStateProviderMigrator } from "./migrations/62-migrate-vault-timeout-settings-svc-to-state-provider";
|
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 { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||||
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
|
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
|
||||||
import { MinVersionMigrator } from "./migrations/min-version";
|
import { MinVersionMigrator } from "./migrations/min-version";
|
||||||
|
|
||||||
export const MIN_VERSION = 3;
|
export const MIN_VERSION = 3;
|
||||||
export const CURRENT_VERSION = 62;
|
export const CURRENT_VERSION = 65;
|
||||||
export type MinVersion = typeof MIN_VERSION;
|
export type MinVersion = typeof MIN_VERSION;
|
||||||
|
|
||||||
export function createMigrationBuilder() {
|
export function createMigrationBuilder() {
|
||||||
@@ -130,7 +133,10 @@ export function createMigrationBuilder() {
|
|||||||
.with(KdfConfigMigrator, 58, 59)
|
.with(KdfConfigMigrator, 58, 59)
|
||||||
.with(KnownAccountsMigrator, 59, 60)
|
.with(KnownAccountsMigrator, 59, 60)
|
||||||
.with(PinStateMigrator, 60, 61)
|
.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(
|
export async function currentVersion(
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import {
|
|||||||
// Represents data in state service pre-migration
|
// Represents data in state service pre-migration
|
||||||
function preMigrationJson() {
|
function preMigrationJson() {
|
||||||
return {
|
return {
|
||||||
|
// desktop only global data format
|
||||||
|
"global.vaultTimeout": -1,
|
||||||
|
"global.vaultTimeoutAction": "lock",
|
||||||
|
|
||||||
global: {
|
global: {
|
||||||
vaultTimeout: 30,
|
vaultTimeout: 30,
|
||||||
vaultTimeoutAction: "lock",
|
vaultTimeoutAction: "lock",
|
||||||
@@ -267,6 +271,10 @@ describe("VaultTimeoutSettingsServiceStateProviderMigrator", () => {
|
|||||||
otherStuff: "otherStuff",
|
otherStuff: "otherStuff",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Expect we removed desktop specially formatted global data
|
||||||
|
expect(helper.remove).toHaveBeenCalledWith("global\\.vaultTimeout");
|
||||||
|
expect(helper.remove).toHaveBeenCalledWith("global\\.vaultTimeoutAction");
|
||||||
|
|
||||||
// User data
|
// User data
|
||||||
expect(helper.set).toHaveBeenCalledWith("user1", {
|
expect(helper.set).toHaveBeenCalledWith("user1", {
|
||||||
settings: {
|
settings: {
|
||||||
|
|||||||
@@ -122,10 +122,15 @@ export class VaultTimeoutSettingsServiceStateProviderMigrator extends Migrator<6
|
|||||||
|
|
||||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
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?.vaultTimeout;
|
||||||
delete globalData?.vaultTimeoutAction;
|
delete globalData?.vaultTimeoutAction;
|
||||||
await helper.set("global", globalData);
|
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> {
|
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);
|
||||||
|
}
|
||||||
@@ -38,6 +38,12 @@ export abstract class GeneratorHistoryService {
|
|||||||
*/
|
*/
|
||||||
take: (userId: UserId, credential: string) => Promise<GeneratedCredential | null>;
|
take: (userId: UserId, credential: string) => Promise<GeneratedCredential | null>;
|
||||||
|
|
||||||
|
/** Deletes a user's credential history.
|
||||||
|
* @param userId identifies the user taking the credential.
|
||||||
|
* @returns A promise that completes when the history is cleared.
|
||||||
|
*/
|
||||||
|
clear: (userId: UserId) => Promise<GeneratedCredential[]>;
|
||||||
|
|
||||||
/** Lists all credentials for a user.
|
/** Lists all credentials for a user.
|
||||||
* @param userId identifies the user listing the credential.
|
* @param userId identifies the user listing the credential.
|
||||||
* @remarks This field is eventually consistent with `track` and `take` operations.
|
* @remarks This field is eventually consistent with `track` and `take` operations.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user