1
0
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:
Cesar Gonzalez
2024-05-21 09:59:32 -05:00
143 changed files with 3816 additions and 2239 deletions

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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];

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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, () => {});

View File

@@ -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,

View File

@@ -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"),

View File

@@ -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();
} }

View File

@@ -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;

View File

@@ -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>

View File

@@ -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,

View File

@@ -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);

View File

@@ -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

View File

@@ -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>

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
); );
} }

View File

@@ -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">&times;</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>

View File

@@ -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);
}
} }

View File

@@ -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">&times;</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>

View File

@@ -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);
}
} }

View File

@@ -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 }}"

View File

@@ -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>

View File

@@ -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();
} }

View File

@@ -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>

View File

@@ -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>!@#$%^&amp;*</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">!@#$%^&amp;*</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>

View File

@@ -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],
}); });

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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,

View File

@@ -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>

View File

@@ -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";
} }
} }
} }

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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); };
}
}
} }

View File

@@ -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>

View File

@@ -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,
) {} ) {}

View File

@@ -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: "$"
&times; 12 }}
{{ "monthAbbr" | i18n }} &times; 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 }} &times;
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.seatPrice / 12
: selectablePlan.PasswordManager.seatPrice
) | currency: "$"
}}
&times; 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 }} &times; {{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ {{
(selectablePlan.isAnnual (selectablePlan.isAnnual
? selectablePlan.PasswordManager.seatPrice / 12 ? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
: selectablePlan.PasswordManager.seatPrice : selectablePlan.PasswordManager.additionalStoragePricePerGb
) | currency: "$" ) | currency: "$"
}} }}
&times; 12 {{ "monthAbbr" | i18n }} = &times; 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 }} &times;
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
: selectablePlan.PasswordManager.additionalStoragePricePerGb
) | currency: "$"
}}
&times; 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 }} &times; {{ 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 }} &times; {{ formGroup.controls["additionalSeats"].value || 0 }} &times;
{{ 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 }} &times;
{{ 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>

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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">&times;</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>

View File

@@ -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,

View File

@@ -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">

View File

@@ -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, {

View File

@@ -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>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-low-kdf",
templateUrl: "low-kdf.component.html",
})
export class LowKdfComponent {}

View File

@@ -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,
], ],

View File

@@ -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()) {

View File

@@ -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;
}
} }

View File

@@ -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)
);
} }
/** /**

View File

@@ -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);
});
});
});

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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([]);
});
});
});
});
});

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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,

View File

@@ -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">&times;</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>

View File

@@ -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);
} }
} }

View File

@@ -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"
}, },

View File

@@ -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,

View File

@@ -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));
} }
} }

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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"
> >

View File

@@ -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,
} }
``` ```

View File

@@ -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>

View File

@@ -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";

View File

@@ -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,

View File

@@ -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));

View File

@@ -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,

View File

@@ -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,

View File

@@ -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));

View File

@@ -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,

View File

@@ -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));

View File

@@ -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,

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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,
);
} }
}), }),
); );

View File

@@ -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 ||

View File

@@ -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;
} }

View File

@@ -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");

View File

@@ -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(

View File

@@ -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: {

View File

@@ -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> {

View File

@@ -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);
});
});
});

View File

@@ -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);
}

View File

@@ -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);
});
});
});

View File

@@ -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
}
}

View File

@@ -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);
});
});
});

View File

@@ -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);
}

View File

@@ -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