mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
Merge branch 'main' into autofill/pm-8027-inline-menu-appears-within-input-fields-that-do-not-relate-to-user-login
This commit is contained in:
@@ -153,7 +153,7 @@ describe("AccountSwitcherService", () => {
|
||||
|
||||
await selectAccountPromise;
|
||||
|
||||
expect(accountService.switchAccount).toBeCalledWith(null);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("switchAccount", { userId: null });
|
||||
|
||||
expect(removeListenerSpy).toBeCalledTimes(1);
|
||||
});
|
||||
@@ -176,7 +176,7 @@ describe("AccountSwitcherService", () => {
|
||||
|
||||
await selectAccountPromise;
|
||||
|
||||
expect(accountService.switchAccount).toBeCalledWith("1");
|
||||
expect(messagingService.send).toHaveBeenCalledWith("switchAccount", { userId: "1" });
|
||||
expect(messagingService.send).toBeCalledWith(
|
||||
"switchAccount",
|
||||
matches((payload) => {
|
||||
|
||||
@@ -134,7 +134,6 @@ export class AccountSwitcherService {
|
||||
const switchAccountFinishedPromise = this.listenForSwitchAccountFinish(userId);
|
||||
|
||||
// Initiate the actions required to make account switching happen
|
||||
await this.accountService.switchAccount(userId);
|
||||
this.messagingService.send("switchAccount", { userId }); // This message should cause switchAccountFinish to be sent
|
||||
|
||||
// Wait until we receive the switchAccountFinished message
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mock, mockReset } from "jest-mock-extended";
|
||||
import { mock, MockProxy, mockReset } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
@@ -62,7 +62,8 @@ describe("OverlayBackground", () => {
|
||||
let overlayBackground: OverlayBackground;
|
||||
const cipherService = mock<CipherService>();
|
||||
const autofillService = mock<AutofillService>();
|
||||
const authService = mock<AuthService>();
|
||||
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
|
||||
const environmentService = mock<EnvironmentService>();
|
||||
environmentService.environment$ = new BehaviorSubject(
|
||||
@@ -94,6 +95,9 @@ describe("OverlayBackground", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
||||
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
|
||||
authService = mock<AuthService>();
|
||||
authService.activeAccountStatus$ = activeAccountStatusMock$;
|
||||
overlayBackground = new OverlayBackground(
|
||||
cipherService,
|
||||
autofillService,
|
||||
@@ -166,11 +170,11 @@ describe("OverlayBackground", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
});
|
||||
|
||||
it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => {
|
||||
overlayBackground["userAuthStatus"] = AuthenticationStatus.Locked;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
jest.spyOn(BrowserApi, "getTabFromCurrentWindowId");
|
||||
jest.spyOn(cipherService, "getAllDecryptedForUrl");
|
||||
|
||||
|
||||
@@ -136,7 +136,8 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
* list of ciphers if the extension is not unlocked.
|
||||
*/
|
||||
async updateOverlayCiphers() {
|
||||
if (this.userAuthStatus !== AuthenticationStatus.Unlocked) {
|
||||
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -167,7 +168,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
|
||||
private async getOverlayCipherData(): Promise<OverlayCipherData[]> {
|
||||
const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
||||
const overlayCiphersArray = Array.from(this.overlayLoginCiphers);
|
||||
const overlayCipherData = [];
|
||||
const overlayCipherData: OverlayCipherData[] = [];
|
||||
let loginCipherIcon: WebsiteIconData;
|
||||
|
||||
for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
||||
@@ -109,9 +110,12 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
// Autofill user settings loaded from state can await the active account state indefinitely
|
||||
// if not guarded by an active account check (e.g. the user is logged in)
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
|
||||
let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off;
|
||||
let autoFillOnPageLoadIsEnabled = false;
|
||||
const overlayVisibility = await this.getOverlayVisibility();
|
||||
|
||||
if (activeAccount) {
|
||||
overlayVisibility = await this.getOverlayVisibility();
|
||||
}
|
||||
|
||||
const mainAutofillScript = overlayVisibility
|
||||
? "bootstrap-autofill-overlay.js"
|
||||
|
||||
@@ -144,13 +144,11 @@ import { NotificationsService } from "@bitwarden/common/services/notifications.s
|
||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||
import {
|
||||
PasswordGenerationService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/password";
|
||||
import {
|
||||
UsernameGenerationService,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/username";
|
||||
legacyPasswordGenerationServiceFactory,
|
||||
legacyUsernameGenerationServiceFactory,
|
||||
} from "@bitwarden/common/tools/generator";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
@@ -649,10 +647,12 @@ export default class MainBackground {
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
|
||||
this.passwordGenerationService = new PasswordGenerationService(
|
||||
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||
this.encryptService,
|
||||
this.cryptoService,
|
||||
this.policyService,
|
||||
this.stateService,
|
||||
this.accountService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider);
|
||||
@@ -1092,10 +1092,14 @@ export default class MainBackground {
|
||||
this.vaultTimeoutSettingsService,
|
||||
);
|
||||
|
||||
this.usernameGenerationService = new UsernameGenerationService(
|
||||
this.cryptoService,
|
||||
this.stateService,
|
||||
this.usernameGenerationService = legacyUsernameGenerationServiceFactory(
|
||||
this.apiService,
|
||||
this.i18nService,
|
||||
this.cryptoService,
|
||||
this.encryptService,
|
||||
this.policyService,
|
||||
this.accountService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
if (!this.popupOnlyContext) {
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface OffscreenDocument {
|
||||
}
|
||||
|
||||
export abstract class OffscreenDocumentService {
|
||||
abstract offscreenApiSupported(): boolean;
|
||||
abstract withDocument<T>(
|
||||
reasons: chrome.offscreen.Reason[],
|
||||
justification: string,
|
||||
|
||||
@@ -49,6 +49,12 @@ describe.each([
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("offscreenApiSupported", () => {
|
||||
it("indicates whether the offscreen API is supported", () => {
|
||||
expect(sut.offscreenApiSupported()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withDocument", () => {
|
||||
it("creates a document when none exists", async () => {
|
||||
await sut.withDocument(reasons, justification, () => {});
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
export class DefaultOffscreenDocumentService implements DefaultOffscreenDocumentService {
|
||||
import { OffscreenDocumentService } from "./abstractions/offscreen-document";
|
||||
|
||||
export class DefaultOffscreenDocumentService implements OffscreenDocumentService {
|
||||
private workerCount = 0;
|
||||
|
||||
constructor(private logService: LogService) {}
|
||||
|
||||
offscreenApiSupported(): boolean {
|
||||
return typeof chrome.offscreen !== "undefined";
|
||||
}
|
||||
|
||||
async withDocument<T>(
|
||||
reasons: chrome.offscreen.Reason[],
|
||||
justification: string,
|
||||
|
||||
@@ -229,9 +229,7 @@ describe("Browser Utils Service", () => {
|
||||
|
||||
it("copies the passed text using the offscreen document if the extension is using manifest v3", async () => {
|
||||
const text = "test";
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
offscreenDocumentService.offscreenApiSupported.mockReturnValue(true);
|
||||
getManifestVersionSpy.mockReturnValue(3);
|
||||
|
||||
browserPlatformUtilsService.copyToClipboard(text);
|
||||
@@ -304,9 +302,7 @@ describe("Browser Utils Service", () => {
|
||||
});
|
||||
|
||||
it("reads the clipboard text using the offscreen document", async () => {
|
||||
jest
|
||||
.spyOn(browserPlatformUtilsService, "getDevice")
|
||||
.mockReturnValue(DeviceType.ChromeExtension);
|
||||
offscreenDocumentService.offscreenApiSupported.mockReturnValue(true);
|
||||
getManifestVersionSpy.mockReturnValue(3);
|
||||
offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) =>
|
||||
Promise.resolve("test"),
|
||||
|
||||
@@ -243,7 +243,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
||||
text = "\u0000";
|
||||
}
|
||||
|
||||
if (this.isChrome() && BrowserApi.isManifestVersion(3)) {
|
||||
if (BrowserApi.isManifestVersion(3) && this.offscreenDocumentService.offscreenApiSupported()) {
|
||||
void this.triggerOffscreenCopyToClipboard(text).then(handleClipboardWriteCallback);
|
||||
|
||||
return;
|
||||
@@ -268,7 +268,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
||||
return await SafariApp.sendMessageToApp("readFromClipboard");
|
||||
}
|
||||
|
||||
if (this.isChrome() && BrowserApi.isManifestVersion(3)) {
|
||||
if (BrowserApi.isManifestVersion(3) && this.offscreenDocumentService.offscreenApiSupported()) {
|
||||
return await this.triggerOffscreenReadFromClipboard();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Location } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -29,22 +28,22 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
stateService: StateService,
|
||||
accountService: AccountService,
|
||||
cipherService: CipherService,
|
||||
route: ActivatedRoute,
|
||||
logService: LogService,
|
||||
accountService: AccountService,
|
||||
ngZone: NgZone,
|
||||
private location: Location,
|
||||
) {
|
||||
super(
|
||||
passwordGenerationService,
|
||||
usernameGenerationService,
|
||||
platformUtilsService,
|
||||
stateService,
|
||||
accountService,
|
||||
i18nService,
|
||||
logService,
|
||||
route,
|
||||
accountService,
|
||||
ngZone,
|
||||
window,
|
||||
);
|
||||
this.cipherService = cipherService;
|
||||
|
||||
@@ -29,11 +29,6 @@
|
||||
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<app-user-verification ngDefaultControl formControlName="secret" name="Secret">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div id="confirmIdentityHelp" class="box-footer">
|
||||
<p>{{ "confirmIdentity" | i18n }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Router } from "@angular/router";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -27,7 +26,6 @@ export class ExportComponent extends BaseExportComponent {
|
||||
policyService: PolicyService,
|
||||
private router: Router,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
formBuilder: UntypedFormBuilder,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
@@ -40,7 +38,6 @@ export class ExportComponent extends BaseExportComponent {
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
formBuilder,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
|
||||
@@ -103,10 +103,8 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa
|
||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||
import {
|
||||
PasswordGenerationService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/generator/password";
|
||||
import { legacyPasswordGenerationServiceFactory } from "@bitwarden/common/tools/generator";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
@@ -499,10 +497,12 @@ export class ServiceContainer {
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
|
||||
this.passwordGenerationService = new PasswordGenerationService(
|
||||
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||
this.encryptService,
|
||||
this.cryptoService,
|
||||
this.policyService,
|
||||
this.stateService,
|
||||
this.accountService,
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.devicesApiService = new DevicesApiServiceImplementation(this.apiService);
|
||||
|
||||
@@ -411,7 +411,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.masterPasswordService.forceSetPasswordReason$(message.userId),
|
||||
)) != ForceSetPasswordReason.None;
|
||||
if (locked) {
|
||||
this.messagingService.send("locked", { userId: message.userId });
|
||||
this.modalService.closeAll();
|
||||
await this.router.navigate(["lock"]);
|
||||
} else if (forcedPasswordReset) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
|
||||
@@ -21,11 +21,6 @@
|
||||
<option *ngFor="let f of formatOptions" [value]="f.value">{{ f.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<app-user-verification ngDefaultControl formControlName="secret" name="secret">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div id="confirmIdentityHelp" class="box-footer">
|
||||
<p>{{ "confirmIdentity" | i18n }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { UntypedFormBuilder } from "@angular/forms";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -24,7 +23,6 @@ export class ExportComponent extends BaseExportComponent implements OnInit {
|
||||
exportService: VaultExportServiceAbstraction,
|
||||
eventCollectionService: EventCollectionService,
|
||||
policyService: PolicyService,
|
||||
userVerificationService: UserVerificationService,
|
||||
formBuilder: UntypedFormBuilder,
|
||||
logService: LogService,
|
||||
fileDownloadService: FileDownloadService,
|
||||
@@ -38,7 +36,6 @@ export class ExportComponent extends BaseExportComponent implements OnInit {
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
formBuilder,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -36,10 +35,6 @@ describe("GeneratorComponent", () => {
|
||||
provide: UsernameGenerationServiceAbstraction,
|
||||
useValue: mock<UsernameGenerationServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: StateService,
|
||||
useValue: mock<StateService>(),
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: platformUtilsServiceMock,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
||||
@@ -6,7 +6,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
|
||||
@@ -18,22 +17,22 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
||||
constructor(
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||
stateService: StateService,
|
||||
accountService: AccountService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
ngZone: NgZone,
|
||||
logService: LogService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
passwordGenerationService,
|
||||
usernameGenerationService,
|
||||
platformUtilsService,
|
||||
stateService,
|
||||
accountService,
|
||||
i18nService,
|
||||
logService,
|
||||
route,
|
||||
accountService,
|
||||
ngZone,
|
||||
window,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,115 +1,99 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="bulkTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="bulkTitle">
|
||||
{{ "confirmUsers" | i18n }}
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
<bit-dialog dialogSize="large" [title]="'confirmUsers' | i18n" [loading]="loading">
|
||||
<ng-container bitDialogContent>
|
||||
<app-callout type="danger" *ngIf="filteredUsers.length <= 0">
|
||||
{{ "noSelectedUsersApplicable" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!loading && !done">
|
||||
<p bitTypography="body1">
|
||||
{{ "fingerprintEnsureIntegrityVerify" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/fingerprint-phrase/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="card-body text-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</div>
|
||||
<app-callout type="danger" *ngIf="filteredUsers.length <= 0">
|
||||
{{ "noSelectedUsersApplicable" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!loading && !done">
|
||||
<p>
|
||||
{{ "fingerprintEnsureIntegrityVerify" | i18n }}
|
||||
<a
|
||||
href="https://bitwarden.com/help/fingerprint-phrase/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ "learnMore" | i18n }}</a
|
||||
>
|
||||
</p>
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th>{{ "fingerprint" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of filteredUsers">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ fingerprints.get(user.id) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngFor="let user of excludedUsers">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading && done">
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of filteredUsers">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="statuses.has(user.id)">
|
||||
{{ statuses.get(user.id) }}
|
||||
</td>
|
||||
<td *ngIf="!statuses.has(user.id)">
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
*ngIf="!done"
|
||||
[disabled]="loading"
|
||||
(click)="submit()"
|
||||
{{ "learnMore" | i18n }}</a
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "confirm" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell colspan="2">{{ "user" | i18n }}</th>
|
||||
<th bitCell>{{ "fingerprint" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of filteredUsers" alignContent="middle">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
<p class="tw-text-muted tw-text-sm" *ngIf="user.name">{{ user.name }}</p>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ fingerprints.get(user.id) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngFor="let user of excludedUsers" alignContent="middle">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
<p class="tw-text-muted tw-text-sm" *ngIf="user.name">{{ user.name }}</p>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading && done">
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell colspan="2">{{ "user" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of filteredUsers" alignContent="middle">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
<p class="tw-text-muted tw-text-sm" *ngIf="user.name">{{ user.name }}</p>
|
||||
</td>
|
||||
<td bitCell *ngIf="statuses.has(user.id)">
|
||||
{{ statuses.get(user.id) }}
|
||||
</td>
|
||||
<td bitCell *ngIf="!statuses.has(user.id)">
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
*ngIf="!done"
|
||||
bitButton
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
(click)="submit()"
|
||||
[disabled]="loading"
|
||||
>
|
||||
{{ "confirm" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
@@ -8,16 +9,22 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
type BulkConfirmDialogData = {
|
||||
organizationId: string;
|
||||
users: BulkUserDetails[];
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-bulk-confirm",
|
||||
templateUrl: "bulk-confirm.component.html",
|
||||
})
|
||||
export class BulkConfirmComponent implements OnInit {
|
||||
@Input() organizationId: string;
|
||||
@Input() users: BulkUserDetails[];
|
||||
organizationId: string;
|
||||
users: BulkUserDetails[];
|
||||
|
||||
excludedUsers: BulkUserDetails[];
|
||||
filteredUsers: BulkUserDetails[];
|
||||
@@ -30,11 +37,15 @@ export class BulkConfirmComponent implements OnInit {
|
||||
error: string;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: BulkConfirmDialogData,
|
||||
protected cryptoService: CryptoService,
|
||||
protected apiService: ApiService,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
) {
|
||||
this.organizationId = data.organizationId;
|
||||
this.users = data.users;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.excludedUsers = this.users.filter((u) => !this.isAccepted(u));
|
||||
@@ -110,4 +121,8 @@ export class BulkConfirmComponent implements OnInit {
|
||||
request,
|
||||
);
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<BulkConfirmDialogData>) {
|
||||
return dialogService.open(BulkConfirmComponent, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,88 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="bulkTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="bulkTitle">
|
||||
{{ "removeUsers" | i18n }}
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-callout type="danger" *ngIf="users.length <= 0">
|
||||
{{ "noSelectedUsersApplicable" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!done">
|
||||
<app-callout type="warning" *ngIf="users.length > 0 && !error">
|
||||
<p>{{ removeUsersWarning }}</p>
|
||||
<p *ngIf="this.showNoMasterPasswordWarning">
|
||||
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
|
||||
</p>
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of users">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="this.showNoMasterPasswordWarning">
|
||||
<span class="text-muted d-block tw-lowercase">
|
||||
<ng-container *ngIf="user.hasMasterPassword === true"> - </ng-container>
|
||||
<ng-container *ngIf="user.hasMasterPassword === false">
|
||||
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{ "noMasterPassword" | i18n }}
|
||||
</ng-container>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<bit-dialog dialogSize="large" [title]="'removeUsers' | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<app-callout type="danger" *ngIf="users.length <= 0">
|
||||
{{ "noSelectedUsersApplicable" | i18n }}
|
||||
</app-callout>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="!done">
|
||||
<app-callout type="warning" *ngIf="users.length > 0 && !error">
|
||||
<p bitTypography="body1">{{ removeUsersWarning }}</p>
|
||||
<p *ngIf="this.showNoMasterPasswordWarning" bitTypography="body1">
|
||||
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
|
||||
</p>
|
||||
</app-callout>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell colspan="2">{{ "user" | i18n }}</th>
|
||||
<th bitCell *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="done">
|
||||
<table class="table table-hover table-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">{{ "user" | i18n }}</th>
|
||||
<th>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let user of users">
|
||||
<td width="30">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
{{ user.email }}
|
||||
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="statuses.has(user.id)">
|
||||
{{ statuses.get(user.id) }}
|
||||
</td>
|
||||
<td *ngIf="!statuses.has(user.id)">
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of users">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
<small class="tw-text-muted tw-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td bitCell *ngIf="this.showNoMasterPasswordWarning">
|
||||
<span class="tw-text-muted tw-block tw-lowercase">
|
||||
<ng-container *ngIf="user.hasMasterPassword === true"> - </ng-container>
|
||||
<ng-container *ngIf="user.hasMasterPassword === false">
|
||||
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{ "noMasterPassword" | i18n }}
|
||||
</ng-container>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="done">
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell colspan="2">{{ "user" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
*ngIf="!done && users.length > 0"
|
||||
[disabled]="loading"
|
||||
(click)="submit()"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "removeUsers" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let user of users">
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td bitCell>
|
||||
{{ user.email }}
|
||||
<small class="tw-text-muted tw-block" *ngIf="user.name">{{ user.name }}</small>
|
||||
</td>
|
||||
<td *ngIf="statuses.has(user.id)" bitCell>
|
||||
{{ statuses.get(user.id) }}
|
||||
</td>
|
||||
<td *ngIf="!statuses.has(user.id)" bitCell>
|
||||
{{ "bulkFilteredMessage" | i18n }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
*ngIf="!done && users.length > 0"
|
||||
bitButton
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
[disabled]="loading"
|
||||
[bitAction]="submit"
|
||||
>
|
||||
{{ "removeUsers" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
type BulkRemoveDialogData = {
|
||||
organizationId: string;
|
||||
users: BulkUserDetails[];
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-bulk-remove",
|
||||
templateUrl: "bulk-remove.component.html",
|
||||
})
|
||||
export class BulkRemoveComponent {
|
||||
@Input() organizationId: string;
|
||||
@Input() set users(value: BulkUserDetails[]) {
|
||||
this._users = value;
|
||||
this.showNoMasterPasswordWarning = this._users.some(
|
||||
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
|
||||
);
|
||||
}
|
||||
|
||||
get users(): BulkUserDetails[] {
|
||||
return this._users;
|
||||
}
|
||||
|
||||
private _users: BulkUserDetails[];
|
||||
organizationId: string;
|
||||
users: BulkUserDetails[];
|
||||
|
||||
statuses: Map<string, string> = new Map();
|
||||
|
||||
@@ -34,12 +30,19 @@ export class BulkRemoveComponent {
|
||||
showNoMasterPasswordWarning = false;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: BulkRemoveDialogData,
|
||||
protected apiService: ApiService,
|
||||
protected i18nService: I18nService,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
) {}
|
||||
) {
|
||||
this.organizationId = data.organizationId;
|
||||
this.users = data.users;
|
||||
this.showNoMasterPasswordWarning = this.users.some(
|
||||
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
|
||||
);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
submit = async () => {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await this.deleteUsers();
|
||||
@@ -54,7 +57,7 @@ export class BulkRemoveComponent {
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
protected async deleteUsers() {
|
||||
return await this.organizationUserService.deleteManyOrganizationUsers(
|
||||
@@ -66,4 +69,8 @@ export class BulkRemoveComponent {
|
||||
protected get removeUsersWarning() {
|
||||
return this.i18nService.t("removeOrgUsersConfirmation");
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<BulkRemoveDialogData>) {
|
||||
return dialogService.open(BulkRemoveComponent, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<bit-tab-group
|
||||
*ngIf="!loading && organization$ | async as organization"
|
||||
@@ -22,7 +22,7 @@
|
||||
>
|
||||
<bit-tab [label]="'role' | i18n">
|
||||
<ng-container *ngIf="!editMode">
|
||||
<p>{{ "inviteUserDesc" | i18n }}</p>
|
||||
<p bitTypography="body1">{{ "inviteUserDesc" | i18n }}</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||
<input id="emails" type="text" appAutoFocus bitInput formControlName="emails" />
|
||||
@@ -32,13 +32,11 @@
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
<fieldset role="radiogroup" aria-labelledby="roleGroupLabel" class="tw-mb-6">
|
||||
<legend
|
||||
id="roleGroupLabel"
|
||||
class="tw-mb-2 tw-block tw-text-base tw-font-semibold tw-text-main"
|
||||
>
|
||||
<bit-radio-group formControlName="type">
|
||||
<bit-label>
|
||||
{{ "memberRole" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
@@ -46,112 +44,63 @@
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</legend>
|
||||
<div class="tw-mb-2 tw-flex tw-items-baseline">
|
||||
<input
|
||||
type="radio"
|
||||
id="userTypeUser"
|
||||
[value]="organizationUserType.User"
|
||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
||||
formControlName="type"
|
||||
name="type"
|
||||
/>
|
||||
<label class="tw-m-0" for="userTypeUser">
|
||||
{{ "user" | i18n }}
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
{{ "userDesc" | i18n }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
</bit-label>
|
||||
<bit-radio-button id="userTypeUser" [value]="organizationUserType.User">
|
||||
<bit-label>{{ "user" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "userDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button
|
||||
*ngIf="!organization.flexibleCollections"
|
||||
class="tw-mb-2 tw-flex tw-items-baseline"
|
||||
id="userTypeManager"
|
||||
[value]="organizationUserType.Manager"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id="userTypeManager"
|
||||
[value]="organizationUserType.Manager"
|
||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
||||
formControlName="type"
|
||||
name="type"
|
||||
/>
|
||||
<label class="tw-m-0" for="userTypeManager">
|
||||
{{ "manager" | i18n }}
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
{{ "managerDesc" | i18n }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="tw-mb-2 tw-flex tw-items-baseline">
|
||||
<input
|
||||
type="radio"
|
||||
id="userTypeAdmin"
|
||||
[value]="organizationUserType.Admin"
|
||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
||||
formControlName="type"
|
||||
name="type"
|
||||
/>
|
||||
<label class="tw-m-0" for="userTypeAdmin">
|
||||
{{ "admin" | i18n }}
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
{{ "adminDesc" | i18n }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="tw-mb-2 tw-flex tw-items-baseline">
|
||||
<input
|
||||
type="radio"
|
||||
id="userTypeOwner"
|
||||
[value]="organizationUserType.Owner"
|
||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
||||
formControlName="type"
|
||||
name="type"
|
||||
/>
|
||||
<label class="tw-m-0" for="userTypeOwner">
|
||||
{{ "owner" | i18n }}
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
{{ "ownerDesc" | i18n }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="tw-flex tw-items-baseline">
|
||||
<input
|
||||
type="radio"
|
||||
id="userTypeCustom"
|
||||
[value]="organizationUserType.Custom"
|
||||
formControlName="type"
|
||||
name="type"
|
||||
class="tw-relative tw-bottom-[-1px] tw-mr-2"
|
||||
[attr.disabled]="!organization.useCustomPermissions || null"
|
||||
/>
|
||||
<label class="tw-m-0" for="userTypeCustom">
|
||||
{{ "custom" | i18n }}
|
||||
<bit-label>{{ "manager" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "managerDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="userTypeAdmin" [value]="organizationUserType.Admin">
|
||||
<bit-label>{{ "admin" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "adminDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="userTypeOwner" [value]="organizationUserType.Owner">
|
||||
<bit-label>{{ "owner" | i18n }}</bit-label>
|
||||
<bit-hint>{{ "ownerDesc" | i18n }}</bit-hint>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button
|
||||
id="userTypeCustom"
|
||||
[value]="organizationUserType.Custom"
|
||||
[disabled]="!organization.useCustomPermissions || null"
|
||||
>
|
||||
<bit-label>{{ "custom" | i18n }}</bit-label>
|
||||
<bit-hint>
|
||||
<ng-container *ngIf="!organization.useCustomPermissions; else enterprise">
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
<p>
|
||||
{{ "customDescNonEnterpriseStart" | i18n
|
||||
}}<a href="https://bitwarden.com/contact/" target="_blank" rel="noreferrer">{{
|
||||
"customDescNonEnterpriseLink" | i18n
|
||||
}}</a
|
||||
}}<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/contact/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>{{ "customDescNonEnterpriseLink" | i18n }}</a
|
||||
>{{ "customDescNonEnterpriseEnd" | i18n }}
|
||||
</div>
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-template #enterprise>
|
||||
<div class="text-base tw-block tw-font-normal tw-text-muted">
|
||||
{{ "customDesc" | i18n }}
|
||||
</div>
|
||||
<p>{{ "customDesc" | i18n }}</p>
|
||||
</ng-template>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
<ng-container *ngIf="customUserTypeSelected">
|
||||
<ng-container *ngIf="!organization.flexibleCollections; else customPermissionsFC">
|
||||
<h3 class="mt-4 d-flex tw-font-semibold">
|
||||
<h3 bitTypography="h3">
|
||||
{{ "permissions" | i18n }}
|
||||
</h3>
|
||||
<div class="row" [formGroup]="permissionsGroup">
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<label class="tw-font-semibold">{{ "managerPermissions" | i18n }}</label>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
|
||||
<div class="tw-col-span-6">
|
||||
<div class="tw-mb-3">
|
||||
<bit-label class="tw-font-semibold">{{
|
||||
"managerPermissions" | i18n
|
||||
}}</bit-label>
|
||||
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
||||
<app-nested-checkbox
|
||||
parentId="manageAssignedCollections"
|
||||
@@ -160,221 +109,126 @@
|
||||
</app-nested-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</label>
|
||||
<div class="tw-col-span-6">
|
||||
<div class="tw-mb-3">
|
||||
<bit-label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</bit-label>
|
||||
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessEventLogs"
|
||||
id="accessEventLogs"
|
||||
formControlName="accessEventLogs"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessEventLogs">
|
||||
{{ "accessEventLogs" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessImportExport"
|
||||
id="accessImportExport"
|
||||
formControlName="accessImportExport"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessImportExport">
|
||||
{{ "accessImportExport" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessReports"
|
||||
id="accessReports"
|
||||
formControlName="accessReports"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessReports">
|
||||
{{ "accessReports" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessEventLogs" />
|
||||
<bit-label>{{ "accessEventLogs" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessImportExport" />
|
||||
<bit-label>{{ "accessImportExport" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessReports" />
|
||||
<bit-label>{{ "accessReports" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<app-nested-checkbox
|
||||
parentId="manageAllCollections"
|
||||
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
||||
>
|
||||
</app-nested-checkbox>
|
||||
<div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageGroups" />
|
||||
<bit-label>{{ "manageGroups" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageSso" />
|
||||
<bit-label>{{ "manageSso" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="managePolicies" />
|
||||
<bit-label>{{ "managePolicies" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageGroups"
|
||||
id="manageGroups"
|
||||
formControlName="manageGroups"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageGroups">
|
||||
{{ "manageGroups" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageSso"
|
||||
id="manageSso"
|
||||
formControlName="manageSso"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageSso">
|
||||
{{ "manageSso" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="managePolicies"
|
||||
id="managePolicies"
|
||||
formControlName="managePolicies"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="managePolicies">
|
||||
{{ "managePolicies" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageUsers"
|
||||
id="manageUsers"
|
||||
bitCheckbox
|
||||
formControlName="manageUsers"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageUsers">
|
||||
{{ "manageUsers" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageResetPassword"
|
||||
id="manageResetPassword"
|
||||
bitCheckbox
|
||||
formControlName="manageResetPassword"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageResetPassword">
|
||||
{{ "manageAccountRecovery" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #customPermissionsFC>
|
||||
<div class="row" [formGroup]="permissionsGroup">
|
||||
<div class="col-4">
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessEventLogs"
|
||||
id="accessEventLogs"
|
||||
formControlName="accessEventLogs"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessEventLogs">
|
||||
{{ "accessEventLogs" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessImportExport"
|
||||
id="accessImportExport"
|
||||
formControlName="accessImportExport"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessImportExport">
|
||||
{{ "accessImportExport" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="accessReports"
|
||||
id="accessReports"
|
||||
formControlName="accessReports"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="accessReports">
|
||||
{{ "accessReports" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
|
||||
<div class="tw-col-span-4">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessEventLogs" />
|
||||
<bit-label>{{ "accessEventLogs" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessImportExport" />
|
||||
<bit-label>{{ "accessImportExport" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="accessReports" />
|
||||
<bit-label>{{ "accessReports" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="tw-col-span-4">
|
||||
<app-nested-checkbox
|
||||
parentId="manageAllCollections"
|
||||
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
||||
>
|
||||
</app-nested-checkbox>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="mb-3">
|
||||
<div>
|
||||
<div class="tw-col-span-4">
|
||||
<div class="tw-mb-3">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageGroups" />
|
||||
<bit-label>{{ "manageGroups" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageSso" />
|
||||
<bit-label>{{ "manageSso" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="managePolicies" />
|
||||
<bit-label>{{ "managePolicies" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageGroups"
|
||||
id="manageGroups"
|
||||
formControlName="manageGroups"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageGroups">
|
||||
{{ "manageGroups" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageSso"
|
||||
id="manageSso"
|
||||
formControlName="manageSso"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageSso">
|
||||
{{ "manageSso" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="managePolicies"
|
||||
id="managePolicies"
|
||||
formControlName="managePolicies"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="managePolicies">
|
||||
{{ "managePolicies" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageUsers"
|
||||
id="manageUsers"
|
||||
bitCheckbox
|
||||
formControlName="manageUsers"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageUsers">
|
||||
{{ "manageUsers" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="manageResetPassword"
|
||||
id="manageResetPassword"
|
||||
bitCheckbox
|
||||
formControlName="manageResetPassword"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<label class="!tw-font-normal" for="manageResetPassword">
|
||||
{{ "manageAccountRecovery" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="organization.useSecretsManager">
|
||||
<h3 class="mt-4">
|
||||
<h3 class="tw-mt-4">
|
||||
{{ "secretsManager" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
@@ -436,6 +290,7 @@
|
||||
<bit-label>
|
||||
{{ "accessAllCollectionsDesc" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
<div [formGroup]="checkboxes">
|
||||
<input
|
||||
type="checkbox"
|
||||
[name]="pascalize(parentId)"
|
||||
[id]="parentId"
|
||||
[formControlName]="parentId"
|
||||
[indeterminate]="parentIndeterminate"
|
||||
/>
|
||||
<label class="!tw-font-normal" [for]="parentId">
|
||||
{{ parentId | i18n }}
|
||||
</label>
|
||||
<div class="tw-ml-6">
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
[formControlName]="parentId"
|
||||
[indeterminate]="parentIndeterminate"
|
||||
/>
|
||||
<bit-label>{{ parentId | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<div class="tw-ml-4">
|
||||
<ng-container *ngFor="let c of checkboxes.controls | keyvalue; trackBy: key">
|
||||
<div class="" *ngIf="c.key != parentId">
|
||||
<input
|
||||
class=""
|
||||
type="checkbox"
|
||||
[name]="pascalize(c.key)"
|
||||
[id]="c.key"
|
||||
[formControl]="c.value"
|
||||
(change)="onChildCheck()"
|
||||
/>
|
||||
<label class="!tw-font-normal" [for]="c.key">
|
||||
{{ c.key | i18n }}
|
||||
</label>
|
||||
<div *ngIf="c.key != parentId">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="c.value" (change)="onChildCheck()" />
|
||||
<bit-label>{{ c.key | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -481,16 +481,13 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
||||
return;
|
||||
}
|
||||
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
BulkRemoveComponent,
|
||||
this.bulkRemoveModalRef,
|
||||
(comp) => {
|
||||
comp.organizationId = this.organization.id;
|
||||
comp.users = this.getCheckedUsers();
|
||||
const dialogRef = BulkRemoveComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organization.id,
|
||||
users: this.getCheckedUsers(),
|
||||
},
|
||||
);
|
||||
|
||||
await modal.onClosedPromise();
|
||||
});
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
await this.load();
|
||||
}
|
||||
|
||||
@@ -558,16 +555,14 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
||||
return;
|
||||
}
|
||||
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
BulkConfirmComponent,
|
||||
this.bulkConfirmModalRef,
|
||||
(comp) => {
|
||||
comp.organizationId = this.organization.id;
|
||||
comp.users = this.getCheckedUsers();
|
||||
const dialogRef = BulkConfirmComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organization.id,
|
||||
users: this.getCheckedUsers(),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await modal.onClosedPromise();
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
await this.load();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,7 @@
|
||||
{{ "disableSendExemption" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
@@ -1,144 +1,64 @@
|
||||
<div [formGroup]="data">
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6 tw-mb-0">
|
||||
<bit-label>{{ "defaultType" | i18n }}</bit-label>
|
||||
<bit-select formControlName="defaultType" id="defaultType">
|
||||
<bit-option *ngFor="let o of defaultTypes" [value]="o.value" [label]="o.name"></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 form-group mb-0">
|
||||
<label for="defaultType">{{ "defaultType" | i18n }}</label>
|
||||
<select
|
||||
id="defaultType"
|
||||
name="defaultType"
|
||||
formControlName="defaultType"
|
||||
class="form-control"
|
||||
>
|
||||
<option *ngFor="let o of defaultTypes" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<h3 bitTypography="h3" class="tw-mt-4">{{ "password" | i18n }}</h3>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minLength" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="5" max="128" formControlName="minLength" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<h3 class="mt-4">{{ "password" | i18n }}</h3>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="minLength">{{ "minLength" | i18n }}</label>
|
||||
<input
|
||||
id="minLength"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="minLength"
|
||||
min="5"
|
||||
max="128"
|
||||
formControlName="minLength"
|
||||
/>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minNumbers" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="0" max="9" formControlName="minNumbers" />
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minSpecial" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="0" max="9" formControlName="minSpecial" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="minNumbers">{{ "minNumbers" | i18n }}</label>
|
||||
<input
|
||||
id="minNumbers"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="minNumbers"
|
||||
min="0"
|
||||
max="9"
|
||||
formControlName="minNumbers"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="minSpecial">{{ "minSpecial" | i18n }}</label>
|
||||
<input
|
||||
id="minSpecial"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="minSpecial"
|
||||
min="0"
|
||||
max="9"
|
||||
formControlName="minSpecial"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="useUpper"
|
||||
formControlName="useUpper"
|
||||
name="useUpper"
|
||||
/>
|
||||
<label class="form-check-label" for="useUpper">A-Z</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="useLower"
|
||||
name="useLower"
|
||||
formControlName="useLower"
|
||||
/>
|
||||
<label class="form-check-label" for="useLower">a-z</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="useNumbers"
|
||||
name="useNumbers"
|
||||
formControlName="useNumbers"
|
||||
/>
|
||||
<label class="form-check-label" for="useNumbers">0-9</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="useSpecial"
|
||||
name="useSpecial"
|
||||
formControlName="useSpecial"
|
||||
/>
|
||||
<label class="form-check-label" for="useSpecial">!@#$%^&*</label>
|
||||
</div>
|
||||
<h3 class="mt-4">{{ "passphrase" | i18n }}</h3>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="minNumberWords">{{ "minimumNumberOfWords" | i18n }}</label>
|
||||
<input
|
||||
id="minNumberWords"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="minNumberWords"
|
||||
min="3"
|
||||
max="20"
|
||||
formControlName="minNumberWords"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="capitalize"
|
||||
name="capitalize"
|
||||
formControlName="capitalize"
|
||||
/>
|
||||
<label class="form-check-label" for="capitalize">{{ "capitalize" | i18n }}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="includeNumber"
|
||||
name="includeNumber"
|
||||
formControlName="includeNumber"
|
||||
/>
|
||||
<label class="form-check-label" for="includeNumber">{{ "includeNumber" | i18n }}</label>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useUpper" id="useUpper" />
|
||||
<bit-label>A-Z</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useLower" id="useLower" />
|
||||
<bit-label>a-z</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useNumbers" id="useNumbers" />
|
||||
<bit-label>0-9</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="useSpecial" id="useSpecial" />
|
||||
<bit-label>!@#$%^&*</bit-label>
|
||||
</bit-form-control>
|
||||
<h3 bitTypography="h3" class="tw-mt-4">{{ "passphrase" | i18n }}</h3>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "minimumNumberOfWords" | i18n }}</bit-label>
|
||||
<input bitInput type="number" min="3" max="20" formControlName="minNumberWords" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="capitalize" id="capitalize" />
|
||||
<bit-label>{{ "capitalize" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="includeNumber" id="includeNumber" />
|
||||
<bit-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { UntypedFormBuilder } from "@angular/forms";
|
||||
import { UntypedFormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -20,14 +20,14 @@ export class PasswordGeneratorPolicy extends BasePolicy {
|
||||
export class PasswordGeneratorPolicyComponent extends BasePolicyComponent {
|
||||
data = this.formBuilder.group({
|
||||
defaultType: [null],
|
||||
minLength: [null],
|
||||
minLength: [null, [Validators.min(5), Validators.max(128)]],
|
||||
useUpper: [null],
|
||||
useLower: [null],
|
||||
useNumbers: [null],
|
||||
useSpecial: [null],
|
||||
minNumbers: [null],
|
||||
minSpecial: [null],
|
||||
minNumberWords: [null],
|
||||
minNumbers: [null, [Validators.min(0), Validators.max(9)]],
|
||||
minSpecial: [null, [Validators.min(0), Validators.max(9)]],
|
||||
minNumberWords: [null, [Validators.min(3), Validators.max(20)]],
|
||||
capitalize: [null],
|
||||
includeNumber: [null],
|
||||
});
|
||||
|
||||
@@ -5,15 +5,7 @@
|
||||
{{ "requireSsoExemption" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
@@ -2,29 +2,15 @@
|
||||
{{ "sendOptionsExemption" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<div [formGroup]="data">
|
||||
<h3 class="mt-4">{{ "options" | i18n }}</h3>
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="disableHideEmail"
|
||||
name="DisableHideEmail"
|
||||
formControlName="disableHideEmail"
|
||||
/>
|
||||
<label class="form-check-label" for="disableHideEmail">{{ "disableHideEmail" | i18n }}</label>
|
||||
</div>
|
||||
<h3 bitTypography="h3" class="tw-mt-4">{{ "options" | i18n }}</h3>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="disableHideEmail" id="disableHideEmail" />
|
||||
<bit-label>{{ "disableHideEmail" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,7 @@
|
||||
{{ "singleOrgPolicyWarning" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enabled"
|
||||
[formControl]="enabled"
|
||||
name="Enabled"
|
||||
/>
|
||||
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
@@ -104,12 +104,11 @@
|
||||
<button type="button" bitButton buttonType="danger" (click)="deleteOrganization()">
|
||||
{{ "deleteOrganization" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="danger" (click)="purgeVault()">
|
||||
<button type="button" bitButton buttonType="danger" [bitAction]="purgeVault">
|
||||
{{ "purgeVault" | i18n }}
|
||||
</button>
|
||||
</app-danger-zone>
|
||||
|
||||
<ng-template #purgeOrganizationTemplate></ng-template>
|
||||
<ng-template #apiKeyTemplate></ng-template>
|
||||
<ng-template #rotateApiKeyTemplate></ng-template>
|
||||
</bit-container>
|
||||
|
||||
@@ -28,8 +28,6 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./
|
||||
templateUrl: "account.component.html",
|
||||
})
|
||||
export class AccountComponent {
|
||||
@ViewChild("purgeOrganizationTemplate", { read: ViewContainerRef, static: true })
|
||||
purgeModalRef: ViewContainerRef;
|
||||
@ViewChild("apiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
apiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
@@ -232,11 +230,14 @@ export class AccountComponent {
|
||||
}
|
||||
}
|
||||
|
||||
async purgeVault() {
|
||||
await this.modalService.openViewRef(PurgeVaultComponent, this.purgeModalRef, (comp) => {
|
||||
comp.organizationId = this.organizationId;
|
||||
purgeVault = async () => {
|
||||
const dialogRef = PurgeVaultComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
};
|
||||
|
||||
async viewApiKey() {
|
||||
await this.modalService.openViewRef(ApiKeyComponent, this.apiKeyModalRef, (comp) => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ActivatedRoute } from "@angular/router";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -30,7 +29,6 @@ export class OrganizationVaultExportComponent extends ExportComponent {
|
||||
private route: ActivatedRoute,
|
||||
policyService: PolicyService,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
formBuilder: UntypedFormBuilder,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
@@ -43,7 +41,6 @@ export class OrganizationVaultExportComponent extends ExportComponent {
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
formBuilder,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
|
||||
@@ -1,50 +1,36 @@
|
||||
<td>
|
||||
<td bitCell>
|
||||
{{ sponsoringOrg.familySponsorshipFriendlyName }}
|
||||
</td>
|
||||
<td>{{ sponsoringOrg.name }}</td>
|
||||
<td>
|
||||
<td bitCell>{{ sponsoringOrg.name }}</td>
|
||||
<td bitCell>
|
||||
<span [ngClass]="statusClass">{{ statusMessage }}</span>
|
||||
</td>
|
||||
<td class="table-action-right">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<td bitCell>
|
||||
<button
|
||||
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
buttonType="main"
|
||||
[bitMenuTriggerFor]="appListDropdown"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #appListDropdown>
|
||||
<button
|
||||
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
|
||||
class="btn btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownMenuButton"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
bitMenuItem
|
||||
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
|
||||
(click)="resendEmail()"
|
||||
[attr.aria-label]="'resendEmailLabel' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||
>
|
||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
||||
{{ "resendEmail" | i18n }}
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<button
|
||||
type="button"
|
||||
#resendEmailBtn
|
||||
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
|
||||
[appApiAction]="resendEmailPromise"
|
||||
class="dropdown-item btn-submit"
|
||||
[disabled]="$any(resendEmailBtn).loading"
|
||||
(click)="resendEmail()"
|
||||
[attr.aria-label]="'resendEmailLabel' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "resendEmail" | i18n }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
#revokeSponsorshipBtn
|
||||
[appApiAction]="revokeSponsorshipPromise"
|
||||
class="dropdown-item text-danger btn-submit"
|
||||
[disabled]="$any(revokeSponsorshipBtn).loading"
|
||||
(click)="revokeSponsorship()"
|
||||
[attr.aria-label]="'revokeAccount' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "remove" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="revokeSponsorship()"
|
||||
[attr.aria-label]="'revokeAccount' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||
>
|
||||
<span class="tw-text-danger">{{ "remove" | i18n }}</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
|
||||
@@ -20,10 +20,7 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
@Output() sponsorshipRemoved = new EventEmitter();
|
||||
|
||||
statusMessage = "loading";
|
||||
statusClass: "text-success" | "text-danger" = "text-success";
|
||||
|
||||
revokeSponsorshipPromise: Promise<any>;
|
||||
resendEmailPromise: Promise<any>;
|
||||
statusClass: "tw-text-success" | "tw-text-danger" = "tw-text-success";
|
||||
|
||||
private locale = "";
|
||||
|
||||
@@ -48,20 +45,15 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
|
||||
async revokeSponsorship() {
|
||||
try {
|
||||
this.revokeSponsorshipPromise = this.doRevokeSponsorship();
|
||||
await this.revokeSponsorshipPromise;
|
||||
await this.doRevokeSponsorship();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
this.revokeSponsorshipPromise = null;
|
||||
}
|
||||
|
||||
async resendEmail() {
|
||||
this.resendEmailPromise = this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
|
||||
await this.resendEmailPromise;
|
||||
await this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("emailSent"));
|
||||
this.resendEmailPromise = null;
|
||||
}
|
||||
|
||||
get isSentAwaitingSync() {
|
||||
@@ -106,31 +98,31 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
"revokeWhenExpired",
|
||||
formatDate(validUntil, "MM/dd/yyyy", this.locale),
|
||||
);
|
||||
this.statusClass = "text-danger";
|
||||
this.statusClass = "tw-text-danger";
|
||||
} else if (toDelete) {
|
||||
// They want to delete and we don't have a valid until date so we can
|
||||
// this should only happen on a self-hosted install
|
||||
this.statusMessage = this.i18nService.t("requestRemoved");
|
||||
this.statusClass = "text-danger";
|
||||
this.statusClass = "tw-text-danger";
|
||||
} else if (validUntil) {
|
||||
// They don't want to delete and they have a valid until date
|
||||
// that means they are actively sponsoring someone
|
||||
this.statusMessage = this.i18nService.t("active");
|
||||
this.statusClass = "text-success";
|
||||
this.statusClass = "tw-text-success";
|
||||
} else if (selfHosted && lastSyncDate) {
|
||||
// We are on a self-hosted install and it has been synced but we have not gotten
|
||||
// a valid until date so we can't know if they are actively sponsoring someone
|
||||
this.statusMessage = this.i18nService.t("sent");
|
||||
this.statusClass = "text-success";
|
||||
this.statusClass = "tw-text-success";
|
||||
} else if (!selfHosted) {
|
||||
// We are in cloud and all other status checks have been false therefore we have
|
||||
// sent the request but it hasn't been accepted yet
|
||||
this.statusMessage = this.i18nService.t("sent");
|
||||
this.statusClass = "text-success";
|
||||
this.statusClass = "tw-text-success";
|
||||
} else {
|
||||
// We are on a self-hosted install and we have not synced yet
|
||||
this.statusMessage = this.i18nService.t("requested");
|
||||
this.statusClass = "text-success";
|
||||
this.statusClass = "tw-text-success";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<auth-anon-layout [title]="pageTitle" [subtitle]="pageSubtitle" [icon]="pageIcon">
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet slot="secondary" name="secondary"></router-outlet>
|
||||
</auth-anon-layout>
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
|
||||
import { AnonLayoutComponent } from "@bitwarden/auth/angular";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Icon } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "anon-layout-wrapper.component.html",
|
||||
imports: [AnonLayoutComponent, RouterModule],
|
||||
})
|
||||
export class AnonLayoutWrapperComponent {
|
||||
protected pageTitle: string;
|
||||
protected pageSubtitle: string;
|
||||
protected pageIcon: Icon;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
this.pageTitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageTitle"]);
|
||||
this.pageSubtitle = this.i18nService.t(this.route.snapshot.firstChild.data["pageSubtitle"]);
|
||||
this.pageIcon = this.route.snapshot.firstChild.data["pageIcon"]; // don't translate
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
<button type="button" bitButton buttonType="danger" (click)="deauthorizeSessions()">
|
||||
{{ "deauthorizeSessions" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="danger" (click)="purgeVault()">
|
||||
<button type="button" bitButton buttonType="danger" [bitAction]="purgeVault">
|
||||
{{ "purgeVault" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="danger" (click)="deleteAccount()">
|
||||
@@ -21,7 +21,6 @@
|
||||
</app-danger-zone>
|
||||
|
||||
<ng-template #deauthorizeSessionsTemplate></ng-template>
|
||||
<ng-template #purgeVaultTemplate></ng-template>
|
||||
<ng-template #deleteAccountTemplate></ng-template>
|
||||
<ng-template #viewUserApiKeyTemplate></ng-template>
|
||||
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.component";
|
||||
|
||||
@@ -15,8 +17,6 @@ import { DeleteAccountComponent } from "./delete-account.component";
|
||||
export class AccountComponent {
|
||||
@ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true })
|
||||
deauthModalRef: ViewContainerRef;
|
||||
@ViewChild("purgeVaultTemplate", { read: ViewContainerRef, static: true })
|
||||
purgeModalRef: ViewContainerRef;
|
||||
@ViewChild("deleteAccountTemplate", { read: ViewContainerRef, static: true })
|
||||
deleteModalRef: ViewContainerRef;
|
||||
|
||||
@@ -24,6 +24,7 @@ export class AccountComponent {
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private dialogService: DialogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
) {}
|
||||
|
||||
@@ -35,9 +36,10 @@ export class AccountComponent {
|
||||
await this.modalService.openViewRef(DeauthorizeSessionsComponent, this.deauthModalRef);
|
||||
}
|
||||
|
||||
async purgeVault() {
|
||||
await this.modalService.openViewRef(PurgeVaultComponent, this.purgeModalRef);
|
||||
}
|
||||
purgeVault = async () => {
|
||||
const dialogRef = PurgeVaultComponent.open(this.dialogService);
|
||||
await lastValueFrom(dialogRef.closed);
|
||||
};
|
||||
|
||||
async deleteAccount() {
|
||||
await this.modalService.openViewRef(DeleteAccountComponent, this.deleteModalRef);
|
||||
|
||||
@@ -1,46 +1,35 @@
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<form
|
||||
*ngIf="profile && !loading"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="name">{{ "name" | i18n }}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="profile.name" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">{{ "email" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Email"
|
||||
[(ngModel)]="profile.email"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<form *ngIf="profile && !loading" [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-6">
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="email" readonly />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<div class="tw-col-span-6">
|
||||
<div class="tw-mb-3">
|
||||
<dynamic-avatar text="{{ profile | userName }}" [id]="profile.id" [size]="'large'">
|
||||
</dynamic-avatar>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary tw-ml-3.5"
|
||||
buttonType="secondary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
appStopClick
|
||||
appStopProp
|
||||
(click)="openChangeAvatar()"
|
||||
[bitAction]="openChangeAvatar"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-pencil-square" aria-hidden="true"></i>
|
||||
Customize
|
||||
@@ -53,9 +42,6 @@
|
||||
</app-account-fingerprint>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary">{{ "save" | i18n }}</button>
|
||||
</form>
|
||||
<ng-template #avatarModalTemplate></ng-template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ViewChild, ViewContainerRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
@@ -6,7 +7,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
@@ -21,16 +21,19 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
profile: ProfileResponse;
|
||||
fingerprintMaterial: string;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
@ViewChild("avatarModalTemplate", { read: ViewContainerRef, static: true })
|
||||
avatarModalRef: ViewContainerRef;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected formGroup = new FormGroup({
|
||||
name: new FormControl(null),
|
||||
email: new FormControl(null),
|
||||
});
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
private modalService: ModalService,
|
||||
) {}
|
||||
@@ -39,6 +42,15 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
this.profile = await this.apiService.getProfile();
|
||||
this.loading = false;
|
||||
this.fingerprintMaterial = await this.stateService.getUserId();
|
||||
this.formGroup.get("name").setValue(this.profile.name);
|
||||
this.formGroup.get("email").setValue(this.profile.email);
|
||||
|
||||
this.formGroup
|
||||
.get("name")
|
||||
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((name) => {
|
||||
this.profile.name = name;
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
@@ -46,7 +58,7 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async openChangeAvatar() {
|
||||
openChangeAvatar = async () => {
|
||||
const modalOpened = await this.modalService.openViewRef(
|
||||
ChangeAvatarComponent,
|
||||
this.avatarModalRef,
|
||||
@@ -57,16 +69,14 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new UpdateProfileRequest(this.profile.name, this.profile.masterPasswordHint);
|
||||
this.formPromise = this.apiService.putProfile(request);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("accountUpdated"));
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
submit = async () => {
|
||||
const request = new UpdateProfileRequest(
|
||||
this.formGroup.get("name").value,
|
||||
this.profile.masterPasswordHint,
|
||||
);
|
||||
await this.apiService.putProfile(request);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("accountUpdated"));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<div class="tw-rounded tw-border tw-border-solid tw-border-warning-600 tw-bg-background">
|
||||
<div class="tw-bg-warning-600 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast">
|
||||
<i class="bwi bwi-envelope bwi-fw" aria-hidden="true"></i> {{ "verifyEmail" | i18n }}
|
||||
</div>
|
||||
<div class="tw-p-5">
|
||||
<p>{{ "verifyEmailDesc" | i18n }}</p>
|
||||
<button id="sendBtn" bitButton type="button" block [bitAction]="send">
|
||||
{{ "sendEmail" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<bit-banner bannerType="warning" (onClose)="onDismiss.emit()">
|
||||
{{ "verifyEmailDesc" | i18n }}
|
||||
<button
|
||||
id="sendBtn"
|
||||
bitLink
|
||||
linkType="contrast"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="unstyled"
|
||||
[bitAction]="send"
|
||||
>
|
||||
{{ "sendEmail" | i18n }}
|
||||
</button>
|
||||
</bit-banner>
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { AsyncActionsModule, BannerModule, ButtonModule, LinkModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-verify-email",
|
||||
templateUrl: "verify-email.component.html",
|
||||
imports: [AsyncActionsModule, BannerModule, ButtonModule, CommonModule, JslibModule, LinkModule],
|
||||
})
|
||||
export class VerifyEmailComponent {
|
||||
actionPromise: Promise<unknown>;
|
||||
|
||||
@Output() onVerified = new EventEmitter<boolean>();
|
||||
@Output() onDismiss = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private tokenService: TokenService,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "chooseYourPlan" | i18n }}</h2>
|
||||
<div *ngFor="let selectableProduct of selectableProducts">
|
||||
<bit-radio-group formControlName="product" [block]="true">
|
||||
<bit-radio-group formControlName="product" [block]="true">
|
||||
<div *ngFor="let selectableProduct of selectableProducts" class="tw-mb-3">
|
||||
<bit-radio-button [value]="selectableProduct.product" (change)="changedProduct()">
|
||||
<bit-label>{{ selectableProduct.nameLocalizationKey | i18n }}</bit-label>
|
||||
<bit-hint class="tw-text-sm"
|
||||
@@ -147,7 +147,7 @@
|
||||
</ng-template>
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
<span *ngIf="selectableProduct.product != productTypes.Free">
|
||||
<span *ngIf="selectableProduct.product != productTypes.Free" class="tw-pl-4">
|
||||
<ng-container
|
||||
*ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship"
|
||||
>
|
||||
@@ -176,6 +176,7 @@
|
||||
!selectableProduct.PasswordManager.basePrice &&
|
||||
selectableProduct.PasswordManager.hasAdditionalSeatsOption
|
||||
"
|
||||
class="tw-pl-4"
|
||||
>
|
||||
{{
|
||||
"costPerUser"
|
||||
@@ -188,11 +189,11 @@
|
||||
}}
|
||||
/{{ "month" | i18n }}
|
||||
</span>
|
||||
<span *ngIf="selectableProduct.product == productTypes.Free">{{
|
||||
<span *ngIf="selectableProduct.product == productTypes.Free" class="tw-pl-4">{{
|
||||
"freeForever" | i18n
|
||||
}}</span>
|
||||
</bit-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</bit-radio-group>
|
||||
</bit-section>
|
||||
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
|
||||
<bit-section
|
||||
@@ -277,126 +278,128 @@
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</bit-section>
|
||||
<bit-section *ngFor="let selectablePlan of selectablePlans">
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||
<bit-radio-group formControlName="plan">
|
||||
<bit-radio-button
|
||||
type="radio"
|
||||
id="interval{{ selectablePlan.type }}"
|
||||
[value]="selectablePlan.type"
|
||||
>
|
||||
<bit-label>{{ (selectablePlan.isAnnual ? "annually" : "monthly") | i18n }}</bit-label>
|
||||
<bit-hint *ngIf="selectablePlan.isAnnual">
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||
>
|
||||
{{ "basePrice" | i18n }}:
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.basePrice / 12
|
||||
: selectablePlan.PasswordManager.basePrice
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
||||
<span class="tw-line-through">{{
|
||||
selectablePlan.PasswordManager.basePrice | currency: "$"
|
||||
}}</span>
|
||||
{{ "freeWithSponsorship" | i18n }}
|
||||
</ng-container>
|
||||
<ng-template #notAcceptingSponsorship>
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
<div *ngFor="let selectablePlan of selectablePlans">
|
||||
<bit-radio-button
|
||||
type="radio"
|
||||
id="interval{{ selectablePlan.type }}"
|
||||
[value]="selectablePlan.type"
|
||||
>
|
||||
<bit-label>{{ (selectablePlan.isAnnual ? "annually" : "monthly") | i18n }}</bit-label>
|
||||
<bit-hint *ngIf="selectablePlan.isAnnual">
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||
>
|
||||
{{ "basePrice" | i18n }}:
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.basePrice / 12
|
||||
: selectablePlan.PasswordManager.basePrice
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
||||
<span class="tw-line-through">{{
|
||||
selectablePlan.PasswordManager.basePrice | currency: "$"
|
||||
}}</span>
|
||||
{{ "freeWithSponsorship" | i18n }}
|
||||
</ng-container>
|
||||
<ng-template #notAcceptingSponsorship>
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
/{{ "year" | i18n }}
|
||||
</ng-template>
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||
>
|
||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||
>{{ "additionalUsers" | i18n }}:</span
|
||||
>
|
||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.seatPrice / 12
|
||||
: selectablePlan.PasswordManager.seatPrice
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12 {{ "monthAbbr" | i18n }} =
|
||||
{{
|
||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||
| currency: "$"
|
||||
}}
|
||||
/{{ "year" | i18n }}
|
||||
</ng-template>
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||
>
|
||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||
>{{ "additionalUsers" | i18n }}:</span
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||
>
|
||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.seatPrice / 12
|
||||
: selectablePlan.PasswordManager.seatPrice
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12 {{ "monthAbbr" | i18n }} =
|
||||
{{
|
||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||
| currency: "$"
|
||||
}}
|
||||
/{{ "year" | i18n }}
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||
>
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
|
||||
: selectablePlan.PasswordManager.additionalStoragePricePerGb
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12 {{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
|
||||
</p>
|
||||
</bit-hint>
|
||||
<bit-hint *ngIf="!selectablePlan.isAnnual">
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||
>
|
||||
{{ "basePrice" | i18n }}:
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
/{{ "month" | i18n }}
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||
>
|
||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||
>{{ "additionalUsers" | i18n }}:</span
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{
|
||||
(selectablePlan.isAnnual
|
||||
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
|
||||
: selectablePlan.PasswordManager.additionalStoragePricePerGb
|
||||
) | currency: "$"
|
||||
}}
|
||||
× 12 {{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
|
||||
</p>
|
||||
</bit-hint>
|
||||
<bit-hint *ngIf="!selectablePlan.isAnnual">
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||
>
|
||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }} =
|
||||
{{
|
||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||
| currency: "$"
|
||||
}}
|
||||
/{{ "month" | i18n }}
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||
>
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
|
||||
</p>
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
{{ "basePrice" | i18n }}:
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
/{{ "month" | i18n }}
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||
>
|
||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||
>{{ "additionalUsers" | i18n }}:</span
|
||||
>
|
||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }} =
|
||||
{{
|
||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||
| currency: "$"
|
||||
}}
|
||||
/{{ "month" | i18n }}
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||
>
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
|
||||
</p>
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
</div>
|
||||
</bit-radio-group>
|
||||
</bit-section>
|
||||
</bit-section>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="default" [title]="'addCredit' | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<p bitTypography="body1">{{ "creditDelayed" | i18n }}</p>
|
||||
<div class="tw-grid tw-grid-cols-2">
|
||||
<bit-radio-group formControlName="method">
|
||||
<bit-radio-button id="credit-method-paypal" [value]="paymentMethodType.PayPal">
|
||||
<bit-label> <i class="bwi bwi-paypal"></i>PayPal</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="credit-method-bitcoin" [value]="paymentMethodType.BitPay">
|
||||
<bit-label> <i class="bwi bwi-bitcoin"></i>Bitcoin</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-2">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "amount" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="creditAmount"
|
||||
(blur)="formatAmount()"
|
||||
required
|
||||
/>
|
||||
<span bitPrefix>$USD</span>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="DialogResult.Cancelled"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
<form #ppButtonForm action="{{ ppButtonFormAction }}" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_xclick" />
|
||||
<input type="hidden" name="business" value="{{ ppButtonBusinessId }}" />
|
||||
<input type="hidden" name="button_subtype" value="services" />
|
||||
<input type="hidden" name="no_note" value="1" />
|
||||
<input type="hidden" name="no_shipping" value="1" />
|
||||
<input type="hidden" name="rm" value="1" />
|
||||
<input type="hidden" name="return" value="{{ returnUrl }}" />
|
||||
<input type="hidden" name="cancel_return" value="{{ returnUrl }}" />
|
||||
<input type="hidden" name="currency_code" value="USD" />
|
||||
<input type="hidden" name="image_url" value="https://bitwarden.com/images/paypal-banner.png" />
|
||||
<input type="hidden" name="bn" value="PP-BuyNowBF:btn_buynow_LG.gif:NonHosted" />
|
||||
<input type="hidden" name="amount" value="{{ formGroup.get('creditAmount').value }}" />
|
||||
<input type="hidden" name="custom" value="{{ ppButtonCustomField }}" />
|
||||
<input type="hidden" name="item_name" value="Bitwarden Account Credit" />
|
||||
<input type="hidden" name="item_number" value="{{ subject }}" />
|
||||
</form>
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -17,6 +11,16 @@ import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/b
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export interface AddCreditDialogData {
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export enum AddCreditDialogResult {
|
||||
Added = "added",
|
||||
Cancelled = "cancelled",
|
||||
}
|
||||
|
||||
export type PayPalConfig = {
|
||||
businessId?: string;
|
||||
@@ -24,17 +28,9 @@ export type PayPalConfig = {
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-add-credit",
|
||||
templateUrl: "add-credit.component.html",
|
||||
templateUrl: "add-credit-dialog.component.html",
|
||||
})
|
||||
export class AddCreditComponent implements OnInit {
|
||||
@Input() creditAmount: string;
|
||||
@Input() showOptions = true;
|
||||
@Input() method = PaymentMethodType.PayPal;
|
||||
@Input() organizationId: string;
|
||||
@Output() onAdded = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
export class AddCreditDialogComponent implements OnInit {
|
||||
@ViewChild("ppButtonForm", { read: ElementRef, static: true }) ppButtonFormRef: ElementRef;
|
||||
|
||||
paymentMethodType = PaymentMethodType;
|
||||
@@ -44,14 +40,22 @@ export class AddCreditComponent implements OnInit {
|
||||
ppLoading = false;
|
||||
subject: string;
|
||||
returnUrl: string;
|
||||
formPromise: Promise<any>;
|
||||
organizationId: string;
|
||||
|
||||
private userId: string;
|
||||
private name: string;
|
||||
private email: string;
|
||||
private region: string;
|
||||
|
||||
protected DialogResult = AddCreditDialogResult;
|
||||
protected formGroup = new FormGroup({
|
||||
method: new FormControl(PaymentMethodType.PayPal),
|
||||
creditAmount: new FormControl(null, [Validators.required]),
|
||||
});
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) protected data: AddCreditDialogData,
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@@ -59,6 +63,7 @@ export class AddCreditComponent implements OnInit {
|
||||
private logService: LogService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.organizationId = data.organizationId;
|
||||
const payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig;
|
||||
this.ppButtonFormAction = payPalConfig.buttonAction;
|
||||
this.ppButtonBusinessId = payPalConfig.businessId;
|
||||
@@ -93,7 +98,18 @@ export class AddCreditComponent implements OnInit {
|
||||
this.returnUrl = window.location.href;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
get creditAmount() {
|
||||
return this.formGroup.value.creditAmount;
|
||||
}
|
||||
set creditAmount(value: string) {
|
||||
this.formGroup.get("creditAmount").setValue(value);
|
||||
}
|
||||
|
||||
get method() {
|
||||
return this.formGroup.value.method;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (this.creditAmount == null || this.creditAmount === "") {
|
||||
return;
|
||||
}
|
||||
@@ -104,33 +120,20 @@ export class AddCreditComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
if (this.method === PaymentMethodType.BitPay) {
|
||||
try {
|
||||
const req = new BitPayInvoiceRequest();
|
||||
req.email = this.email;
|
||||
req.name = this.name;
|
||||
req.credit = true;
|
||||
req.amount = this.creditAmountNumber;
|
||||
req.organizationId = this.organizationId;
|
||||
req.userId = this.userId;
|
||||
req.returnUrl = this.returnUrl;
|
||||
this.formPromise = this.apiService.postBitPayInvoice(req);
|
||||
const bitPayUrl: string = await this.formPromise;
|
||||
this.platformUtilsService.launchUri(bitPayUrl);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
const req = new BitPayInvoiceRequest();
|
||||
req.email = this.email;
|
||||
req.name = this.name;
|
||||
req.credit = true;
|
||||
req.amount = this.creditAmountNumber;
|
||||
req.organizationId = this.organizationId;
|
||||
req.userId = this.userId;
|
||||
req.returnUrl = this.returnUrl;
|
||||
const bitPayUrl: string = await this.apiService.postBitPayInvoice(req);
|
||||
this.platformUtilsService.launchUri(bitPayUrl);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.onAdded.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
this.dialogRef.close(AddCreditDialogResult.Added);
|
||||
};
|
||||
|
||||
formatAmount() {
|
||||
try {
|
||||
@@ -160,3 +163,15 @@ export class AddCreditComponent implements OnInit {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a AddCreditDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export function openAddCreditDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AddCreditDialogData>,
|
||||
) {
|
||||
return dialogService.open<AddCreditDialogResult>(AddCreditDialogComponent, config);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h3 class="card-body-header">{{ "addCredit" | i18n }}</h3>
|
||||
<div class="mb-4 text-lg" *ngIf="showOptions">
|
||||
<div class="form-check form-check-inline">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="Method"
|
||||
id="credit-method-paypal"
|
||||
[value]="paymentMethodType.PayPal"
|
||||
[(ngModel)]="method"
|
||||
/>
|
||||
<label class="form-check-label" for="credit-method-paypal">
|
||||
<i class="bwi bwi-fw bwi-paypal" aria-hidden="true"></i> PayPal</label
|
||||
>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="Method"
|
||||
id="credit-method-bitcoin"
|
||||
[value]="paymentMethodType.BitPay"
|
||||
[(ngModel)]="method"
|
||||
/>
|
||||
<label class="form-check-label" for="credit-method-bitcoin">
|
||||
<i class="bwi bwi-fw bwi-bitcoin" aria-hidden="true"></i> Bitcoin</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<label for="creditAmount">{{ "amount" | i18n }}</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend"><span class="input-group-text">$USD</span></div>
|
||||
<input
|
||||
id="creditAmount"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="CreditAmount"
|
||||
[(ngModel)]="creditAmount"
|
||||
(blur)="formatAmount()"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{ "creditDelayed" | i18n }}</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading || ppLoading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "submit" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<form #ppButtonForm action="{{ ppButtonFormAction }}" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_xclick" />
|
||||
<input type="hidden" name="business" value="{{ ppButtonBusinessId }}" />
|
||||
<input type="hidden" name="button_subtype" value="services" />
|
||||
<input type="hidden" name="no_note" value="1" />
|
||||
<input type="hidden" name="no_shipping" value="1" />
|
||||
<input type="hidden" name="rm" value="1" />
|
||||
<input type="hidden" name="return" value="{{ returnUrl }}" />
|
||||
<input type="hidden" name="cancel_return" value="{{ returnUrl }}" />
|
||||
<input type="hidden" name="currency_code" value="USD" />
|
||||
<input type="hidden" name="image_url" value="https://bitwarden.com/images/paypal-banner.png" />
|
||||
<input type="hidden" name="bn" value="PP-BuyNowBF:btn_buynow_LG.gif:NonHosted" />
|
||||
<input type="hidden" name="amount" value="{{ creditAmount }}" />
|
||||
<input type="hidden" name="custom" value="{{ ppButtonCustomField }}" />
|
||||
<input type="hidden" name="item_name" value="Bitwarden Account Credit" />
|
||||
<input type="hidden" name="item_number" value="{{ subject }}" />
|
||||
</form>
|
||||
@@ -3,7 +3,7 @@ import { NgModule } from "@angular/core";
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { AddCreditComponent } from "./add-credit.component";
|
||||
import { AddCreditDialogComponent } from "./add-credit-dialog.component";
|
||||
import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog.component";
|
||||
import { AdjustStorageComponent } from "./adjust-storage.component";
|
||||
import { BillingHistoryComponent } from "./billing-history.component";
|
||||
@@ -17,7 +17,7 @@ import { UpdateLicenseComponent } from "./update-license.component";
|
||||
@NgModule({
|
||||
imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule],
|
||||
declarations: [
|
||||
AddCreditComponent,
|
||||
AddCreditDialogComponent,
|
||||
AdjustPaymentDialogComponent,
|
||||
AdjustStorageComponent,
|
||||
BillingHistoryComponent,
|
||||
|
||||
@@ -33,22 +33,9 @@
|
||||
<strong>{{ creditOrBalance | currency: "$" }}</strong>
|
||||
</p>
|
||||
<p>{{ "creditAppliedDesc" | i18n }}</p>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
(click)="addCredit()"
|
||||
*ngIf="!showAddCredit"
|
||||
>
|
||||
<button type="button" bitButton buttonType="secondary" [bitAction]="addCredit">
|
||||
{{ "addCredit" | i18n }}
|
||||
</button>
|
||||
<app-add-credit
|
||||
[organizationId]="organizationId"
|
||||
(onAdded)="closeAddCredit(true)"
|
||||
(onCanceled)="closeAddCredit(false)"
|
||||
*ngIf="showAddCredit"
|
||||
>
|
||||
</app-add-credit>
|
||||
<h2 class="spaced-header">{{ "paymentMethod" | i18n }}</h2>
|
||||
<p *ngIf="!paymentSource">{{ "noPaymentMethod" | i18n }}</p>
|
||||
<ng-container *ngIf="paymentSource">
|
||||
|
||||
@@ -15,6 +15,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component";
|
||||
import {
|
||||
AdjustPaymentDialogResult,
|
||||
openAdjustPaymentDialog,
|
||||
@@ -30,7 +31,6 @@ export class PaymentMethodComponent implements OnInit {
|
||||
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
showAddCredit = false;
|
||||
billing: BillingPaymentResponse;
|
||||
org: OrganizationSubscriptionResponse;
|
||||
sub: SubscriptionResponse;
|
||||
@@ -111,18 +111,17 @@ export class PaymentMethodComponent implements OnInit {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
addCredit() {
|
||||
this.showAddCredit = true;
|
||||
}
|
||||
|
||||
closeAddCredit(load: boolean) {
|
||||
this.showAddCredit = false;
|
||||
if (load) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.load();
|
||||
addCredit = async () => {
|
||||
const dialogRef = openAddCreditDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organizationId,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AddCreditDialogResult.Added) {
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
changePayment = async () => {
|
||||
const dialogRef = openAdjustPaymentDialog(this.dialogService, {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<div class="tw-rounded tw-border tw-border-solid tw-border-warning-600 tw-bg-background">
|
||||
<div class="tw-bg-warning-600 tw-px-5 tw-py-2.5 tw-font-bold tw-uppercase tw-text-contrast">
|
||||
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
|
||||
{{ "lowKdfIterations" | i18n }}
|
||||
</div>
|
||||
<div class="tw-p-5">
|
||||
<p>{{ "updateLowKdfIterationsDesc" | i18n }}</p>
|
||||
<a
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
[block]="true"
|
||||
routerLink="/settings/security/security-keys"
|
||||
>
|
||||
{{ "updateKdfSettings" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-low-kdf",
|
||||
templateUrl: "low-kdf.component.html",
|
||||
})
|
||||
export class LowKdfComponent {}
|
||||
@@ -53,7 +53,6 @@ import { TwoFactorSetupComponent } from "../auth/settings/two-factor-setup.compo
|
||||
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor-verify.component";
|
||||
import { TwoFactorWebAuthnComponent } from "../auth/settings/two-factor-webauthn.component";
|
||||
import { TwoFactorYubiKeyComponent } from "../auth/settings/two-factor-yubikey.component";
|
||||
import { VerifyEmailComponent } from "../auth/settings/verify-email.component";
|
||||
import { UserVerificationModule } from "../auth/shared/components/user-verification";
|
||||
import { SsoComponent } from "../auth/sso.component";
|
||||
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component";
|
||||
@@ -70,7 +69,6 @@ import { HeaderModule } from "../layouts/header/header.module";
|
||||
import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module";
|
||||
import { UserLayoutComponent } from "../layouts/user-layout.component";
|
||||
import { DomainRulesComponent } from "../settings/domain-rules.component";
|
||||
import { LowKdfComponent } from "../settings/low-kdf.component";
|
||||
import { PreferencesComponent } from "../settings/preferences.component";
|
||||
import { VaultTimeoutInputComponent } from "../settings/vault-timeout-input.component";
|
||||
import { GeneratorComponent } from "../tools/generator.component";
|
||||
@@ -186,11 +184,9 @@ import { SharedModule } from "./shared.module";
|
||||
UpdatePasswordComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
VaultTimeoutInputComponent,
|
||||
VerifyEmailComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
VerifyRecoverDeleteProviderComponent,
|
||||
LowKdfComponent,
|
||||
],
|
||||
exports: [
|
||||
UserVerificationModule,
|
||||
@@ -264,11 +260,9 @@ import { SharedModule } from "./shared.module";
|
||||
UpdateTempPasswordComponent,
|
||||
UserLayoutComponent,
|
||||
VaultTimeoutInputComponent,
|
||||
VerifyEmailComponent,
|
||||
VerifyEmailTokenComponent,
|
||||
VerifyRecoverDeleteComponent,
|
||||
VerifyRecoverDeleteProviderComponent,
|
||||
LowKdfComponent,
|
||||
HeaderModule,
|
||||
DangerZoneComponent,
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
||||
@@ -6,7 +6,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
@@ -21,23 +20,23 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
||||
constructor(
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||
stateService: StateService,
|
||||
accountService: AccountService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
logService: LogService,
|
||||
route: ActivatedRoute,
|
||||
ngZone: NgZone,
|
||||
private dialogService: DialogService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
passwordGenerationService,
|
||||
usernameGenerationService,
|
||||
platformUtilsService,
|
||||
stateService,
|
||||
accountService,
|
||||
i18nService,
|
||||
logService,
|
||||
route,
|
||||
accountService,
|
||||
ngZone,
|
||||
window,
|
||||
);
|
||||
if (platformUtilsService.isSelfHost()) {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { UntypedFormBuilder } from "@angular/forms";
|
||||
|
||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -26,7 +24,6 @@ export class ExportComponent extends BaseExportComponent {
|
||||
eventCollectionService: EventCollectionService,
|
||||
policyService: PolicyService,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
formBuilder: UntypedFormBuilder,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
@@ -39,84 +36,10 @@ export class ExportComponent extends BaseExportComponent {
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
logService,
|
||||
userVerificationService,
|
||||
formBuilder,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
organizationService,
|
||||
);
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (this.isFileEncryptedExport && this.filePassword != this.confirmFilePassword) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("filePasswordAndConfirmFilePasswordDoNotMatch"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.exportForm.markAllAsTouched();
|
||||
if (this.exportForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.disabledByPolicy) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("personalVaultExportPolicyInEffect"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const userVerified = await this.verifyUser();
|
||||
if (!userVerified) {
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.doExport();
|
||||
};
|
||||
|
||||
protected saved() {
|
||||
super.saved();
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("exportSuccess"));
|
||||
}
|
||||
|
||||
private async verifyUser(): Promise<boolean> {
|
||||
let confirmDescription = "exportWarningDesc";
|
||||
if (this.isFileEncryptedExport) {
|
||||
confirmDescription = "fileEncryptedExportWarningDesc";
|
||||
} else if (this.isAccountEncryptedExport) {
|
||||
confirmDescription = "encExportKeyWarningDesc";
|
||||
}
|
||||
|
||||
const result = await UserVerificationDialogComponent.open(this.dialogService, {
|
||||
title: "confirmVaultExport",
|
||||
bodyText: confirmDescription,
|
||||
confirmButtonOptions: {
|
||||
text: "exportVault",
|
||||
type: "primary",
|
||||
},
|
||||
});
|
||||
|
||||
// Handle the result of the dialog based on user action and verification success
|
||||
if (result.userAction === "cancel") {
|
||||
// User cancelled the dialog
|
||||
return false;
|
||||
}
|
||||
|
||||
// User confirmed the dialog so check verification success
|
||||
if (!result.verificationSuccess) {
|
||||
if (result.noAvailableClientVerificationMethods) {
|
||||
// No client-side verification methods are available
|
||||
// Could send user to configure a verification method like PIN or biometrics
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,14 +85,26 @@ export class CollectionAdminView extends CollectionView {
|
||||
* Whether the user can modify user access to this collection
|
||||
*/
|
||||
canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageUsers;
|
||||
const allowAdminAccessToAllCollectionItems =
|
||||
!flexibleCollectionsV1Enabled || org.allowAdminAccessToAllCollectionItems;
|
||||
|
||||
return (
|
||||
(org.permissions.manageUsers && allowAdminAccessToAllCollectionItems) ||
|
||||
this.canEdit(org, flexibleCollectionsV1Enabled)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user can modify group access to this collection
|
||||
*/
|
||||
canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups;
|
||||
const allowAdminAccessToAllCollectionItems =
|
||||
!flexibleCollectionsV1Enabled || org.allowAdminAccessToAllCollectionItems;
|
||||
|
||||
return (
|
||||
(org.permissions.manageGroups && allowAdminAccessToAllCollectionItems) ||
|
||||
this.canEdit(org, flexibleCollectionsV1Enabled)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { KdfType } from "@bitwarden/common/platform/enums";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
import {
|
||||
PREMIUM_BANNER_REPROMPT_KEY,
|
||||
VaultBannersService,
|
||||
VisibleVaultBanner,
|
||||
} from "./vault-banners.service";
|
||||
|
||||
describe("VaultBannersService", () => {
|
||||
let service: VaultBannersService;
|
||||
const isSelfHost = jest.fn().mockReturnValue(false);
|
||||
const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(false);
|
||||
const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId));
|
||||
const getEmailVerified = jest.fn().mockResolvedValue(true);
|
||||
const hasMasterPassword = jest.fn().mockResolvedValue(true);
|
||||
const getKdfConfig = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600000 });
|
||||
const getLastSync = jest.fn().mockResolvedValue(null);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
getLastSync.mockClear().mockResolvedValue(new Date("2024-05-14"));
|
||||
isSelfHost.mockClear();
|
||||
getEmailVerified.mockClear().mockResolvedValue(true);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
VaultBannersService,
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: { isSelfHost },
|
||||
},
|
||||
{
|
||||
provide: BillingAccountProfileStateService,
|
||||
useValue: { hasPremiumFromAnySource$: hasPremiumFromAnySource$ },
|
||||
},
|
||||
{
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: { isSelfHost },
|
||||
},
|
||||
{
|
||||
provide: TokenService,
|
||||
useValue: { getEmailVerified },
|
||||
},
|
||||
{
|
||||
provide: UserVerificationService,
|
||||
useValue: { hasMasterPassword },
|
||||
},
|
||||
{
|
||||
provide: KdfConfigService,
|
||||
useValue: { getKdfConfig },
|
||||
},
|
||||
{
|
||||
provide: SyncService,
|
||||
useValue: { getLastSync },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("Premium", () => {
|
||||
it("waits until sync is completed before showing premium banner", async () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
isSelfHost.mockReturnValue(false);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
jest.advanceTimersByTime(201);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show a premium banner for self-hosted users", async () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
isSelfHost.mockReturnValue(true);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
jest.advanceTimersByTime(201);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show a premium banner when they have access to premium", async () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(true);
|
||||
isSelfHost.mockReturnValue(false);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
jest.advanceTimersByTime(201);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(false);
|
||||
});
|
||||
|
||||
describe("dismissing", () => {
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
const date = new Date("2023-06-08");
|
||||
date.setHours(0, 0, 0, 0);
|
||||
jest.setSystemTime(date.getTime());
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
await service.dismissBanner(VisibleVaultBanner.Premium);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("updates state on first dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneWeekLater = new Date("2023-06-15");
|
||||
oneWeekLater.setHours(0, 0, 0, 0);
|
||||
|
||||
expect(state).toEqual({
|
||||
numberOfDismissals: 1,
|
||||
nextPromptDate: oneWeekLater.getTime(),
|
||||
});
|
||||
});
|
||||
|
||||
it("updates state on second dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneMonthLater = new Date("2023-07-08");
|
||||
oneMonthLater.setHours(0, 0, 0, 0);
|
||||
|
||||
expect(state).toEqual({
|
||||
numberOfDismissals: 2,
|
||||
nextPromptDate: oneMonthLater.getTime(),
|
||||
});
|
||||
});
|
||||
|
||||
it("updates state on third dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneYearLater = new Date("2024-06-08");
|
||||
oneYearLater.setHours(0, 0, 0, 0);
|
||||
|
||||
expect(state).toEqual({
|
||||
numberOfDismissals: 3,
|
||||
nextPromptDate: oneYearLater.getTime(),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("KDFSettings", () => {
|
||||
beforeEach(async () => {
|
||||
hasMasterPassword.mockResolvedValue(true);
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 });
|
||||
});
|
||||
|
||||
it("shows low KDF iteration banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show low KDF iteration banner if KDF type is not PBKDF2_SHA256", async () => {
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.Argon2id, iterations: 600001 });
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show low KDF for iterations about 600,000", async () => {
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 });
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||
});
|
||||
|
||||
it("dismisses low KDF iteration banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(true);
|
||||
|
||||
await service.dismissBanner(VisibleVaultBanner.KDFSettings);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("OutdatedBrowser", () => {
|
||||
beforeEach(async () => {
|
||||
// Hardcode `MSIE` in userAgent string
|
||||
const userAgent = "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 MSIE";
|
||||
Object.defineProperty(navigator, "userAgent", {
|
||||
configurable: true,
|
||||
get: () => userAgent,
|
||||
});
|
||||
});
|
||||
|
||||
it("shows outdated browser banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowUpdateBrowserBanner()).toBe(true);
|
||||
});
|
||||
|
||||
it("dismisses outdated browser banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowUpdateBrowserBanner()).toBe(true);
|
||||
|
||||
await service.dismissBanner(VisibleVaultBanner.OutdatedBrowser);
|
||||
|
||||
expect(await service.shouldShowUpdateBrowserBanner()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("VerifyEmail", () => {
|
||||
beforeEach(async () => {
|
||||
getEmailVerified.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it("shows verify email banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowVerifyEmailBanner()).toBe(true);
|
||||
});
|
||||
|
||||
it("dismisses verify email banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowVerifyEmailBanner()).toBe(true);
|
||||
|
||||
await service.dismissBanner(VisibleVaultBanner.VerifyEmail);
|
||||
|
||||
expect(await service.shouldShowVerifyEmailBanner()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Subject, Observable, combineLatest, firstValueFrom, map } from "rxjs";
|
||||
import { mergeMap, take } from "rxjs/operators";
|
||||
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
|
||||
import {
|
||||
StateProvider,
|
||||
ActiveUserState,
|
||||
KeyDefinition,
|
||||
PREMIUM_BANNER_DISK_LOCAL,
|
||||
BANNERS_DISMISSED_DISK,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
export enum VisibleVaultBanner {
|
||||
KDFSettings = "kdf-settings",
|
||||
OutdatedBrowser = "outdated-browser",
|
||||
Premium = "premium",
|
||||
VerifyEmail = "verify-email",
|
||||
}
|
||||
|
||||
type PremiumBannerReprompt = {
|
||||
numberOfDismissals: number;
|
||||
/** Timestamp representing when to show the prompt next */
|
||||
nextPromptDate: number;
|
||||
};
|
||||
|
||||
/** Banners that will be re-shown on a new session */
|
||||
type SessionBanners = Omit<VisibleVaultBanner, VisibleVaultBanner.Premium>;
|
||||
|
||||
export const PREMIUM_BANNER_REPROMPT_KEY = new KeyDefinition<PremiumBannerReprompt>(
|
||||
PREMIUM_BANNER_DISK_LOCAL,
|
||||
"bannerReprompt",
|
||||
{
|
||||
deserializer: (bannerReprompt) => bannerReprompt,
|
||||
},
|
||||
);
|
||||
|
||||
export const BANNERS_DISMISSED_DISK_KEY = new KeyDefinition<SessionBanners[]>(
|
||||
BANNERS_DISMISSED_DISK,
|
||||
"bannersDismissed",
|
||||
{
|
||||
deserializer: (bannersDismissed) => bannersDismissed,
|
||||
},
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class VaultBannersService {
|
||||
shouldShowPremiumBanner$: Observable<boolean>;
|
||||
|
||||
private premiumBannerState: ActiveUserState<PremiumBannerReprompt>;
|
||||
private sessionBannerState: ActiveUserState<SessionBanners[]>;
|
||||
|
||||
/**
|
||||
* Emits when the sync service has completed a sync
|
||||
*
|
||||
* This is needed because `hasPremiumFromAnySource$` will emit false until the sync is completed
|
||||
* resulting in the premium banner being shown briefly on startup when the user has access to
|
||||
* premium features.
|
||||
*/
|
||||
private syncCompleted$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private tokenService: TokenService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private stateProvider: StateProvider,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private syncService: SyncService,
|
||||
) {
|
||||
this.pollUntilSynced();
|
||||
this.premiumBannerState = this.stateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY);
|
||||
this.sessionBannerState = this.stateProvider.getActive(BANNERS_DISMISSED_DISK_KEY);
|
||||
|
||||
const premiumSources$ = combineLatest([
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
|
||||
this.premiumBannerState.state$,
|
||||
]);
|
||||
|
||||
this.shouldShowPremiumBanner$ = this.syncCompleted$.pipe(
|
||||
take(1), // Wait until the first sync is complete before considering the premium status
|
||||
mergeMap(() => premiumSources$),
|
||||
map(([canAccessPremium, dismissedState]) => {
|
||||
const shouldShowPremiumBanner =
|
||||
!canAccessPremium && !this.platformUtilsService.isSelfHost();
|
||||
|
||||
// Check if nextPromptDate is in the past passed
|
||||
if (shouldShowPremiumBanner && dismissedState?.nextPromptDate) {
|
||||
const nextPromptDate = new Date(dismissedState.nextPromptDate);
|
||||
const now = new Date();
|
||||
return now >= nextPromptDate;
|
||||
}
|
||||
|
||||
return shouldShowPremiumBanner;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Returns true when the update browser banner should be shown */
|
||||
async shouldShowUpdateBrowserBanner(): Promise<boolean> {
|
||||
const outdatedBrowser = window.navigator.userAgent.indexOf("MSIE") !== -1;
|
||||
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||
VisibleVaultBanner.OutdatedBrowser,
|
||||
);
|
||||
|
||||
return outdatedBrowser && !alreadyDismissed;
|
||||
}
|
||||
|
||||
/** Returns true when the verify email banner should be shown */
|
||||
async shouldShowVerifyEmailBanner(): Promise<boolean> {
|
||||
const needsVerification = !(await this.tokenService.getEmailVerified());
|
||||
|
||||
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||
VisibleVaultBanner.VerifyEmail,
|
||||
);
|
||||
|
||||
return needsVerification && !alreadyDismissed;
|
||||
}
|
||||
|
||||
/** Returns true when the low KDF iteration banner should be shown */
|
||||
async shouldShowLowKDFBanner(): Promise<boolean> {
|
||||
const hasLowKDF = (await this.userVerificationService.hasMasterPassword())
|
||||
? await this.isLowKdfIteration()
|
||||
: false;
|
||||
|
||||
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||
VisibleVaultBanner.KDFSettings,
|
||||
);
|
||||
|
||||
return hasLowKDF && !alreadyDismissed;
|
||||
}
|
||||
|
||||
/** Dismiss the given banner and perform any respective side effects */
|
||||
async dismissBanner(banner: SessionBanners): Promise<void> {
|
||||
if (banner === VisibleVaultBanner.Premium) {
|
||||
await this.dismissPremiumBanner();
|
||||
} else {
|
||||
await this.sessionBannerState.update((current) => {
|
||||
const bannersDismissed = current ?? [];
|
||||
|
||||
return [...bannersDismissed, banner];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns banners that have already been dismissed */
|
||||
private async getBannerDismissedState(): Promise<SessionBanners[]> {
|
||||
// `state$` can emit null when a value has not been set yet,
|
||||
// use nullish coalescing to default to an empty array
|
||||
return (await firstValueFrom(this.sessionBannerState.state$)) ?? [];
|
||||
}
|
||||
|
||||
/** Increment dismissal state of the premium banner */
|
||||
private async dismissPremiumBanner(): Promise<void> {
|
||||
await this.premiumBannerState.update((current) => {
|
||||
const numberOfDismissals = current?.numberOfDismissals ?? 0;
|
||||
const now = new Date();
|
||||
|
||||
// Set midnight of the current day
|
||||
now.setHours(0, 0, 0, 0);
|
||||
|
||||
// First dismissal, re-prompt in 1 week
|
||||
if (numberOfDismissals === 0) {
|
||||
now.setDate(now.getDate() + 7);
|
||||
return {
|
||||
numberOfDismissals: 1,
|
||||
nextPromptDate: now.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
// Second dismissal, re-prompt in 1 month
|
||||
if (numberOfDismissals === 1) {
|
||||
now.setMonth(now.getMonth() + 1);
|
||||
return {
|
||||
numberOfDismissals: 2,
|
||||
nextPromptDate: now.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
// 3+ dismissals, re-prompt each year
|
||||
// Avoid day/month edge cases and only increment year
|
||||
const nextYear = new Date(now.getFullYear() + 1, now.getMonth(), now.getDate());
|
||||
nextYear.setHours(0, 0, 0, 0);
|
||||
return {
|
||||
numberOfDismissals: numberOfDismissals + 1,
|
||||
nextPromptDate: nextYear.getTime(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async isLowKdfIteration() {
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
return (
|
||||
kdfConfig.kdfType === KdfType.PBKDF2_SHA256 &&
|
||||
kdfConfig.iterations < PBKDF2_ITERATIONS.defaultValue
|
||||
);
|
||||
}
|
||||
|
||||
/** Poll the `syncService` until a sync is completed */
|
||||
private pollUntilSynced() {
|
||||
const interval = setInterval(async () => {
|
||||
const lastSync = await this.syncService.getLastSync();
|
||||
if (lastSync !== null) {
|
||||
clearInterval(interval);
|
||||
this.syncCompleted$.next();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<bit-banner
|
||||
id="update-browser-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
bannerType="warning"
|
||||
*ngIf="visibleBanners.includes(VisibleVaultBanner.OutdatedBrowser)"
|
||||
(onClose)="dismissBanner(VisibleVaultBanner.OutdatedBrowser)"
|
||||
>
|
||||
{{ "updateBrowserDesc" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="contrast"
|
||||
target="_blank"
|
||||
href="https://browser-update.org/update-browser.html"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ "updateBrowser" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
|
||||
<bit-banner
|
||||
id="kdf-settings-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
bannerType="warning"
|
||||
*ngIf="visibleBanners.includes(VisibleVaultBanner.KDFSettings)"
|
||||
(onClose)="dismissBanner(VisibleVaultBanner.KDFSettings)"
|
||||
>
|
||||
{{ "lowKDFIterationsBanner" | i18n }}
|
||||
<a bitLink linkType="contrast" routerLink="/settings/security/security-keys">
|
||||
{{ "changeKDFSettings" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
|
||||
<app-verify-email
|
||||
id="verify-email-banner"
|
||||
*ngIf="visibleBanners.includes(VisibleVaultBanner.VerifyEmail)"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
(onDismiss)="dismissBanner(VisibleVaultBanner.VerifyEmail)"
|
||||
(onVerified)="dismissBanner(VisibleVaultBanner.VerifyEmail)"
|
||||
></app-verify-email>
|
||||
|
||||
<bit-banner
|
||||
id="premium-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
bannerType="premium"
|
||||
*ngIf="premiumBannerVisible$ | async"
|
||||
(onClose)="dismissBanner(VisibleVaultBanner.Premium)"
|
||||
>
|
||||
{{ "premiumUpgradeUnlockFeatures" | i18n }}
|
||||
<a bitLink linkType="contrast" routerLink="/settings/subscription/premium">
|
||||
{{ "goPremium" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
@@ -0,0 +1,140 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { BannerComponent, BannerModule } from "@bitwarden/components";
|
||||
|
||||
import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
|
||||
import { LooseComponentsModule } from "../../../shared";
|
||||
|
||||
import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service";
|
||||
import { VaultBannersComponent } from "./vault-banners.component";
|
||||
|
||||
describe("VaultBannersComponent", () => {
|
||||
let component: VaultBannersComponent;
|
||||
let fixture: ComponentFixture<VaultBannersComponent>;
|
||||
const premiumBanner$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
const bannerService = mock<VaultBannersService>({
|
||||
shouldShowPremiumBanner$: premiumBanner$,
|
||||
shouldShowUpdateBrowserBanner: jest.fn(),
|
||||
shouldShowVerifyEmailBanner: jest.fn(),
|
||||
shouldShowLowKDFBanner: jest.fn(),
|
||||
dismissBanner: jest.fn(),
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
bannerService.shouldShowPremiumBanner$ = premiumBanner$;
|
||||
bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false);
|
||||
bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false);
|
||||
bannerService.shouldShowLowKDFBanner.mockResolvedValue(false);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [BannerModule, LooseComponentsModule, VerifyEmailComponent],
|
||||
declarations: [VaultBannersComponent, I18nPipe],
|
||||
providers: [
|
||||
{
|
||||
provide: VaultBannersService,
|
||||
useValue: bannerService,
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>({ t: (key) => key }),
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useValue: mock<ApiService>(),
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: mock<PlatformUtilsService>(),
|
||||
},
|
||||
{
|
||||
provide: TokenService,
|
||||
useValue: mock<TokenService>(),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(VaultBannersComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe("premiumBannerVisible$", () => {
|
||||
it("shows premium banner", async () => {
|
||||
premiumBanner$.next(true);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
||||
expect(banner.componentInstance.bannerType).toBe("premium");
|
||||
});
|
||||
|
||||
it("dismisses premium banner", async () => {
|
||||
premiumBanner$.next(false);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const banner = fixture.debugElement.query(By.directive(BannerComponent));
|
||||
expect(banner).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("determineVisibleBanner", () => {
|
||||
[
|
||||
{
|
||||
name: "OutdatedBrowser",
|
||||
method: bannerService.shouldShowUpdateBrowserBanner,
|
||||
banner: VisibleVaultBanner.OutdatedBrowser,
|
||||
},
|
||||
{
|
||||
name: "VerifyEmail",
|
||||
method: bannerService.shouldShowVerifyEmailBanner,
|
||||
banner: VisibleVaultBanner.VerifyEmail,
|
||||
},
|
||||
{
|
||||
name: "LowKDF",
|
||||
method: bannerService.shouldShowLowKDFBanner,
|
||||
banner: VisibleVaultBanner.KDFSettings,
|
||||
},
|
||||
].forEach(({ name, method, banner }) => {
|
||||
describe(name, () => {
|
||||
beforeEach(async () => {
|
||||
method.mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it(`shows ${name} banner`, async () => {
|
||||
expect(component.visibleBanners).toEqual([banner]);
|
||||
});
|
||||
|
||||
it(`dismisses ${name} banner`, async () => {
|
||||
const dismissButton = fixture.debugElement.nativeElement.querySelector(
|
||||
'button[biticonbutton="bwi-close"]',
|
||||
);
|
||||
|
||||
// Mock out the banner service returning false after dismissing
|
||||
method.mockResolvedValue(false);
|
||||
|
||||
dismissButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(bannerService.dismissBanner).toHaveBeenCalledWith(banner);
|
||||
|
||||
expect(component.visibleBanners).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { VaultBannersService, VisibleVaultBanner } from "./services/vault-banners.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-banners",
|
||||
templateUrl: "./vault-banners.component.html",
|
||||
})
|
||||
export class VaultBannersComponent implements OnInit {
|
||||
visibleBanners: VisibleVaultBanner[] = [];
|
||||
premiumBannerVisible$: Observable<boolean>;
|
||||
VisibleVaultBanner = VisibleVaultBanner;
|
||||
|
||||
constructor(private vaultBannerService: VaultBannersService) {
|
||||
this.premiumBannerVisible$ = this.vaultBannerService.shouldShowPremiumBanner$;
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.determineVisibleBanners();
|
||||
}
|
||||
|
||||
async dismissBanner(banner: VisibleVaultBanner): Promise<void> {
|
||||
await this.vaultBannerService.dismissBanner(banner);
|
||||
|
||||
await this.determineVisibleBanners();
|
||||
}
|
||||
|
||||
/** Determine which banners should be present */
|
||||
private async determineVisibleBanners(): Promise<void> {
|
||||
const showBrowserOutdated = await this.vaultBannerService.shouldShowUpdateBrowserBanner();
|
||||
const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner();
|
||||
const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner();
|
||||
|
||||
this.visibleBanners = [
|
||||
showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null,
|
||||
showVerifyEmail ? VisibleVaultBanner.VerifyEmail : null,
|
||||
showLowKdf ? VisibleVaultBanner.KDFSettings : null,
|
||||
].filter(Boolean); // remove all falsy values, i.e. null
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
<app-vault-banners></app-vault-banners>
|
||||
|
||||
<app-vault-header
|
||||
[filter]="filter"
|
||||
[loading]="refreshing && !performingInitialLoad"
|
||||
@@ -14,8 +16,8 @@
|
||||
<app-vault-onboarding [ciphers]="ciphers" [orgs]="allOrganizations" (onAddCipher)="addCipher()">
|
||||
</app-vault-onboarding>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="tw-flex tw-flex-row -tw-mx-2.5">
|
||||
<div class="tw-basis-1/4 tw-max-w-1/4 tw-px-2.5">
|
||||
<div class="groupings">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
@@ -30,7 +32,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
|
||||
<div class="tw-basis-3/4 tw-max-w-3/4 tw-px-2.5">
|
||||
<app-callout type="warning" *ngIf="activeFilter.isDeleted" icon="bwi-exclamation-triangle">
|
||||
{{ trashCleanupWarning }}
|
||||
</app-callout>
|
||||
@@ -81,44 +83,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<app-low-kdf class="d-block mb-4" *ngIf="showLowKdf"> </app-low-kdf>
|
||||
|
||||
<app-verify-email
|
||||
*ngIf="showVerifyEmail"
|
||||
class="d-block mb-4"
|
||||
(onVerified)="emailVerified($event)"
|
||||
></app-verify-email>
|
||||
|
||||
<div class="card border-warning mb-4" *ngIf="showBrowserOutdated">
|
||||
<div class="card-header bg-warning text-white">
|
||||
<i class="bwi bwi-exclamation-triangle bwi-fw" aria-hidden="true"></i>
|
||||
{{ "updateBrowser" | i18n }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ "updateBrowserDesc" | i18n }}</p>
|
||||
<a
|
||||
class="btn btn-block btn-outline-secondary"
|
||||
target="_blank"
|
||||
href="https://browser-update.org/update-browser.html"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ "updateBrowser" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card border-success mb-4" *ngIf="showPremiumCallout">
|
||||
<div class="card-header bg-success text-white">
|
||||
<i class="bwi bwi-star-f bwi-fw" aria-hidden="true"></i> {{ "goPremium" | i18n }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/subscription/premium">
|
||||
{{ "goPremium" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #attachments></ng-template>
|
||||
|
||||
@@ -35,9 +35,6 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -47,7 +44,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
@@ -122,10 +118,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
|
||||
collectionsModalRef: ViewContainerRef;
|
||||
|
||||
showVerifyEmail = false;
|
||||
showBrowserOutdated = false;
|
||||
showPremiumCallout = false;
|
||||
showLowKdf = false;
|
||||
trashCleanupWarning: string = null;
|
||||
kdfIterations: number;
|
||||
activeFilter: VaultFilter = new VaultFilter();
|
||||
@@ -161,7 +153,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private i18nService: I18nService,
|
||||
private modalService: ModalService,
|
||||
private dialogService: DialogService,
|
||||
private tokenService: TokenService,
|
||||
private messagingService: MessagingService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
@@ -180,14 +171,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private searchPipe: SearchPipe,
|
||||
private configService: ConfigService,
|
||||
private apiService: ApiService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private toastService: ToastService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.showBrowserOutdated = window.navigator.userAgent.indexOf("MSIE") !== -1;
|
||||
this.trashCleanupWarning = this.i18nService.t(
|
||||
this.platformUtilsService.isSelfHost()
|
||||
? "trashCleanupWarningSelfHosted"
|
||||
@@ -197,18 +185,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
const firstSetup$ = this.route.queryParams.pipe(
|
||||
first(),
|
||||
switchMap(async (params: Params) => {
|
||||
this.showVerifyEmail = !(await this.tokenService.getEmailVerified());
|
||||
this.showLowKdf = (await this.userVerificationService.hasMasterPassword())
|
||||
? await this.isLowKdfIteration()
|
||||
: false;
|
||||
await this.syncService.fullSync(false);
|
||||
|
||||
const canAccessPremium = await firstValueFrom(
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$,
|
||||
);
|
||||
this.showPremiumCallout =
|
||||
!this.showVerifyEmail && !canAccessPremium && !this.platformUtilsService.isSelfHost();
|
||||
|
||||
const cipherId = getCipherIdFromParams(params);
|
||||
if (!cipherId) {
|
||||
return;
|
||||
@@ -412,16 +390,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
get isShowingCards() {
|
||||
return (
|
||||
this.showBrowserOutdated || this.showPremiumCallout || this.showVerifyEmail || this.showLowKdf
|
||||
);
|
||||
}
|
||||
|
||||
emailVerified(verified: boolean) {
|
||||
this.showVerifyEmail = !verified;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
this.destroy$.next();
|
||||
@@ -1005,14 +973,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
: this.cipherService.softDeleteWithServer(id);
|
||||
}
|
||||
|
||||
async isLowKdfIteration() {
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
return (
|
||||
kdfConfig.kdfType === KdfType.PBKDF2_SHA256 &&
|
||||
kdfConfig.iterations < PBKDF2_ITERATIONS.defaultValue
|
||||
);
|
||||
}
|
||||
|
||||
protected async repromptCipher(ciphers: CipherView[]) {
|
||||
const notProtected = !ciphers.find((cipher) => cipher.reprompt !== CipherRepromptType.None);
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { BreadcrumbsModule } from "@bitwarden/components";
|
||||
import { BannerModule, BreadcrumbsModule } from "@bitwarden/components";
|
||||
|
||||
import { VerifyEmailComponent } from "../../auth/settings/verify-email.component";
|
||||
import { LooseComponentsModule, SharedModule } from "../../shared";
|
||||
import { CollectionDialogModule } from "../components/collection-dialog";
|
||||
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
||||
@@ -11,6 +12,8 @@ import { GroupBadgeModule } from "../org-vault/group-badge/group-badge.module";
|
||||
import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module";
|
||||
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "./pipes/pipes.module";
|
||||
import { VaultBannersService } from "./vault-banners/services/vault-banners.service";
|
||||
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./vault-onboarding/services/abstraction/vault-onboarding.service";
|
||||
@@ -34,10 +37,13 @@ import { VaultComponent } from "./vault.component";
|
||||
VaultItemsModule,
|
||||
CollectionDialogModule,
|
||||
VaultOnboardingComponent,
|
||||
BannerModule,
|
||||
VerifyEmailComponent,
|
||||
],
|
||||
declarations: [VaultComponent, VaultHeaderComponent],
|
||||
declarations: [VaultComponent, VaultHeaderComponent, VaultBannersComponent],
|
||||
exports: [VaultComponent],
|
||||
providers: [
|
||||
VaultBannersService,
|
||||
{
|
||||
provide: VaultOnboardingServiceAbstraction,
|
||||
useClass: VaultOnboardingService,
|
||||
|
||||
@@ -1,38 +1,19 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="purgeVaultTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="purgeVaultTitle">{{ "purgeVault" | i18n }}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ (organizationId ? "purgeOrgVaultDesc" : "purgeVaultDesc") | i18n }}</p>
|
||||
<app-callout type="warning">{{ "purgeVaultWarning" | i18n }}</app-callout>
|
||||
<app-user-verification [(ngModel)]="masterPassword" ngDefaultControl name="secret">
|
||||
</app-user-verification>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "purgeVault" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="default" [title]="'purgeVault' | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<p bitTypography="body1">
|
||||
{{ (organizationId ? "purgeOrgVaultDesc" : "purgeVaultDesc") | i18n }}
|
||||
</p>
|
||||
<app-callout type="warning">{{ "purgeVaultWarning" | i18n }}</app-callout>
|
||||
<app-user-verification formControlName="masterPassword"></app-user-verification>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton type="submit" buttonType="danger">
|
||||
{{ "purgeVault" | i18n }}
|
||||
</button>
|
||||
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -1,55 +1,60 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export interface PurgeVaultDialogData {
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-purge-vault",
|
||||
templateUrl: "purge-vault.component.html",
|
||||
})
|
||||
export class PurgeVaultComponent {
|
||||
@Input() organizationId?: string = null;
|
||||
organizationId: string = null;
|
||||
|
||||
masterPassword: Verification;
|
||||
formPromise: Promise<unknown>;
|
||||
formGroup = new FormGroup({
|
||||
masterPassword: new FormControl<Verification>(null),
|
||||
});
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: PurgeVaultDialogData,
|
||||
private dialogRef: DialogRef,
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private router: Router,
|
||||
private logService: LogService,
|
||||
private syncService: SyncService,
|
||||
) {}
|
||||
) {
|
||||
this.organizationId = data && data.organizationId ? data.organizationId : null;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.masterPassword)
|
||||
.then((request) => this.apiService.postPurgeCiphers(request, this.organizationId));
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("vaultPurged"));
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.syncService.fullSync(true);
|
||||
if (this.organizationId != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["organizations", this.organizationId, "vault"]);
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["vault"]);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
submit = async () => {
|
||||
const response = this.userVerificationService
|
||||
.buildRequest(this.formGroup.value.masterPassword)
|
||||
.then((request) => this.apiService.postPurgeCiphers(request, this.organizationId));
|
||||
await response;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("vaultPurged"));
|
||||
await this.syncService.fullSync(true);
|
||||
if (this.organizationId != null) {
|
||||
await this.router.navigate(["organizations", this.organizationId, "vault"]);
|
||||
} else {
|
||||
await this.router.navigate(["vault"]);
|
||||
}
|
||||
this.dialogRef.close();
|
||||
};
|
||||
|
||||
static open(dialogService: DialogService, config?: DialogConfig<PurgeVaultDialogData>) {
|
||||
return dialogService.open(PurgeVaultComponent, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8223,6 +8223,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lowKDFIterationsBanner": {
|
||||
"message": "Low KDF iterations. Increase your iterations to improve the security of your account."
|
||||
},
|
||||
"changeKDFSettings": {
|
||||
"message": "Change KDF settings"
|
||||
},
|
||||
"secureYourInfrastructure": {
|
||||
"message": "Secure your infrastructure"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user