mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 17:53:39 +00:00
Merge branch 'main' of github.com:bitwarden/clients
This commit is contained in:
@@ -3035,6 +3035,9 @@
|
|||||||
"accountSecurity": {
|
"accountSecurity": {
|
||||||
"message": "Account security"
|
"message": "Account security"
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"message": "Notifications"
|
||||||
|
},
|
||||||
"appearance": {
|
"appearance": {
|
||||||
"message": "Appearance"
|
"message": "Appearance"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ export const fido2AuthGuard: CanActivateFn = async (
|
|||||||
const authStatus = await authService.getAuthStatus();
|
const authStatus = await authService.getAuthStatus();
|
||||||
|
|
||||||
if (authStatus === AuthenticationStatus.Locked) {
|
if (authStatus === AuthenticationStatus.Locked) {
|
||||||
routerService.setPreviousUrl(state.url);
|
// Appending fromLock=true to the query params to indicate that the user is being redirected from the lock screen, this is used for user verification.
|
||||||
|
const previousUrl = `${state.url}&fromLock=true`;
|
||||||
|
routerService.setPreviousUrl(previousUrl);
|
||||||
return router.createUrlTree(["/lock"], { queryParams: route.queryParams });
|
return router.createUrlTree(["/lock"], { queryParams: route.queryParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<form #form (ngSubmit)="submit()">
|
<form #form (ngSubmit)="submit()">
|
||||||
<header>
|
<header>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<button type="button" routerLink="/tabs/settings">
|
<button type="button" routerLink="/notifications">
|
||||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||||
<span>{{ "back" | i18n }}</span>
|
<span>{{ "back" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -8,8 +8,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
import { enableAccountSwitching } from "../../platform/flags";
|
import { enableAccountSwitching } from "../../../platform/flags";
|
||||||
|
|
||||||
interface ExcludedDomain {
|
interface ExcludedDomain {
|
||||||
uri: string;
|
uri: string;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<header>
|
||||||
|
<div class="left">
|
||||||
|
<button type="button" routerLink="/tabs/settings">
|
||||||
|
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||||
|
<span>{{ "back" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h1 class="center">
|
||||||
|
<span class="title">{{ "notifications" | i18n }}</span>
|
||||||
|
</h1>
|
||||||
|
<div class="right">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main tabindex="-1">
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="use-passkeys">{{ "enableUsePasskeys" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="use-passkeys"
|
||||||
|
type="checkbox"
|
||||||
|
aria-describedby="use-passkeysHelp"
|
||||||
|
(change)="updateEnablePasskeys()"
|
||||||
|
[(ngModel)]="enablePasskeys"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="use-passkeysHelp" class="box-footer">
|
||||||
|
{{ "usePasskeysDesc" | i18n }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="addlogin-notification-bar">{{ "enableAddLoginNotification" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="addlogin-notification-bar"
|
||||||
|
type="checkbox"
|
||||||
|
aria-describedby="addlogin-notification-barHelp"
|
||||||
|
(change)="updateAddLoginNotification()"
|
||||||
|
[(ngModel)]="enableAddLoginNotification"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="addlogin-notification-barHelp" class="box-footer">
|
||||||
|
{{
|
||||||
|
accountSwitcherEnabled
|
||||||
|
? ("addLoginNotificationDescAlt" | i18n)
|
||||||
|
: ("addLoginNotificationDesc" | i18n)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="changedpass-notification-bar">{{
|
||||||
|
"enableChangedPasswordNotification" | i18n
|
||||||
|
}}</label>
|
||||||
|
<input
|
||||||
|
id="changedpass-notification-bar"
|
||||||
|
type="checkbox"
|
||||||
|
aria-describedby="changedpass-notification-barHelp"
|
||||||
|
(change)="updateChangedPasswordNotification()"
|
||||||
|
[(ngModel)]="enableChangedPasswordNotification"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="changedpass-notification-barHelp" class="box-footer">
|
||||||
|
{{
|
||||||
|
accountSwitcherEnabled
|
||||||
|
? ("changedPasswordNotificationDescAlt" | i18n)
|
||||||
|
: ("changedPasswordNotificationDesc" | i18n)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box list">
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/excluded-domains"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "excludedDomains" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
|
|
||||||
|
import { enableAccountSwitching } from "../../../platform/flags";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "autofill-notification-settings",
|
||||||
|
templateUrl: "notifications.component.html",
|
||||||
|
})
|
||||||
|
export class NotifcationsSettingsComponent implements OnInit {
|
||||||
|
enableAddLoginNotification = false;
|
||||||
|
enableChangedPasswordNotification = false;
|
||||||
|
enablePasskeys = true;
|
||||||
|
accountSwitcherEnabled = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||||
|
private vaultSettingsService: VaultSettingsService,
|
||||||
|
) {
|
||||||
|
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.enableAddLoginNotification = await firstValueFrom(
|
||||||
|
this.userNotificationSettingsService.enableAddedLoginPrompt$,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.enableChangedPasswordNotification = await firstValueFrom(
|
||||||
|
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAddLoginNotification() {
|
||||||
|
await this.userNotificationSettingsService.setEnableAddedLoginPrompt(
|
||||||
|
this.enableAddLoginNotification,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateChangedPasswordNotification() {
|
||||||
|
await this.userNotificationSettingsService.setEnableChangedPasswordPrompt(
|
||||||
|
this.enableChangedPasswordNotification,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEnablePasskeys() {
|
||||||
|
await this.vaultSettingsService.setEnablePasskeys(this.enablePasskeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input, OnInit } from "@angular/core";
|
import { Component, Input, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../browser-popup-utils";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-pop-out",
|
selector: "app-pop-out",
|
||||||
templateUrl: "pop-out.component.html",
|
templateUrl: "pop-out.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, JslibModule],
|
||||||
})
|
})
|
||||||
export class PopOutComponent implements OnInit {
|
export class PopOutComponent implements OnInit {
|
||||||
@Input() show = true;
|
@Input() show = true;
|
||||||
@@ -24,9 +28,7 @@ export class PopOutComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expand() {
|
async expand() {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await BrowserPopupUtils.openCurrentPagePopout(window);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
BrowserPopupUtils.openCurrentPagePopout(window);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,6 +163,10 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
|||||||
* the view is open.
|
* the view is open.
|
||||||
*/
|
*/
|
||||||
async isViewOpen(): Promise<boolean> {
|
async isViewOpen(): Promise<boolean> {
|
||||||
|
if (this.isSafari()) {
|
||||||
|
// Query views on safari since chrome.runtime.sendMessage does not timeout and will hang.
|
||||||
|
return BrowserApi.isPopupOpen();
|
||||||
|
}
|
||||||
return Boolean(await BrowserApi.sendMessageWithResponse("checkVaultPopupHeartbeat"));
|
return Boolean(await BrowserApi.sendMessageWithResponse("checkVaultPopupHeartbeat"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,9 +196,6 @@ export const routerTransition = trigger("routerTransition", [
|
|||||||
transition("vault-settings => sync", inSlideLeft),
|
transition("vault-settings => sync", inSlideLeft),
|
||||||
transition("sync => vault-settings", outSlideRight),
|
transition("sync => vault-settings", outSlideRight),
|
||||||
|
|
||||||
transition("tabs => excluded-domains", inSlideLeft),
|
|
||||||
transition("excluded-domains => tabs", outSlideRight),
|
|
||||||
|
|
||||||
transition("tabs => options", inSlideLeft),
|
transition("tabs => options", inSlideLeft),
|
||||||
transition("options => tabs", outSlideRight),
|
transition("options => tabs", outSlideRight),
|
||||||
|
|
||||||
@@ -223,6 +220,13 @@ export const routerTransition = trigger("routerTransition", [
|
|||||||
transition("tabs => edit-send, send-type => edit-send", inSlideUp),
|
transition("tabs => edit-send, send-type => edit-send", inSlideUp),
|
||||||
transition("edit-send => tabs, edit-send => send-type", outSlideDown),
|
transition("edit-send => tabs, edit-send => send-type", outSlideDown),
|
||||||
|
|
||||||
|
// Notification settings
|
||||||
|
transition("tabs => notifications", inSlideLeft),
|
||||||
|
transition("notifications => tabs", outSlideRight),
|
||||||
|
|
||||||
|
transition("notifications => excluded-domains", inSlideLeft),
|
||||||
|
transition("excluded-domains => notifications", outSlideRight),
|
||||||
|
|
||||||
transition("tabs => autofill", inSlideLeft),
|
transition("tabs => autofill", inSlideLeft),
|
||||||
transition("autofill => tabs", outSlideRight),
|
transition("autofill => tabs", outSlideRight),
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp
|
|||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
|
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
||||||
|
import { NotifcationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||||
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
||||||
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
||||||
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
||||||
@@ -56,7 +58,6 @@ import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.c
|
|||||||
|
|
||||||
import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils";
|
import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils";
|
||||||
import { debounceNavigationGuard } from "./services/debounce-navigation.service";
|
import { debounceNavigationGuard } from "./services/debounce-navigation.service";
|
||||||
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
|
||||||
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
||||||
import { OptionsComponent } from "./settings/options.component";
|
import { OptionsComponent } from "./settings/options.component";
|
||||||
import { TabsV2Component } from "./tabs-v2.component";
|
import { TabsV2Component } from "./tabs-v2.component";
|
||||||
@@ -256,6 +257,12 @@ const routes: Routes = [
|
|||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "account-security" },
|
data: { state: "account-security" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "notifications",
|
||||||
|
component: NotifcationsSettingsComponent,
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
data: { state: "notifications" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "vault-settings",
|
path: "vault-settings",
|
||||||
component: VaultSettingsComponent,
|
component: VaultSettingsComponent,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components
|
|||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||||
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||||
|
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||||
import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components";
|
import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components";
|
||||||
import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui";
|
import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui";
|
||||||
|
|
||||||
@@ -37,7 +38,10 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp
|
|||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
|
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
||||||
|
import { NotifcationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||||
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
||||||
|
import { PopOutComponent } from "../platform/popup/components/pop-out.component";
|
||||||
import { HeaderComponent } from "../platform/popup/header.component";
|
import { HeaderComponent } from "../platform/popup/header.component";
|
||||||
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
|
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
|
||||||
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
|
||||||
@@ -78,10 +82,8 @@ import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.c
|
|||||||
|
|
||||||
import { AppRoutingModule } from "./app-routing.module";
|
import { AppRoutingModule } from "./app-routing.module";
|
||||||
import { AppComponent } from "./app.component";
|
import { AppComponent } from "./app.component";
|
||||||
import { PopOutComponent } from "./components/pop-out.component";
|
|
||||||
import { UserVerificationComponent } from "./components/user-verification.component";
|
import { UserVerificationComponent } from "./components/user-verification.component";
|
||||||
import { ServicesModule } from "./services/services.module";
|
import { ServicesModule } from "./services/services.module";
|
||||||
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
|
||||||
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
||||||
import { OptionsComponent } from "./settings/options.component";
|
import { OptionsComponent } from "./settings/options.component";
|
||||||
import { TabsV2Component } from "./tabs-v2.component";
|
import { TabsV2Component } from "./tabs-v2.component";
|
||||||
@@ -116,10 +118,12 @@ import "../platform/popup/locales";
|
|||||||
AccountComponent,
|
AccountComponent,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
ExportScopeCalloutComponent,
|
ExportScopeCalloutComponent,
|
||||||
|
PopOutComponent,
|
||||||
PopupPageComponent,
|
PopupPageComponent,
|
||||||
PopupTabNavigationComponent,
|
PopupTabNavigationComponent,
|
||||||
PopupFooterComponent,
|
PopupFooterComponent,
|
||||||
PopupHeaderComponent,
|
PopupHeaderComponent,
|
||||||
|
UserVerificationDialogComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
ActionButtonsComponent,
|
ActionButtonsComponent,
|
||||||
@@ -149,11 +153,11 @@ import "../platform/popup/locales";
|
|||||||
LoginViaAuthRequestComponent,
|
LoginViaAuthRequestComponent,
|
||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponent,
|
||||||
OptionsComponent,
|
OptionsComponent,
|
||||||
|
NotifcationsSettingsComponent,
|
||||||
AppearanceComponent,
|
AppearanceComponent,
|
||||||
GeneratorComponent,
|
GeneratorComponent,
|
||||||
PasswordGeneratorHistoryComponent,
|
PasswordGeneratorHistoryComponent,
|
||||||
PasswordHistoryComponent,
|
PasswordHistoryComponent,
|
||||||
PopOutComponent,
|
|
||||||
PremiumComponent,
|
PremiumComponent,
|
||||||
RegisterComponent,
|
RegisterComponent,
|
||||||
SendAddEditComponent,
|
SendAddEditComponent,
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
|||||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { UnauthGuardService } from "../../auth/popup/services";
|
import { UnauthGuardService } from "../../auth/popup/services";
|
||||||
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
||||||
@@ -117,6 +118,7 @@ import { ForegroundMemoryStorageService } from "../../platform/storage/foregroun
|
|||||||
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
|
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
|
||||||
import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service";
|
import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service";
|
||||||
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
|
import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service";
|
||||||
|
import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service";
|
||||||
import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service";
|
import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service";
|
||||||
import { VaultFilterService } from "../../vault/services/vault-filter.service";
|
import { VaultFilterService } from "../../vault/services/vault-filter.service";
|
||||||
|
|
||||||
@@ -600,6 +602,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
provide: CLIENT_TYPE,
|
provide: CLIENT_TYPE,
|
||||||
useValue: ClientType.Browser,
|
useValue: ClientType.Browser,
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: Fido2UserVerificationService,
|
||||||
|
useClass: Fido2UserVerificationService,
|
||||||
|
deps: [PasswordRepromptService, UserVerificationService, DialogService],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -60,67 +60,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="totpHelp" class="box-footer">{{ "disableAutoTotpCopyDesc" | i18n }}</div>
|
<div id="totpHelp" class="box-footer">{{ "disableAutoTotpCopyDesc" | i18n }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="addlogin-notification-bar">{{ "enableAddLoginNotification" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="addlogin-notification-bar"
|
|
||||||
type="checkbox"
|
|
||||||
aria-describedby="addlogin-notification-barHelp"
|
|
||||||
(change)="updateAddLoginNotification()"
|
|
||||||
[(ngModel)]="enableAddLoginNotification"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="addlogin-notification-barHelp" class="box-footer">
|
|
||||||
{{
|
|
||||||
accountSwitcherEnabled
|
|
||||||
? ("addLoginNotificationDescAlt" | i18n)
|
|
||||||
: ("addLoginNotificationDesc" | i18n)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="changedpass-notification-bar">{{
|
|
||||||
"enableChangedPasswordNotification" | i18n
|
|
||||||
}}</label>
|
|
||||||
<input
|
|
||||||
id="changedpass-notification-bar"
|
|
||||||
type="checkbox"
|
|
||||||
aria-describedby="changedpass-notification-barHelp"
|
|
||||||
(change)="updateChangedPasswordNotification()"
|
|
||||||
[(ngModel)]="enableChangedPasswordNotification"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="changedpass-notification-barHelp" class="box-footer">
|
|
||||||
{{
|
|
||||||
accountSwitcherEnabled
|
|
||||||
? ("changedPasswordNotificationDescAlt" | i18n)
|
|
||||||
: ("changedPasswordNotificationDesc" | i18n)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="use-passkeys">{{ "enableUsePasskeys" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="use-passkeys"
|
|
||||||
type="checkbox"
|
|
||||||
aria-describedby="use-passkeysHelp"
|
|
||||||
(change)="updateEnablePasskeys()"
|
|
||||||
[(ngModel)]="enablePasskeys"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="use-passkeysHelp" class="box-footer">
|
|
||||||
{{ "usePasskeysDesc" | i18n }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="box-content">
|
<div class="box-content">
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { firstValueFrom } from "rxjs";
|
|||||||
|
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
|
||||||
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
|
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
|
||||||
import {
|
import {
|
||||||
UriMatchStrategy,
|
UriMatchStrategy,
|
||||||
@@ -25,9 +24,6 @@ export class OptionsComponent implements OnInit {
|
|||||||
autoFillOnPageLoadOptions: any[];
|
autoFillOnPageLoadOptions: any[];
|
||||||
enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true?
|
enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true?
|
||||||
enableContextMenuItem = false;
|
enableContextMenuItem = false;
|
||||||
enableAddLoginNotification = false;
|
|
||||||
enableChangedPasswordNotification = false;
|
|
||||||
enablePasskeys = true;
|
|
||||||
showCardsCurrentTab = false;
|
showCardsCurrentTab = false;
|
||||||
showIdentitiesCurrentTab = false;
|
showIdentitiesCurrentTab = false;
|
||||||
showClearClipboard = true;
|
showClearClipboard = true;
|
||||||
@@ -36,13 +32,11 @@ export class OptionsComponent implements OnInit {
|
|||||||
clearClipboard: ClearClipboardDelaySetting;
|
clearClipboard: ClearClipboardDelaySetting;
|
||||||
clearClipboardOptions: any[];
|
clearClipboardOptions: any[];
|
||||||
showGeneral = true;
|
showGeneral = true;
|
||||||
showAutofill = true;
|
|
||||||
showDisplay = true;
|
showDisplay = true;
|
||||||
accountSwitcherEnabled = false;
|
accountSwitcherEnabled = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
private domainSettingsService: DomainSettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
@@ -82,14 +76,6 @@ export class OptionsComponent implements OnInit {
|
|||||||
this.autofillSettingsService.autofillOnPageLoadDefault$,
|
this.autofillSettingsService.autofillOnPageLoadDefault$,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.enableAddLoginNotification = await firstValueFrom(
|
|
||||||
this.userNotificationSettingsService.enableAddedLoginPrompt$,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.enableChangedPasswordNotification = await firstValueFrom(
|
|
||||||
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.enableContextMenuItem = await firstValueFrom(
|
this.enableContextMenuItem = await firstValueFrom(
|
||||||
this.autofillSettingsService.enableContextMenu$,
|
this.autofillSettingsService.enableContextMenu$,
|
||||||
);
|
);
|
||||||
@@ -101,8 +87,6 @@ export class OptionsComponent implements OnInit {
|
|||||||
|
|
||||||
this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$);
|
this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$);
|
||||||
|
|
||||||
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
|
|
||||||
|
|
||||||
const defaultUriMatch = await firstValueFrom(
|
const defaultUriMatch = await firstValueFrom(
|
||||||
this.domainSettingsService.defaultUriMatchStrategy$,
|
this.domainSettingsService.defaultUriMatchStrategy$,
|
||||||
);
|
);
|
||||||
@@ -111,22 +95,6 @@ export class OptionsComponent implements OnInit {
|
|||||||
this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$);
|
this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAddLoginNotification() {
|
|
||||||
await this.userNotificationSettingsService.setEnableAddedLoginPrompt(
|
|
||||||
this.enableAddLoginNotification,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateChangedPasswordNotification() {
|
|
||||||
await this.userNotificationSettingsService.setEnableChangedPasswordPrompt(
|
|
||||||
this.enableChangedPasswordNotification,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateEnablePasskeys() {
|
|
||||||
await this.vaultSettingsService.setEnablePasskeys(this.enablePasskeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateContextMenuItem() {
|
async updateContextMenuItem() {
|
||||||
await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem);
|
await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem);
|
||||||
this.messagingService.send("bgUpdateContextMenu");
|
this.messagingService.send("bgUpdateContextMenu");
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { ButtonModule, DialogModule } from "@bitwarden/components";
|
import { ButtonModule, DialogModule } from "@bitwarden/components";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: "about.component.html",
|
templateUrl: "about-dialog.component.html",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, JslibModule, DialogModule, ButtonModule],
|
imports: [CommonModule, JslibModule, DialogModule, ButtonModule],
|
||||||
})
|
})
|
||||||
export class AboutComponent {
|
export class AboutDialogComponent {
|
||||||
protected year = new Date().getFullYear();
|
protected year = new Date().getFullYear();
|
||||||
protected version$: Observable<string>;
|
protected version$: Observable<string>;
|
||||||
|
|
||||||
@@ -30,17 +30,17 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="box-content-row box-content-row-flex text-default"
|
class="box-content-row box-content-row-flex text-default"
|
||||||
routerLink="/vault-settings"
|
routerLink="/notifications"
|
||||||
>
|
>
|
||||||
<div class="row-main">{{ "vault" | i18n }}</div>
|
<div class="row-main">{{ "notifications" | i18n }}</div>
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="box-content-row box-content-row-flex text-default"
|
class="box-content-row box-content-row-flex text-default"
|
||||||
routerLink="/excluded-domains"
|
routerLink="/vault-settings"
|
||||||
>
|
>
|
||||||
<div class="row-main">{{ "excludedDomains" | i18n }}</div>
|
<div class="row-main">{{ "vault" | i18n }}</div>
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { DialogService } from "@bitwarden/components";
|
|||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
import { AboutComponent } from "./about/about.component";
|
import { AboutDialogComponent } from "./about-dialog/about-dialog.component";
|
||||||
|
|
||||||
const RateUrls = {
|
const RateUrls = {
|
||||||
[DeviceType.ChromeExtension]:
|
[DeviceType.ChromeExtension]:
|
||||||
@@ -84,7 +84,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
about() {
|
about() {
|
||||||
this.dialogService.open(AboutComponent);
|
this.dialogService.open(AboutDialogComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
rate() {
|
rate() {
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view
|
|||||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||||
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
|
||||||
|
|
||||||
import { ZonedMessageListenerService } from "../../../../platform/browser/zoned-message-listener.service";
|
import { ZonedMessageListenerService } from "../../../../platform/browser/zoned-message-listener.service";
|
||||||
import {
|
import {
|
||||||
BrowserFido2Message,
|
BrowserFido2Message,
|
||||||
BrowserFido2UserInterfaceSession,
|
BrowserFido2UserInterfaceSession,
|
||||||
} from "../../../fido2/browser-fido2-user-interface.service";
|
} from "../../../fido2/browser-fido2-user-interface.service";
|
||||||
|
import { Fido2UserVerificationService } from "../../../services/fido2-user-verification.service";
|
||||||
import { VaultPopoutType } from "../../utils/vault-popout-window";
|
import { VaultPopoutType } from "../../utils/vault-popout-window";
|
||||||
|
|
||||||
interface ViewData {
|
interface ViewData {
|
||||||
@@ -59,6 +59,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
protected data$: Observable<ViewData>;
|
protected data$: Observable<ViewData>;
|
||||||
protected sessionId?: string;
|
protected sessionId?: string;
|
||||||
protected senderTabId?: string;
|
protected senderTabId?: string;
|
||||||
|
protected fromLock?: boolean;
|
||||||
protected ciphers?: CipherView[] = [];
|
protected ciphers?: CipherView[] = [];
|
||||||
protected displayedCiphers?: CipherView[] = [];
|
protected displayedCiphers?: CipherView[] = [];
|
||||||
protected loading = false;
|
protected loading = false;
|
||||||
@@ -71,13 +72,13 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private domainSettingsService: DomainSettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private browserMessagingApi: ZonedMessageListenerService,
|
private browserMessagingApi: ZonedMessageListenerService,
|
||||||
|
private fido2UserVerificationService: Fido2UserVerificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -89,6 +90,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
sessionId: queryParamMap.get("sessionId"),
|
sessionId: queryParamMap.get("sessionId"),
|
||||||
senderTabId: queryParamMap.get("senderTabId"),
|
senderTabId: queryParamMap.get("senderTabId"),
|
||||||
senderUrl: queryParamMap.get("senderUrl"),
|
senderUrl: queryParamMap.get("senderUrl"),
|
||||||
|
fromLock: queryParamMap.get("fromLock"),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -101,6 +103,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.sessionId = queryParams.sessionId;
|
this.sessionId = queryParams.sessionId;
|
||||||
this.senderTabId = queryParams.senderTabId;
|
this.senderTabId = queryParams.senderTabId;
|
||||||
this.url = queryParams.senderUrl;
|
this.url = queryParams.senderUrl;
|
||||||
|
this.fromLock = queryParams.fromLock === "true";
|
||||||
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
|
// For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session.
|
||||||
if (
|
if (
|
||||||
message.type === "NewSessionCreatedRequest" &&
|
message.type === "NewSessionCreatedRequest" &&
|
||||||
@@ -210,7 +213,11 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
protected async submit() {
|
protected async submit() {
|
||||||
const data = this.message$.value;
|
const data = this.message$.value;
|
||||||
if (data?.type === "PickCredentialRequest") {
|
if (data?.type === "PickCredentialRequest") {
|
||||||
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
|
const userVerified = await this.fido2UserVerificationService.handleUserVerification(
|
||||||
|
data.userVerification,
|
||||||
|
this.cipher,
|
||||||
|
this.fromLock,
|
||||||
|
);
|
||||||
|
|
||||||
this.send({
|
this.send({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
@@ -231,7 +238,11 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userVerified = await this.handleUserVerification(data.userVerification, this.cipher);
|
const userVerified = await this.fido2UserVerificationService.handleUserVerification(
|
||||||
|
data.userVerification,
|
||||||
|
this.cipher,
|
||||||
|
this.fromLock,
|
||||||
|
);
|
||||||
|
|
||||||
this.send({
|
this.send({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
@@ -248,14 +259,21 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
const data = this.message$.value;
|
const data = this.message$.value;
|
||||||
if (data?.type === "ConfirmNewCredentialRequest") {
|
if (data?.type === "ConfirmNewCredentialRequest") {
|
||||||
const name = data.credentialName || data.rpId;
|
const name = data.credentialName || data.rpId;
|
||||||
await this.createNewCipher(name);
|
const userVerified = await this.fido2UserVerificationService.handleUserVerification(
|
||||||
|
data.userVerification,
|
||||||
|
this.cipher,
|
||||||
|
this.fromLock,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data.userVerification || userVerified) {
|
||||||
|
await this.createNewCipher(name);
|
||||||
|
}
|
||||||
|
|
||||||
// We are bypassing user verification pending implementation of PIN and biometric support.
|
|
||||||
this.send({
|
this.send({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
cipherId: this.cipher?.id,
|
cipherId: this.cipher?.id,
|
||||||
type: "ConfirmNewCredentialResponse",
|
type: "ConfirmNewCredentialResponse",
|
||||||
userVerified: data.userVerification,
|
userVerified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +322,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
uilocation: "popout",
|
uilocation: "popout",
|
||||||
senderTabId: this.senderTabId,
|
senderTabId: this.senderTabId,
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
|
fromLock: this.fromLock,
|
||||||
userVerification: data.userVerification,
|
userVerification: data.userVerification,
|
||||||
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`,
|
||||||
},
|
},
|
||||||
@@ -374,20 +393,6 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleUserVerification(
|
|
||||||
userVerificationRequested: boolean,
|
|
||||||
cipher: CipherView,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0;
|
|
||||||
|
|
||||||
if (masterPasswordRepromptRequired) {
|
|
||||||
return await this.passwordRepromptService.showPasswordPrompt();
|
|
||||||
}
|
|
||||||
|
|
||||||
// We are bypassing user verification pending implementation of PIN and biometric support.
|
|
||||||
return userVerificationRequested;
|
|
||||||
}
|
|
||||||
|
|
||||||
private send(msg: BrowserFido2Message) {
|
private send(msg: BrowserFido2Message) {
|
||||||
BrowserFido2UserInterfaceSession.sendMessage({
|
BrowserFido2UserInterfaceSession.sendMessage({
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { BrowserApi } from "../../../../platform/browser/browser-api";
|
|||||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||||
import { PopupCloseWarningService } from "../../../../popup/services/popup-close-warning.service";
|
import { PopupCloseWarningService } from "../../../../popup/services/popup-close-warning.service";
|
||||||
import { BrowserFido2UserInterfaceSession } from "../../../fido2/browser-fido2-user-interface.service";
|
import { BrowserFido2UserInterfaceSession } from "../../../fido2/browser-fido2-user-interface.service";
|
||||||
|
import { Fido2UserVerificationService } from "../../../services/fido2-user-verification.service";
|
||||||
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
|
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
|
||||||
import { closeAddEditVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
|
import { closeAddEditVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
|
||||||
|
|
||||||
@@ -69,6 +70,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
datePipe: DatePipe,
|
datePipe: DatePipe,
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
|
private fido2UserVerificationService: Fido2UserVerificationService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
cipherService,
|
cipherService,
|
||||||
@@ -168,11 +170,17 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
|
|
||||||
async submit(): Promise<boolean> {
|
async submit(): Promise<boolean> {
|
||||||
const fido2SessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
const fido2SessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||||
const { isFido2Session, sessionId, userVerification } = fido2SessionData;
|
const { isFido2Session, sessionId, userVerification, fromLock } = fido2SessionData;
|
||||||
const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session;
|
const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
inFido2PopoutWindow &&
|
inFido2PopoutWindow &&
|
||||||
!(await this.handleFido2UserVerification(sessionId, userVerification))
|
userVerification &&
|
||||||
|
!(await this.fido2UserVerificationService.handleUserVerification(
|
||||||
|
userVerification,
|
||||||
|
this.cipher,
|
||||||
|
fromLock,
|
||||||
|
))
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -327,14 +335,6 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleFido2UserVerification(
|
|
||||||
sessionId: string,
|
|
||||||
userVerification: boolean,
|
|
||||||
): Promise<boolean> {
|
|
||||||
// We are bypassing user verification pending implementation of PIN and biometric support.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
repromptChanged() {
|
repromptChanged() {
|
||||||
super.repromptChanged();
|
super.repromptChanged();
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function fido2PopoutSessionData$() {
|
|||||||
fallbackSupported: queryParams.fallbackSupported === "true",
|
fallbackSupported: queryParams.fallbackSupported === "true",
|
||||||
userVerification: queryParams.userVerification === "true",
|
userVerification: queryParams.userVerification === "true",
|
||||||
senderUrl: queryParams.senderUrl as string,
|
senderUrl: queryParams.senderUrl as string,
|
||||||
|
fromLock: queryParams.fromLock === "true",
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||||
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { SetPinComponent } from "./../../auth/popup/components/set-pin.component";
|
||||||
|
import { Fido2UserVerificationService } from "./fido2-user-verification.service";
|
||||||
|
|
||||||
|
jest.mock("@bitwarden/auth/angular", () => ({
|
||||||
|
UserVerificationDialogComponent: {
|
||||||
|
open: jest.fn().mockResolvedValue({ userAction: "confirm", verificationSuccess: true }),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../../auth/popup/components/set-pin.component", () => {
|
||||||
|
return {
|
||||||
|
SetPinComponent: {
|
||||||
|
open: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Fido2UserVerificationService", () => {
|
||||||
|
let fido2UserVerificationService: Fido2UserVerificationService;
|
||||||
|
|
||||||
|
let passwordRepromptService: MockProxy<PasswordRepromptService>;
|
||||||
|
let userVerificationService: MockProxy<UserVerificationService>;
|
||||||
|
let dialogService: MockProxy<DialogService>;
|
||||||
|
let cipher: CipherView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
passwordRepromptService = mock<PasswordRepromptService>();
|
||||||
|
userVerificationService = mock<UserVerificationService>();
|
||||||
|
dialogService = mock<DialogService>();
|
||||||
|
|
||||||
|
cipher = createCipherView();
|
||||||
|
|
||||||
|
fido2UserVerificationService = new Fido2UserVerificationService(
|
||||||
|
passwordRepromptService,
|
||||||
|
userVerificationService,
|
||||||
|
dialogService,
|
||||||
|
);
|
||||||
|
|
||||||
|
(UserVerificationDialogComponent.open as jest.Mock).mockResolvedValue({
|
||||||
|
userAction: "confirm",
|
||||||
|
verificationSuccess: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("handleUserVerification", () => {
|
||||||
|
describe("user verification requested is true", () => {
|
||||||
|
it("should return true if user is redirected from lock screen and master password reprompt is not required", async () => {
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
true,
|
||||||
|
cipher,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => {
|
||||||
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
|
userVerificationService.hasMasterPassword.mockResolvedValue(true);
|
||||||
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
true,
|
||||||
|
cipher,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(passwordRepromptService.showPasswordPrompt).toHaveBeenCalled();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||||
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
|
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||||
|
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
true,
|
||||||
|
cipher,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, {
|
||||||
|
clientSideOnlyVerification: true,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||||
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
|
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||||
|
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
true,
|
||||||
|
cipher,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, {
|
||||||
|
clientSideOnlyVerification: true,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => {
|
||||||
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
|
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||||
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
true,
|
||||||
|
cipher,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, {
|
||||||
|
clientSideOnlyVerification: true,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call user verification dialog if user is not redirected from lock screen and no master password reprompt is required", async () => {
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
true,
|
||||||
|
cipher,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, {
|
||||||
|
clientSideOnlyVerification: true,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prompt user to set pin if user has no verification method", async () => {
|
||||||
|
(UserVerificationDialogComponent.open as jest.Mock).mockResolvedValue({
|
||||||
|
userAction: "confirm",
|
||||||
|
verificationSuccess: false,
|
||||||
|
noAvailableClientVerificationMethods: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fido2UserVerificationService.handleUserVerification(true, cipher, false);
|
||||||
|
|
||||||
|
expect(SetPinComponent.open).toHaveBeenCalledWith(dialogService);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("user verification requested is false", () => {
|
||||||
|
it("should return false if user is redirected from lock screen and master password reprompt is not required", async () => {
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
false,
|
||||||
|
cipher,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if user is not redirected from lock screen and master password reprompt is not required", async () => {
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
false,
|
||||||
|
cipher,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => {
|
||||||
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
|
userVerificationService.hasMasterPassword.mockResolvedValue(true);
|
||||||
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
false,
|
||||||
|
cipher,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(passwordRepromptService.showPasswordPrompt).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||||
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
|
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||||
|
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
false,
|
||||||
|
cipher,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, {
|
||||||
|
clientSideOnlyVerification: true,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => {
|
||||||
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
|
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||||
|
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
false,
|
||||||
|
cipher,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, {
|
||||||
|
clientSideOnlyVerification: true,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => {
|
||||||
|
cipher.reprompt = CipherRepromptType.Password;
|
||||||
|
userVerificationService.hasMasterPassword.mockResolvedValue(false);
|
||||||
|
passwordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await fido2UserVerificationService.handleUserVerification(
|
||||||
|
false,
|
||||||
|
cipher,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, {
|
||||||
|
clientSideOnlyVerification: true,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createCipherView() {
|
||||||
|
const cipher = new CipherView();
|
||||||
|
cipher.id = Utils.newGuid();
|
||||||
|
cipher.type = CipherType.Login;
|
||||||
|
cipher.reprompt = CipherRepromptType.None;
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||||
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { SetPinComponent } from "../../auth/popup/components/set-pin.component";
|
||||||
|
|
||||||
|
export class Fido2UserVerificationService {
|
||||||
|
constructor(
|
||||||
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
|
private userVerificationService: UserVerificationService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles user verification for a user based on the cipher and user verification requested.
|
||||||
|
* @param userVerificationRequested Indicates if user verification is required or not.
|
||||||
|
* @param cipher Contains details about the cipher including master password reprompt.
|
||||||
|
* @param fromLock Indicates if the request is from the lock screen.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async handleUserVerification(
|
||||||
|
userVerificationRequested: boolean,
|
||||||
|
cipher: CipherView,
|
||||||
|
fromLock: boolean,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0;
|
||||||
|
|
||||||
|
// If the request is from the lock screen, treat unlocking the vault as user verification,
|
||||||
|
// unless a master password reprompt is required.
|
||||||
|
if (fromLock) {
|
||||||
|
return masterPasswordRepromptRequired
|
||||||
|
? await this.handleMasterPasswordReprompt()
|
||||||
|
: userVerificationRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (masterPasswordRepromptRequired) {
|
||||||
|
return await this.handleMasterPasswordReprompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userVerificationRequested) {
|
||||||
|
return await this.showUserVerificationDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
return userVerificationRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showMasterPasswordReprompt(): Promise<boolean> {
|
||||||
|
return await this.passwordRepromptService.showPasswordPrompt();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showUserVerificationDialog(): Promise<boolean> {
|
||||||
|
const result = await UserVerificationDialogComponent.open(this.dialogService, {
|
||||||
|
clientSideOnlyVerification: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.userAction === "cancel") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unsuccessful verification attempts.
|
||||||
|
if (!result.verificationSuccess) {
|
||||||
|
// Check if no client-side verification methods are available.
|
||||||
|
if (result.noAvailableClientVerificationMethods) {
|
||||||
|
return await this.promptUserToSetPin();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.verificationSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleMasterPasswordReprompt(): Promise<boolean> {
|
||||||
|
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
||||||
|
|
||||||
|
// TDE users have no master password, so we need to use the UserVerification prompt
|
||||||
|
return hasMasterPassword
|
||||||
|
? await this.showMasterPasswordReprompt()
|
||||||
|
: await this.showUserVerificationDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async promptUserToSetPin() {
|
||||||
|
const dialogRef = SetPinComponent.open(this.dialogService);
|
||||||
|
|
||||||
|
if (!dialogRef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userHasPinSet = await firstValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
|
if (!userHasPinSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user has set a PIN, re-invoke the user verification dialog to complete the verification process.
|
||||||
|
return await this.showUserVerificationDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,23 +165,4 @@ export class OrganizationUserResetPasswordService {
|
|||||||
}
|
}
|
||||||
return requests;
|
return requests;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Nov 6, 2023: Use new Key Rotation Service for posting rotated data.
|
|
||||||
*/
|
|
||||||
async postLegacyRotation(
|
|
||||||
userId: string,
|
|
||||||
requests: OrganizationUserResetPasswordWithIdRequest[],
|
|
||||||
): Promise<void> {
|
|
||||||
if (requests == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const request of requests) {
|
|
||||||
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
|
|
||||||
request.organizationId,
|
|
||||||
userId,
|
|
||||||
request,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { concatMap, takeUntil, map, lastValueFrom } from "rxjs";
|
import { concatMap, takeUntil, map } from "rxjs";
|
||||||
import { tap } from "rxjs/operators";
|
import { tap } from "rxjs/operators";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
@@ -16,7 +16,6 @@ import { DialogService } from "@bitwarden/components";
|
|||||||
|
|
||||||
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
|
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
|
||||||
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
|
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
|
||||||
import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-verify.component";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-two-factor-setup",
|
selector: "app-two-factor-setup",
|
||||||
@@ -66,17 +65,17 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
|
|||||||
async manage(type: TwoFactorProviderType) {
|
async manage(type: TwoFactorProviderType) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TwoFactorProviderType.OrganizationDuo: {
|
case TwoFactorProviderType.OrganizationDuo: {
|
||||||
const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, {
|
const result: AuthResponse<TwoFactorDuoResponse> = await this.callTwoFactorVerifyDialog(
|
||||||
data: { type: type, organizationId: this.organizationId },
|
TwoFactorProviderType.OrganizationDuo,
|
||||||
});
|
|
||||||
const result: AuthResponse<TwoFactorDuoResponse> = await lastValueFrom(
|
|
||||||
twoFactorVerifyDialogRef.closed,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
|
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
|
||||||
|
duoComp.type = TwoFactorProviderType.OrganizationDuo;
|
||||||
|
duoComp.organizationId = this.organizationId;
|
||||||
duoComp.auth(result);
|
duoComp.auth(result);
|
||||||
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||||
this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo);
|
this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo);
|
||||||
|
|||||||
@@ -2,7 +2,17 @@ import { DOCUMENT } from "@angular/common";
|
|||||||
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { NavigationEnd, Router } from "@angular/router";
|
import { NavigationEnd, Router } from "@angular/router";
|
||||||
import * as jq from "jquery";
|
import * as jq from "jquery";
|
||||||
import { Subject, filter, firstValueFrom, map, switchMap, takeUntil, timeout, timer } from "rxjs";
|
import {
|
||||||
|
Subject,
|
||||||
|
combineLatest,
|
||||||
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
|
map,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
timeout,
|
||||||
|
timer,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
@@ -15,6 +25,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
@@ -241,8 +252,12 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
new SendOptionsPolicy(),
|
new SendOptionsPolicy(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.paymentMethodWarningsRefresh$
|
combineLatest([
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.ShowPaymentMethodWarningBanners),
|
||||||
|
this.paymentMethodWarningsRefresh$,
|
||||||
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
|
filter(([showPaymentMethodWarningBanners]) => showPaymentMethodWarningBanners),
|
||||||
switchMap(() => this.organizationService.memberOrganizations$),
|
switchMap(() => this.organizationService.memberOrganizations$),
|
||||||
switchMap(
|
switchMap(
|
||||||
async (organizations) =>
|
async (organizations) =>
|
||||||
|
|||||||
@@ -328,16 +328,4 @@ export class EmergencyAccessService {
|
|||||||
private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise<EncryptedString> {
|
private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise<EncryptedString> {
|
||||||
return (await this.cryptoService.rsaEncrypt(userKey.key, publicKey)).encryptedString;
|
return (await this.cryptoService.rsaEncrypt(userKey.key, publicKey)).encryptedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Nov 6, 2023: Use new Key Rotation Service for posting rotated data.
|
|
||||||
*/
|
|
||||||
async postLegacyRotation(requests: EmergencyAccessWithIdRequest[]): Promise<void> {
|
|
||||||
if (requests == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const request of requests) {
|
|
||||||
await this.emergencyAccessApiService.putEmergencyAccess(request.id, request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ describe("KeyRotationService", () => {
|
|||||||
mockEncryptService,
|
mockEncryptService,
|
||||||
mockStateService,
|
mockStateService,
|
||||||
mockAccountService,
|
mockAccountService,
|
||||||
mockConfigService,
|
|
||||||
mockKdfConfigService,
|
mockKdfConfigService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -191,16 +190,6 @@ describe("KeyRotationService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses legacy rotation if feature flag is off", async () => {
|
|
||||||
mockConfigService.getFeatureFlag.mockResolvedValueOnce(false);
|
|
||||||
|
|
||||||
await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword");
|
|
||||||
|
|
||||||
expect(mockApiService.postUserKeyUpdate).toHaveBeenCalled();
|
|
||||||
expect(mockEmergencyAccessService.postLegacyRotation).toHaveBeenCalled();
|
|
||||||
expect(mockResetPasswordService.postLegacyRotation).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if server rotation fails", async () => {
|
it("throws if server rotation fails", async () => {
|
||||||
mockApiService.postUserKeyUpdate.mockRejectedValueOnce(new Error("mockError"));
|
mockApiService.postUserKeyUpdate.mockRejectedValueOnce(new Error("mockError"));
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
|||||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
@@ -39,7 +37,6 @@ export class UserKeyRotationService {
|
|||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private configService: ConfigService,
|
|
||||||
private kdfConfigService: KdfConfigService,
|
private kdfConfigService: KdfConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -90,11 +87,7 @@ export class UserKeyRotationService {
|
|||||||
request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey);
|
request.emergencyAccessKeys = await this.emergencyAccessService.getRotatedKeys(newUserKey);
|
||||||
request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey);
|
request.resetPasswordKeys = await this.resetPasswordService.getRotatedKeys(newUserKey);
|
||||||
|
|
||||||
if (await this.configService.getFeatureFlag(FeatureFlag.KeyRotationImprovements)) {
|
await this.apiService.postUserKeyUpdate(request);
|
||||||
await this.apiService.postUserKeyUpdate(request);
|
|
||||||
} else {
|
|
||||||
await this.rotateUserKeyAndEncryptedDataLegacy(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
await this.deviceTrustService.rotateDevicesTrust(
|
await this.deviceTrustService.rotateDevicesTrust(
|
||||||
@@ -139,16 +132,4 @@ export class UserKeyRotationService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async rotateUserKeyAndEncryptedDataLegacy(request: UpdateKeyRequest): Promise<void> {
|
|
||||||
// Update keys, ciphers, folders, and sends
|
|
||||||
await this.apiService.postUserKeyUpdate(request);
|
|
||||||
|
|
||||||
// Update emergency access keys
|
|
||||||
await this.emergencyAccessService.postLegacyRotation(request.emergencyAccessKeys);
|
|
||||||
|
|
||||||
// Update account recovery keys
|
|
||||||
const userId = await this.stateService.getUserId();
|
|
||||||
await this.resetPasswordService.postLegacyRotation(userId, request.resetPasswordKeys);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,129 +1,143 @@
|
|||||||
<div *ngIf="selfHosted" class="page-header">
|
<bit-section>
|
||||||
<h1>{{ "subscription" | i18n }}</h1>
|
<h2 *ngIf="!selfHosted" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||||
</div>
|
<bit-callout
|
||||||
<div *ngIf="!selfHosted" class="tabbed-header">
|
type="info"
|
||||||
<h1>{{ "goPremium" | i18n }}</h1>
|
*ngIf="canAccessPremium$ | async"
|
||||||
</div>
|
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||||
<bit-callout
|
icon="bwi bwi-star-f"
|
||||||
type="info"
|
|
||||||
*ngIf="canAccessPremium$ | async"
|
|
||||||
title="{{ 'youHavePremiumAccess' | i18n }}"
|
|
||||||
icon="bwi bwi-star-f"
|
|
||||||
>
|
|
||||||
{{ "alreadyPremiumFromOrg" | i18n }}
|
|
||||||
</bit-callout>
|
|
||||||
<bit-callout type="success">
|
|
||||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
|
||||||
<ul class="bwi-ul">
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpStorage" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpEmergency" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpReports" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpTotp" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpSupport" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpFuture" | i18n }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p class="text-lg" [ngClass]="{ 'mb-0': !selfHosted }">
|
|
||||||
{{
|
|
||||||
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
|
||||||
}}
|
|
||||||
<a routerLink="/create-organization" [queryParams]="{ plan: 'families' }">{{
|
|
||||||
"bitwardenFamiliesPlan" | i18n
|
|
||||||
}}</a>
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
bitButton
|
|
||||||
href="{{ this.cloudWebVaultUrl }}/#/settings/subscription/premium"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
buttonType="secondary"
|
|
||||||
*ngIf="selfHosted"
|
|
||||||
>
|
>
|
||||||
{{ "purchasePremium" | i18n }}
|
{{ "alreadyPremiumFromOrg" | i18n }}
|
||||||
</a>
|
</bit-callout>
|
||||||
</bit-callout>
|
<bit-callout type="success">
|
||||||
<ng-container *ngIf="selfHosted">
|
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||||
<p>{{ "uploadLicenseFilePremium" | i18n }}</p>
|
<ul class="bwi-ul">
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<li>
|
||||||
<div class="form-group">
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
<label for="file">{{ "licenseFile" | i18n }}</label>
|
{{ "premiumSignUpStorage" | i18n }}
|
||||||
<input type="file" id="file" class="form-control-file" name="file" required />
|
</li>
|
||||||
<small class="form-text text-muted">{{
|
<li>
|
||||||
"licenseFileDesc" | i18n: "bitwarden_premium_license.json"
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
}}</small>
|
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
||||||
</div>
|
</li>
|
||||||
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpEmergency" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpReports" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpTotp" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpSupport" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpFuture" | i18n }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !selfHosted }">
|
||||||
|
{{
|
||||||
|
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
||||||
|
}}
|
||||||
|
<a
|
||||||
|
bitLink
|
||||||
|
linkType="primary"
|
||||||
|
routerLink="/create-organization"
|
||||||
|
[queryParams]="{ plan: 'families' }"
|
||||||
|
>{{ "bitwardenFamiliesPlan" | i18n }}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
bitButton
|
||||||
|
href="{{ this.cloudWebVaultUrl }}/#/settings/subscription/premium"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
buttonType="secondary"
|
||||||
|
*ngIf="selfHosted"
|
||||||
|
>
|
||||||
|
{{ "purchasePremium" | i18n }}
|
||||||
|
</a>
|
||||||
|
</bit-callout>
|
||||||
|
</bit-section>
|
||||||
|
<bit-section *ngIf="selfHosted">
|
||||||
|
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||||
|
<form [formGroup]="licenseForm" [bitSubmit]="submit">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||||
|
<div>
|
||||||
|
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||||
|
{{ "chooseFile" | i18n }}
|
||||||
|
</button>
|
||||||
|
{{ this.licenseFile ? this.licenseFile.name : ("noFileChosen" | i18n) }}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
#fileSelector
|
||||||
|
type="file"
|
||||||
|
formControlName="file"
|
||||||
|
(change)="setSelectedFile($event)"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||||
{{ "submit" | i18n }}
|
{{ "submit" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</ng-container>
|
</bit-section>
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
|
<form [formGroup]="addonForm" [bitSubmit]="submit" *ngIf="!selfHosted">
|
||||||
<h2 class="mt-5">{{ "addons" | i18n }}</h2>
|
<bit-section>
|
||||||
<div class="row">
|
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||||
<div class="form-group col-6">
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
<label for="additionalStorage">{{ "additionalStorageGb" | i18n }}</label>
|
<bit-form-field class="tw-col-span-6">
|
||||||
<input
|
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||||
id="additionalStorage"
|
<input
|
||||||
class="form-control"
|
bitInput
|
||||||
type="number"
|
formControlName="additionalStorage"
|
||||||
name="AdditionalStorageGb"
|
type="number"
|
||||||
[(ngModel)]="additionalStorage"
|
step="1"
|
||||||
min="0"
|
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||||
max="99"
|
/>
|
||||||
step="1"
|
<bit-hint>{{
|
||||||
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
"additionalStorageIntervalDesc"
|
||||||
/>
|
| i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n)
|
||||||
<small class="text-muted form-text">{{
|
}}</bit-hint>
|
||||||
"additionalStorageIntervalDesc"
|
</bit-form-field>
|
||||||
| i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n)
|
|
||||||
}}</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</bit-section>
|
||||||
<h2 class="spaced-header">{{ "summary" | i18n }}</h2>
|
<bit-section>
|
||||||
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||||
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB ×
|
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
||||||
{{ storageGbPrice | currency: "$" }} =
|
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB ×
|
||||||
{{ additionalStorageTotal | currency: "$" }}
|
{{ storageGbPrice | currency: "$" }} =
|
||||||
<hr class="my-3" />
|
{{ additionalStorageTotal | currency: "$" }}
|
||||||
<h2 class="spaced-header mb-4">{{ "paymentInformation" | i18n }}</h2>
|
<hr class="tw-my-3" />
|
||||||
<app-payment [hideBank]="true"></app-payment>
|
</bit-section>
|
||||||
<app-tax-info></app-tax-info>
|
<bit-section>
|
||||||
<div id="price" class="my-4">
|
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
||||||
<div class="text-muted text-sm">
|
<app-payment [hideBank]="true"></app-payment>
|
||||||
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}
|
<app-tax-info></app-tax-info>
|
||||||
<br />
|
<div id="price" class="tw-my-4">
|
||||||
<ng-container>
|
<div class="tw-text-muted tw-text-sm">
|
||||||
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
|
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}
|
||||||
</ng-container>
|
<br />
|
||||||
|
<ng-container>
|
||||||
|
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<hr class="tw-my-1 tw-w-1/4 tw-ml-0" />
|
||||||
|
<p bitTypography="body1">
|
||||||
|
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-1 col-3 ml-0" />
|
<p bitTypography="body2">{{ "paymentChargedAnnually" | i18n }}</p>
|
||||||
<p class="text-lg">
|
<button type="submit" bitButton bitFormButton>
|
||||||
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
|
{{ "submit" | i18n }}
|
||||||
</p>
|
</button>
|
||||||
</div>
|
</bit-section>
|
||||||
<small class="text-muted font-italic">{{ "paymentChargedAnnually" | i18n }}</small>
|
|
||||||
<button type="submit" bitButton [loading]="form.loading">
|
|
||||||
{{ "submit" | i18n }}
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component, OnInit, ViewChild } from "@angular/core";
|
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||||
|
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { firstValueFrom, Observable } from "rxjs";
|
import { firstValueFrom, Observable } from "rxjs";
|
||||||
|
|
||||||
@@ -7,7 +8,6 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
|
|||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
@@ -26,11 +26,16 @@ export class PremiumComponent implements OnInit {
|
|||||||
premiumPrice = 10;
|
premiumPrice = 10;
|
||||||
familyPlanMaxUserCount = 6;
|
familyPlanMaxUserCount = 6;
|
||||||
storageGbPrice = 4;
|
storageGbPrice = 4;
|
||||||
additionalStorage = 0;
|
|
||||||
cloudWebVaultUrl: string;
|
cloudWebVaultUrl: string;
|
||||||
|
licenseFile: File = null;
|
||||||
|
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
|
protected licenseForm = new FormGroup({
|
||||||
|
file: new FormControl(null, [Validators.required]),
|
||||||
|
});
|
||||||
|
protected addonForm = new FormGroup({
|
||||||
|
additionalStorage: new FormControl(0, [Validators.max(99), Validators.min(0)]),
|
||||||
|
});
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
@@ -39,14 +44,17 @@ export class PremiumComponent implements OnInit {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private logService: LogService,
|
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
) {
|
) {
|
||||||
this.selfHosted = platformUtilsService.isSelfHost();
|
this.selfHosted = platformUtilsService.isSelfHost();
|
||||||
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||||
}
|
}
|
||||||
|
protected setSelectedFile(event: Event) {
|
||||||
|
const fileInputEl = <HTMLInputElement>event.target;
|
||||||
|
const file: File = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
|
||||||
|
this.licenseFile = file;
|
||||||
|
}
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
||||||
if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) {
|
if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) {
|
||||||
@@ -56,13 +64,11 @@ export class PremiumComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
submit = async () => {
|
||||||
async submit() {
|
this.licenseForm.markAllAsTouched();
|
||||||
let files: FileList = null;
|
this.addonForm.markAllAsTouched();
|
||||||
if (this.selfHosted) {
|
if (this.selfHosted) {
|
||||||
const fileEl = document.getElementById("file") as HTMLInputElement;
|
if (this.licenseFile == null) {
|
||||||
files = fileEl.files;
|
|
||||||
if (files == null || files.length === 0) {
|
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
this.i18nService.t("errorOccurred"),
|
this.i18nService.t("errorOccurred"),
|
||||||
@@ -72,53 +78,48 @@ export class PremiumComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (this.selfHosted) {
|
||||||
if (this.selfHosted) {
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
if (!this.tokenService.getEmailVerified()) {
|
||||||
if (!this.tokenService.getEmailVerified()) {
|
this.platformUtilsService.showToast(
|
||||||
this.platformUtilsService.showToast(
|
"error",
|
||||||
"error",
|
this.i18nService.t("errorOccurred"),
|
||||||
this.i18nService.t("errorOccurred"),
|
this.i18nService.t("verifyEmailFirst"),
|
||||||
this.i18nService.t("verifyEmailFirst"),
|
);
|
||||||
);
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append("license", files[0]);
|
|
||||||
this.formPromise = this.apiService.postAccountLicense(fd).then(() => {
|
|
||||||
return this.finalizePremium();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.formPromise = this.paymentComponent
|
|
||||||
.createPaymentToken()
|
|
||||||
.then((result) => {
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append("paymentMethodType", result[1].toString());
|
|
||||||
if (result[0] != null) {
|
|
||||||
fd.append("paymentToken", result[0]);
|
|
||||||
}
|
|
||||||
fd.append("additionalStorageGb", (this.additionalStorage || 0).toString());
|
|
||||||
fd.append("country", this.taxInfoComponent.taxInfo.country);
|
|
||||||
fd.append("postalCode", this.taxInfoComponent.taxInfo.postalCode);
|
|
||||||
return this.apiService.postPremium(fd);
|
|
||||||
})
|
|
||||||
.then((paymentResponse) => {
|
|
||||||
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {
|
|
||||||
return this.paymentComponent.handleStripeCardPayment(
|
|
||||||
paymentResponse.paymentIntentClientSecret,
|
|
||||||
() => this.finalizePremium(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return this.finalizePremium();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
await this.formPromise;
|
|
||||||
} catch (e) {
|
const fd = new FormData();
|
||||||
this.logService.error(e);
|
fd.append("license", this.licenseFile);
|
||||||
|
await this.apiService.postAccountLicense(fd).then(() => {
|
||||||
|
return this.finalizePremium();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.paymentComponent
|
||||||
|
.createPaymentToken()
|
||||||
|
.then((result) => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("paymentMethodType", result[1].toString());
|
||||||
|
if (result[0] != null) {
|
||||||
|
fd.append("paymentToken", result[0]);
|
||||||
|
}
|
||||||
|
fd.append("additionalStorageGb", (this.additionalStorage || 0).toString());
|
||||||
|
fd.append("country", this.taxInfoComponent.taxInfo.country);
|
||||||
|
fd.append("postalCode", this.taxInfoComponent.taxInfo.postalCode);
|
||||||
|
return this.apiService.postPremium(fd);
|
||||||
|
})
|
||||||
|
.then((paymentResponse) => {
|
||||||
|
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {
|
||||||
|
return this.paymentComponent.handleStripeCardPayment(
|
||||||
|
paymentResponse.paymentIntentClientSecret,
|
||||||
|
() => this.finalizePremium(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.finalizePremium();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async finalizePremium() {
|
async finalizePremium() {
|
||||||
await this.apiService.refreshIdentityToken();
|
await this.apiService.refreshIdentityToken();
|
||||||
@@ -127,6 +128,9 @@ export class PremiumComponent implements OnInit {
|
|||||||
await this.router.navigate(["/settings/subscription/user-subscription"]);
|
await this.router.navigate(["/settings/subscription/user-subscription"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get additionalStorage(): number {
|
||||||
|
return this.addonForm.get("additionalStorage").value;
|
||||||
|
}
|
||||||
get additionalStorageTotal(): number {
|
get additionalStorageTotal(): number {
|
||||||
return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
|
return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,57 @@
|
|||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<form [formGroup]="adjustSubscriptionForm" [bitSubmit]="submit">
|
||||||
<div>
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
<div class="row">
|
<div class="tw-col-span-8">
|
||||||
<div class="form-group col-8">
|
<bit-form-field>
|
||||||
<label for="newSeatCount">{{ "subscriptionSeats" | i18n }}</label>
|
<bit-label>{{ "subscriptionSeats" | i18n }}</bit-label>
|
||||||
<input
|
<input bitInput formControlName="newSeatCount" type="number" min="0" step="1" />
|
||||||
id="newSeatCount"
|
<bit-hint>
|
||||||
class="form-control"
|
|
||||||
type="number"
|
|
||||||
name="NewSeatCount"
|
|
||||||
[(ngModel)]="newSeatCount"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<small class="d-block text-muted mb-4">
|
|
||||||
<strong>{{ "total" | i18n }}:</strong> {{ additionalSeatCount || 0 }} ×
|
<strong>{{ "total" | i18n }}:</strong> {{ additionalSeatCount || 0 }} ×
|
||||||
{{ seatPrice | currency: "$" }} = {{ adjustedSeatTotal | currency: "$" }} /
|
{{ seatPrice | currency: "$" }} = {{ adjustedSeatTotal | currency: "$" }} /
|
||||||
{{ interval | i18n }}
|
{{ interval | i18n }}</bit-hint
|
||||||
</small>
|
>
|
||||||
</div>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-4">
|
</div>
|
||||||
<div class="form-group col-sm">
|
<div>
|
||||||
<div class="form-check">
|
<bit-form-control>
|
||||||
<input
|
<input
|
||||||
id="limitSubscription"
|
bitCheckbox
|
||||||
class="form-check-input"
|
formControlName="limitSubscription"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="LimitSubscription"
|
(change)="limitSubscriptionChanged()"
|
||||||
[(ngModel)]="limitSubscription"
|
/>
|
||||||
(change)="limitSubscriptionChanged()"
|
<bit-label>{{ "limitSubscription" | i18n }}</bit-label>
|
||||||
/>
|
<bit-hint> {{ "limitSubscriptionDesc" | i18n }}</bit-hint>
|
||||||
<label for="limitSubscription">{{ "limitSubscription" | i18n }}</label>
|
</bit-form-control>
|
||||||
</div>
|
</div>
|
||||||
<small class="d-block text-muted">{{ "limitSubscriptionDesc" | i18n }}</small>
|
<div
|
||||||
</div>
|
class="tw-grid tw-grid-cols-12 tw-gap-4 tw-mb-4"
|
||||||
</div>
|
[hidden]="!adjustSubscriptionForm.value.limitSubscription"
|
||||||
<div class="row mb-4" [hidden]="!limitSubscription">
|
>
|
||||||
<div class="form-group col-sm">
|
<div class="tw-col-span-8">
|
||||||
<label for="maxAutoscaleSeats">{{ "maxSeatLimit" | i18n }}</label>
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "maxSeatLimit" | i18n }}</bit-label>
|
||||||
<input
|
<input
|
||||||
id="maxAutoscaleSeats"
|
bitInput
|
||||||
class="form-control col-8"
|
formControlName="newMaxSeats"
|
||||||
type="number"
|
type="number"
|
||||||
name="MaxAutoscaleSeats"
|
[min]="
|
||||||
[(ngModel)]="newMaxSeats"
|
adjustSubscriptionForm.value.newSeatCount == null
|
||||||
[min]="newSeatCount == null ? 1 : newSeatCount"
|
? 1
|
||||||
|
: adjustSubscriptionForm.value.newSeatCount
|
||||||
|
"
|
||||||
step="1"
|
step="1"
|
||||||
[required]="limitSubscription"
|
|
||||||
/>
|
/>
|
||||||
<small class="d-block text-muted">
|
<bit-hint>
|
||||||
<strong>{{ "maxSeatCost" | i18n }}:</strong> {{ additionalMaxSeatCount || 0 }} ×
|
<strong>{{ "maxSeatCost" | i18n }}:</strong> {{ additionalMaxSeatCount || 0 }} ×
|
||||||
{{ seatPrice | currency: "$" }} = {{ maxSeatTotal | currency: "$" }} /
|
{{ seatPrice | currency: "$" }} = {{ maxSeatTotal | currency: "$" }} /
|
||||||
{{ interval | i18n }}
|
{{ interval | i18n }}</bit-hint
|
||||||
</small>
|
>
|
||||||
</div>
|
</bit-form-field>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button bitButton buttonType="primary" bitFormButton type="submit">
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<app-payment [showMethods]="false"></app-payment>
|
<app-payment [showMethods]="false"></app-payment>
|
||||||
|
|||||||
@@ -1,77 +1,102 @@
|
|||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request";
|
import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-adjust-subscription",
|
selector: "app-adjust-subscription",
|
||||||
templateUrl: "adjust-subscription.component.html",
|
templateUrl: "adjust-subscription.component.html",
|
||||||
})
|
})
|
||||||
export class AdjustSubscription {
|
export class AdjustSubscription implements OnInit, OnDestroy {
|
||||||
@Input() organizationId: string;
|
@Input() organizationId: string;
|
||||||
@Input() maxAutoscaleSeats: number;
|
@Input() maxAutoscaleSeats: number;
|
||||||
@Input() currentSeatCount: number;
|
@Input() currentSeatCount: number;
|
||||||
@Input() seatPrice = 0;
|
@Input() seatPrice = 0;
|
||||||
@Input() interval = "year";
|
@Input() interval = "year";
|
||||||
@Output() onAdjusted = new EventEmitter();
|
@Output() onAdjusted = new EventEmitter();
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
formPromise: Promise<void>;
|
adjustSubscriptionForm = this.formBuilder.group({
|
||||||
limitSubscription: boolean;
|
newSeatCount: [0, [Validators.min(0)]],
|
||||||
newSeatCount: number;
|
limitSubscription: [false],
|
||||||
newMaxSeats: number;
|
newMaxSeats: [0, [Validators.min(0)]],
|
||||||
|
});
|
||||||
|
get limitSubscription(): boolean {
|
||||||
|
return this.adjustSubscriptionForm.value.limitSubscription;
|
||||||
|
}
|
||||||
constructor(
|
constructor(
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private logService: LogService,
|
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.limitSubscription = this.maxAutoscaleSeats != null;
|
this.adjustSubscriptionForm.patchValue({
|
||||||
this.newSeatCount = this.currentSeatCount;
|
newSeatCount: this.currentSeatCount,
|
||||||
this.newMaxSeats = this.maxAutoscaleSeats;
|
limitSubscription: this.maxAutoscaleSeats != null,
|
||||||
|
newMaxSeats: this.maxAutoscaleSeats,
|
||||||
|
});
|
||||||
|
this.adjustSubscriptionForm
|
||||||
|
.get("limitSubscription")
|
||||||
|
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
this.adjustSubscriptionForm
|
||||||
|
.get("newMaxSeats")
|
||||||
|
.addValidators([
|
||||||
|
Validators.min(
|
||||||
|
this.adjustSubscriptionForm.value.newSeatCount == null
|
||||||
|
? 1
|
||||||
|
: this.adjustSubscriptionForm.value.newSeatCount,
|
||||||
|
),
|
||||||
|
Validators.required,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
this.adjustSubscriptionForm.get("newMaxSeats").updateValueAndValidity();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
ngOnDestroy() {
|
||||||
try {
|
this.destroy$.next();
|
||||||
const request = new OrganizationSubscriptionUpdateRequest(
|
this.destroy$.complete();
|
||||||
this.additionalSeatCount,
|
}
|
||||||
this.newMaxSeats,
|
submit = async () => {
|
||||||
);
|
this.adjustSubscriptionForm.markAllAsTouched();
|
||||||
this.formPromise = this.organizationApiService.updatePasswordManagerSeats(
|
if (this.adjustSubscriptionForm.invalid) {
|
||||||
this.organizationId,
|
return;
|
||||||
request,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.formPromise;
|
|
||||||
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("subscriptionUpdated"),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
}
|
||||||
|
const request = new OrganizationSubscriptionUpdateRequest(
|
||||||
|
this.additionalSeatCount,
|
||||||
|
this.adjustSubscriptionForm.value.newMaxSeats,
|
||||||
|
);
|
||||||
|
await this.organizationApiService.updatePasswordManagerSeats(this.organizationId, request);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated"));
|
||||||
|
|
||||||
this.onAdjusted.emit();
|
this.onAdjusted.emit();
|
||||||
}
|
};
|
||||||
|
|
||||||
limitSubscriptionChanged() {
|
limitSubscriptionChanged() {
|
||||||
if (!this.limitSubscription) {
|
if (!this.adjustSubscriptionForm.value.limitSubscription) {
|
||||||
this.newMaxSeats = null;
|
this.adjustSubscriptionForm.value.newMaxSeats = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get additionalSeatCount(): number {
|
get additionalSeatCount(): number {
|
||||||
return this.newSeatCount ? this.newSeatCount - this.currentSeatCount : 0;
|
return this.adjustSubscriptionForm.value.newSeatCount
|
||||||
|
? this.adjustSubscriptionForm.value.newSeatCount - this.currentSeatCount
|
||||||
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get additionalMaxSeatCount(): number {
|
get additionalMaxSeatCount(): number {
|
||||||
return this.newMaxSeats ? this.newMaxSeats - this.currentSeatCount : 0;
|
return this.adjustSubscriptionForm.value.newMaxSeats
|
||||||
|
? this.adjustSubscriptionForm.value.newMaxSeats - this.currentSeatCount
|
||||||
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get adjustedSeatTotal(): number {
|
get adjustedSeatTotal(): number {
|
||||||
|
|||||||
@@ -1,400 +1,428 @@
|
|||||||
<ng-container *ngIf="loading">
|
<ng-container *ngIf="loading">
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||||
title="{{ 'loading' | i18n }}"
|
title="{{ 'loading' | i18n }}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="createOrganization && selfHosted">
|
<ng-container *ngIf="createOrganization && selfHosted">
|
||||||
<p>{{ "uploadLicenseFileOrg" | i18n }}</p>
|
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
|
||||||
<div class="form-group">
|
<bit-form-field>
|
||||||
<label for="file">{{ "licenseFile" | i18n }}</label>
|
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||||
<input type="file" id="file" class="form-control-file" name="file" required />
|
<div>
|
||||||
<small class="form-text text-muted">{{
|
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||||
"licenseFileDesc" | i18n: "bitwarden_organization_license.json"
|
{{ "chooseFile" | i18n }}
|
||||||
}}</small>
|
</button>
|
||||||
</div>
|
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
</div>
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
<input
|
||||||
<span>{{ "submit" | i18n }}</span>
|
#fileSelector
|
||||||
|
hidden
|
||||||
|
bitInput
|
||||||
|
type="file"
|
||||||
|
formControlName="file"
|
||||||
|
(change)="setSelectedFile($event)"
|
||||||
|
accept="application/JSON"
|
||||||
|
/>
|
||||||
|
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||||
|
{{ "submit" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<form
|
<form
|
||||||
#form
|
|
||||||
[formGroup]="formGroup"
|
[formGroup]="formGroup"
|
||||||
(ngSubmit)="submit()"
|
[bitSubmit]="submit"
|
||||||
[appApiAction]="formPromise"
|
|
||||||
ngNativeValidate
|
|
||||||
*ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans"
|
*ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans"
|
||||||
class="tw-pt-6"
|
class="tw-pt-6"
|
||||||
>
|
>
|
||||||
<app-org-info
|
<bit-section>
|
||||||
(changedBusinessOwned)="changedOwnedBusiness()"
|
<app-org-info
|
||||||
[formGroup]="formGroup"
|
(changedBusinessOwned)="changedOwnedBusiness()"
|
||||||
[createOrganization]="createOrganization"
|
[formGroup]="formGroup"
|
||||||
[isProvider]="!!providerId"
|
[createOrganization]="createOrganization"
|
||||||
[acceptingSponsorship]="acceptingSponsorship"
|
[isProvider]="!!providerId"
|
||||||
></app-org-info>
|
[acceptingSponsorship]="acceptingSponsorship"
|
||||||
<h2 class="mt-5">{{ "chooseYourPlan" | i18n }}</h2>
|
>
|
||||||
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block">
|
</app-org-info>
|
||||||
<input
|
</bit-section>
|
||||||
class="form-check-input"
|
<bit-section>
|
||||||
type="radio"
|
<h2 bitTypography="h2">{{ "chooseYourPlan" | i18n }}</h2>
|
||||||
name="product"
|
<div *ngFor="let selectableProduct of selectableProducts">
|
||||||
id="product{{ selectableProduct.product }}"
|
<bit-radio-group formControlName="product" [block]="true">
|
||||||
[value]="selectableProduct.product"
|
<bit-radio-button [value]="selectableProduct.product" (change)="changedProduct()">
|
||||||
formControlName="product"
|
<bit-label>{{ selectableProduct.nameLocalizationKey | i18n }}</bit-label>
|
||||||
(change)="changedProduct()"
|
<bit-hint class="tw-text-sm"
|
||||||
/>
|
>{{ selectableProduct.descriptionLocalizationKey | i18n: "1" }}
|
||||||
<label class="form-check-label" for="product{{ selectableProduct.product }}">
|
<ng-container
|
||||||
{{ selectableProduct.nameLocalizationKey | i18n }}
|
*ngIf="selectableProduct.product === productTypes.Enterprise; else nonEnterprisePlans"
|
||||||
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n: "1" }}</small>
|
>
|
||||||
<ng-container
|
<ul class="tw-pl-0 tw-list-inside tw-mb-0">
|
||||||
*ngIf="selectableProduct.product === productTypes.Enterprise; else nonEnterprisePlans"
|
<li>{{ "includeAllTeamsFeatures" | i18n }}</li>
|
||||||
>
|
<li *ngIf="selectableProduct.hasSelfHost">{{ "onPremHostingOptional" | i18n }}</li>
|
||||||
<small>• {{ "includeAllTeamsFeatures" | i18n }}</small>
|
<li *ngIf="selectableProduct.hasSso">{{ "includeSsoAuthentication" | i18n }}</li>
|
||||||
<small *ngIf="selectableProduct.hasSelfHost">• {{ "onPremHostingOptional" | i18n }}</small>
|
<li *ngIf="selectableProduct.hasPolicies">
|
||||||
<small *ngIf="selectableProduct.hasSso">• {{ "includeSsoAuthentication" | i18n }}</small>
|
{{ "includeEnterprisePolicies" | i18n }}
|
||||||
<small *ngIf="selectableProduct.hasPolicies"
|
</li>
|
||||||
>• {{ "includeEnterprisePolicies" | i18n }}</small
|
<li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
||||||
>
|
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
||||||
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization"
|
</li>
|
||||||
>•
|
</ul>
|
||||||
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
</ng-container>
|
||||||
</small>
|
<ng-template #nonEnterprisePlans>
|
||||||
</ng-container>
|
<ng-container
|
||||||
<ng-template #nonEnterprisePlans>
|
*ngIf="selectableProduct.product === productTypes.Teams; else fullFeatureList"
|
||||||
<ng-container
|
>
|
||||||
*ngIf="selectableProduct.product === productTypes.Teams; else fullFeatureList"
|
<ul class="tw-pl-0 tw-list-inside tw-mb-0">
|
||||||
>
|
<li>{{ "includeAllTeamsStarterFeatures" | i18n }}</li>
|
||||||
<small>• {{ "includeAllTeamsStarterFeatures" | i18n }}</small>
|
<li>{{ "chooseMonthlyOrAnnualBilling" | i18n }}</li>
|
||||||
<small>• {{ "chooseMonthlyOrAnnualBilling" | i18n }}</small>
|
<li>{{ "abilityToAddMoreThanNMembers" | i18n: 10 }}</li>
|
||||||
<small>• {{ "abilityToAddMoreThanNMembers" | i18n: 10 }}</small>
|
<li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
||||||
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
||||||
• {{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
</li>
|
||||||
</small>
|
</ul>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #fullFeatureList>
|
<ng-template #fullFeatureList>
|
||||||
<small *ngIf="selectableProduct.product == productTypes.Free"
|
<ul class="tw-pl-0 tw-list-inside tw-mb-0">
|
||||||
>• {{ "limitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}</small
|
<li *ngIf="selectableProduct.product == productTypes.Free">
|
||||||
|
{{ "limitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
*ngIf="
|
||||||
|
selectableProduct.product != productTypes.Free &&
|
||||||
|
selectableProduct.product != productTypes.TeamsStarter &&
|
||||||
|
selectableProduct.PasswordManager.maxSeats
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ "addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="!selectableProduct.PasswordManager.maxSeats">
|
||||||
|
{{ "addShareUnlimitedUsers" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.PasswordManager.maxCollections">
|
||||||
|
{{
|
||||||
|
"limitedCollections" | i18n: selectableProduct.PasswordManager.maxCollections
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.PasswordManager.maxAdditionalSeats">
|
||||||
|
{{
|
||||||
|
"addShareLimitedUsers"
|
||||||
|
| i18n: selectableProduct.PasswordManager.maxAdditionalSeats
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="!selectableProduct.PasswordManager.maxCollections">
|
||||||
|
{{ "createUnlimitedCollections" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.PasswordManager.baseStorageGb">
|
||||||
|
{{
|
||||||
|
"gbEncryptedFileStorage"
|
||||||
|
| i18n: selectableProduct.PasswordManager.baseStorageGb + "GB"
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.hasGroups">
|
||||||
|
{{ "controlAccessWithGroups" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.hasApi">{{ "trackAuditLogs" | i18n }}</li>
|
||||||
|
<li *ngIf="selectableProduct.hasDirectory">
|
||||||
|
{{ "syncUsersFromDirectory" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.hasSelfHost">
|
||||||
|
{{ "onPremHostingOptional" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.usersGetPremium">{{ "usersGetPremium" | i18n }}</li>
|
||||||
|
<li *ngIf="selectableProduct.product != productTypes.Free">
|
||||||
|
{{ "priorityCustomerSupport" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
||||||
|
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
|
</bit-hint>
|
||||||
|
</bit-radio-button>
|
||||||
|
<span *ngIf="selectableProduct.product != productTypes.Free">
|
||||||
|
<ng-container
|
||||||
|
*ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship"
|
||||||
>
|
>
|
||||||
<small
|
|
||||||
*ngIf="
|
|
||||||
selectableProduct.product != productTypes.Free &&
|
|
||||||
selectableProduct.product != productTypes.TeamsStarter &&
|
|
||||||
selectableProduct.PasswordManager.maxSeats
|
|
||||||
"
|
|
||||||
>•
|
|
||||||
{{ "addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="!selectableProduct.PasswordManager.maxSeats"
|
|
||||||
>• {{ "addShareUnlimitedUsers" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.PasswordManager.maxCollections"
|
|
||||||
>•
|
|
||||||
{{
|
|
||||||
"limitedCollections" | i18n: selectableProduct.PasswordManager.maxCollections
|
|
||||||
}}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.PasswordManager.maxAdditionalSeats"
|
|
||||||
>•
|
|
||||||
{{
|
|
||||||
"addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxAdditionalSeats
|
|
||||||
}}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="!selectableProduct.PasswordManager.maxCollections"
|
|
||||||
>• {{ "createUnlimitedCollections" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.PasswordManager.baseStorageGb"
|
|
||||||
>•
|
|
||||||
{{
|
|
||||||
"gbEncryptedFileStorage"
|
|
||||||
| i18n: selectableProduct.PasswordManager.baseStorageGb + "GB"
|
|
||||||
}}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.hasGroups"
|
|
||||||
>• {{ "controlAccessWithGroups" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.hasApi">• {{ "trackAuditLogs" | i18n }}</small>
|
|
||||||
<small *ngIf="selectableProduct.hasDirectory"
|
|
||||||
>• {{ "syncUsersFromDirectory" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.hasSelfHost"
|
|
||||||
>• {{ "onPremHostingOptional" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.usersGetPremium">• {{ "usersGetPremium" | i18n }}</small>
|
|
||||||
<small *ngIf="selectableProduct.product != productTypes.Free"
|
|
||||||
>• {{ "priorityCustomerSupport" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization"
|
|
||||||
>•
|
|
||||||
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
|
||||||
</small>
|
|
||||||
</ng-template>
|
|
||||||
</ng-template>
|
|
||||||
<span *ngIf="selectableProduct.product != productTypes.Free">
|
|
||||||
<ng-container *ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship">
|
|
||||||
{{
|
|
||||||
(selectableProduct.isAnnual
|
|
||||||
? selectableProduct.PasswordManager.basePrice / 12
|
|
||||||
: selectableProduct.PasswordManager.basePrice
|
|
||||||
) | currency: "$"
|
|
||||||
}}
|
|
||||||
/{{ "month" | i18n }},
|
|
||||||
{{ "includesXUsers" | i18n: selectableProduct.PasswordManager.baseSeats }}
|
|
||||||
<ng-container *ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption">
|
|
||||||
{{ ("additionalUsers" | i18n).toLowerCase() }}
|
|
||||||
{{
|
{{
|
||||||
(selectableProduct.isAnnual
|
(selectableProduct.isAnnual
|
||||||
? selectableProduct.PasswordManager.seatPrice / 12
|
? selectableProduct.PasswordManager.basePrice / 12
|
||||||
: selectableProduct.PasswordManager.seatPrice
|
: selectableProduct.PasswordManager.basePrice
|
||||||
) | currency: "$"
|
) | currency: "$"
|
||||||
}}
|
}}
|
||||||
/{{ "month" | i18n }}
|
/{{ "month" | i18n }},
|
||||||
</ng-container>
|
{{ "includesXUsers" | i18n: selectableProduct.PasswordManager.baseSeats }}
|
||||||
</ng-container>
|
<ng-container *ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption">
|
||||||
</span>
|
{{ ("additionalUsers" | i18n).toLowerCase() }}
|
||||||
<span
|
{{
|
||||||
*ngIf="
|
(selectableProduct.isAnnual
|
||||||
!selectableProduct.PasswordManager.basePrice &&
|
|
||||||
selectableProduct.PasswordManager.hasAdditionalSeatsOption
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
"costPerUser"
|
|
||||||
| i18n
|
|
||||||
: ((selectableProduct.isAnnual
|
|
||||||
? selectableProduct.PasswordManager.seatPrice / 12
|
? selectableProduct.PasswordManager.seatPrice / 12
|
||||||
: selectableProduct.PasswordManager.seatPrice
|
: selectableProduct.PasswordManager.seatPrice
|
||||||
)
|
) | currency: "$"
|
||||||
| currency: "$")
|
}}
|
||||||
}}
|
/{{ "month" | i18n }}
|
||||||
/{{ "month" | i18n }}
|
</ng-container>
|
||||||
</span>
|
</ng-container>
|
||||||
<span *ngIf="selectableProduct.product == productTypes.Free">{{ "freeForever" | i18n }}</span>
|
</span>
|
||||||
</label>
|
<span
|
||||||
</div>
|
*ngIf="
|
||||||
<div *ngIf="formGroup.value.product !== productTypes.Free">
|
!selectableProduct.PasswordManager.basePrice &&
|
||||||
<ng-container
|
selectableProduct.PasswordManager.hasAdditionalSeatsOption
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
"costPerUser"
|
||||||
|
| i18n
|
||||||
|
: ((selectableProduct.isAnnual
|
||||||
|
? selectableProduct.PasswordManager.seatPrice / 12
|
||||||
|
: selectableProduct.PasswordManager.seatPrice
|
||||||
|
)
|
||||||
|
| currency: "$")
|
||||||
|
}}
|
||||||
|
/{{ "month" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="selectableProduct.product == productTypes.Free">{{
|
||||||
|
"freeForever" | i18n
|
||||||
|
}}</span>
|
||||||
|
</bit-radio-group>
|
||||||
|
</div>
|
||||||
|
</bit-section>
|
||||||
|
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
|
||||||
|
<bit-section
|
||||||
*ngIf="
|
*ngIf="
|
||||||
selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
|
selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
|
||||||
!selectedPlan.PasswordManager.baseSeats
|
!selectedPlan.PasswordManager.baseSeats
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<h2 class="mt-5">{{ "users" | i18n }}</h2>
|
<h2 bitTypography="h2">{{ "users" | i18n }}</h2>
|
||||||
<div class="row">
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
<div class="col-6">
|
<bit-form-field class="tw-col-span-6">
|
||||||
<label for="additionalSeats">{{ "userSeats" | i18n }}</label>
|
<bit-label>{{ "userSeats" | i18n }}</bit-label>
|
||||||
<input
|
<input
|
||||||
id="additionalSeats"
|
bitInput
|
||||||
class="form-control"
|
|
||||||
type="number"
|
type="number"
|
||||||
name="additionalSeats"
|
|
||||||
formControlName="additionalSeats"
|
formControlName="additionalSeats"
|
||||||
placeholder="{{ 'userSeatsDesc' | i18n }}"
|
placeholder="{{ 'userSeatsDesc' | i18n }}"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "userSeatsHowManyDesc" | i18n }}</small>
|
<bit-hint class="tw-text-sm">{{ "userSeatsHowManyDesc" | i18n }}</bit-hint>
|
||||||
</div>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</bit-section>
|
||||||
<h2 class="mt-5">{{ "addons" | i18n }}</h2>
|
<bit-section>
|
||||||
<div
|
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||||
class="row"
|
<div
|
||||||
*ngIf="
|
class="tw-grid tw-grid-cols-12 tw-gap-4"
|
||||||
selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
|
*ngIf="
|
||||||
selectedPlan.PasswordManager.baseSeats
|
selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
|
||||||
"
|
selectedPlan.PasswordManager.baseSeats
|
||||||
>
|
"
|
||||||
<div class="form-group col-6">
|
>
|
||||||
<label for="additionalSeats">{{ "additionalUserSeats" | i18n }}</label>
|
<bit-form-field class="tw-col-span-6">
|
||||||
<input
|
<bit-label>{{ "additionalUserSeats" | i18n }}</bit-label>
|
||||||
id="additionalSeats"
|
|
||||||
class="form-control"
|
|
||||||
type="number"
|
|
||||||
name="additionalSeats"
|
|
||||||
formControlName="additionalSeats"
|
|
||||||
placeholder="{{ 'userSeatsDesc' | i18n }}"
|
|
||||||
/>
|
|
||||||
<small class="text-muted form-text">{{
|
|
||||||
"userSeatsAdditionalDesc"
|
|
||||||
| i18n
|
|
||||||
: selectedPlan.PasswordManager.baseSeats
|
|
||||||
: (seatPriceMonthly(selectedPlan) | currency: "$")
|
|
||||||
}}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="form-group col-6">
|
|
||||||
<label for="additionalStorage">{{ "additionalStorageGb" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="additionalStorage"
|
|
||||||
class="form-control"
|
|
||||||
type="number"
|
|
||||||
name="additionalStorageGb"
|
|
||||||
formControlName="additionalStorage"
|
|
||||||
step="1"
|
|
||||||
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
|
||||||
/>
|
|
||||||
<small class="text-muted form-text">{{
|
|
||||||
"additionalStorageIntervalDesc"
|
|
||||||
| i18n
|
|
||||||
: "1 GB"
|
|
||||||
: (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
|
|
||||||
: ("month" | i18n)
|
|
||||||
}}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="form-group col-6" *ngIf="selectedPlan.PasswordManager.hasPremiumAccessOption">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
<input
|
||||||
id="premiumAccess"
|
bitInput
|
||||||
class="form-check-input"
|
type="number"
|
||||||
type="checkbox"
|
formControlName="additionalSeats"
|
||||||
name="premiumAccessAddon"
|
placeholder="{{ 'userSeatsDesc' | i18n }}"
|
||||||
formControlName="premiumAccessAddon"
|
|
||||||
/>
|
/>
|
||||||
<label for="premiumAccess" class="form-check-label bold">{{
|
<bit-hint class="tx-text-sm"
|
||||||
"premiumAccess" | i18n
|
>{{
|
||||||
}}</label>
|
"userSeatsAdditionalDesc"
|
||||||
</div>
|
| i18n
|
||||||
<small class="text-muted form-text">{{
|
: selectedPlan.PasswordManager.baseSeats
|
||||||
"premiumAccessDesc" | i18n: (3.33 | currency: "$") : ("month" | i18n)
|
: (seatPriceMonthly(selectedPlan) | currency: "$")
|
||||||
}}</small>
|
}}
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
<h2 class="spaced-header">{{ "summary" | i18n }}</h2>
|
<bit-form-field class="tw-col-span-6">
|
||||||
<div class="form-check form-check-block" *ngFor="let selectablePlan of selectablePlans">
|
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
bitInput
|
||||||
type="radio"
|
type="number"
|
||||||
name="plan"
|
formControlName="additionalStorage"
|
||||||
id="interval{{ selectablePlan.type }}"
|
step="1"
|
||||||
[value]="selectablePlan.type"
|
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||||
formControlName="plan"
|
/>
|
||||||
/>
|
<bit-hint class="tw-text-sm">{{
|
||||||
<label class="form-check-label" for="interval{{ selectablePlan.type }}">
|
"additionalStorageIntervalDesc"
|
||||||
<ng-container *ngIf="selectablePlan.isAnnual">
|
| i18n
|
||||||
{{ "annually" | i18n }}
|
: "1 GB"
|
||||||
<small *ngIf="selectablePlan.PasswordManager.basePrice">
|
: (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
|
||||||
{{ "basePrice" | i18n }}:
|
: ("month" | i18n)
|
||||||
{{
|
}}</bit-hint>
|
||||||
(selectablePlan.isAnnual
|
</bit-form-field>
|
||||||
? selectablePlan.PasswordManager.basePrice / 12
|
</div>
|
||||||
: selectablePlan.PasswordManager.basePrice
|
</bit-section>
|
||||||
) | currency: "$"
|
<bit-section>
|
||||||
}}
|
<div
|
||||||
× 12
|
class="tw-grid tw-grid-cols-12 tw-gap-4"
|
||||||
{{ "monthAbbr" | i18n }}
|
*ngIf="selectedPlan.PasswordManager.hasPremiumAccessOption"
|
||||||
=
|
>
|
||||||
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
<bit-form-control class="tw-col-span-6">
|
||||||
<span style="text-decoration: line-through">{{
|
<bit-label>{{ "premiumAccess" | i18n }}</bit-label>
|
||||||
selectablePlan.PasswordManager.basePrice | currency: "$"
|
<input type="checkbox" bitCheckbox formControlName="premiumAccessAddon" />
|
||||||
}}</span>
|
<bit-hint class="tw-text-sm">{{
|
||||||
{{ "freeWithSponsorship" | i18n }}
|
"premiumAccessDesc" | i18n: (3.33 | currency: "$") : ("month" | i18n)
|
||||||
</ng-container>
|
}}</bit-hint>
|
||||||
<ng-template #notAcceptingSponsorship>
|
</bit-form-control>
|
||||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
</div>
|
||||||
|
</bit-section>
|
||||||
|
<bit-section *ngFor="let selectablePlan of selectablePlans">
|
||||||
|
<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: "$" }}
|
||||||
|
/{{ "year" | i18n }}
|
||||||
|
</ng-template>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="tw-mb-0"
|
||||||
|
bitTypography="body2"
|
||||||
|
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||||
|
>
|
||||||
|
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||||
|
>{{ "additionalUsers" | i18n }}:</span
|
||||||
|
>
|
||||||
|
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||||
|
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||||
|
{{
|
||||||
|
(selectablePlan.isAnnual
|
||||||
|
? selectablePlan.PasswordManager.seatPrice / 12
|
||||||
|
: selectablePlan.PasswordManager.seatPrice
|
||||||
|
) | currency: "$"
|
||||||
|
}}
|
||||||
|
× 12 {{ "monthAbbr" | i18n }} =
|
||||||
|
{{
|
||||||
|
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||||
|
| currency: "$"
|
||||||
|
}}
|
||||||
/{{ "year" | i18n }}
|
/{{ "year" | i18n }}
|
||||||
</ng-template>
|
</p>
|
||||||
</small>
|
<p
|
||||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption">
|
class="tw-mb-0"
|
||||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
bitTypography="body2"
|
||||||
>{{ "additionalUsers" | i18n }}:</span
|
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||||
>
|
>
|
||||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
{{ "additionalStorageGb" | i18n }}:
|
||||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||||
{{
|
{{
|
||||||
(selectablePlan.isAnnual
|
(selectablePlan.isAnnual
|
||||||
? selectablePlan.PasswordManager.seatPrice / 12
|
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
|
||||||
: selectablePlan.PasswordManager.seatPrice
|
: selectablePlan.PasswordManager.additionalStoragePricePerGb
|
||||||
) | currency: "$"
|
) | currency: "$"
|
||||||
}}
|
}}
|
||||||
× 12 {{ "monthAbbr" | i18n }} =
|
× 12 {{ "monthAbbr" | i18n }} =
|
||||||
{{
|
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
|
||||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
</p>
|
||||||
| currency: "$"
|
</bit-hint>
|
||||||
}}
|
<bit-hint *ngIf="!selectablePlan.isAnnual">
|
||||||
/{{ "year" | i18n }}
|
<p
|
||||||
</small>
|
class="tw-mb-0"
|
||||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption">
|
bitTypography="body2"
|
||||||
{{ "additionalStorageGb" | i18n }}:
|
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
|
||||||
{{
|
|
||||||
(selectablePlan.isAnnual
|
|
||||||
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
|
|
||||||
: selectablePlan.PasswordManager.additionalStoragePricePerGb
|
|
||||||
) | currency: "$"
|
|
||||||
}}
|
|
||||||
× 12 {{ "monthAbbr" | i18n }} =
|
|
||||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
|
|
||||||
</small>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="!selectablePlan.isAnnual">
|
|
||||||
{{ "monthly" | i18n }}
|
|
||||||
<small *ngIf="selectablePlan.PasswordManager.basePrice">
|
|
||||||
{{ "basePrice" | i18n }}:
|
|
||||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
|
||||||
{{ "monthAbbr" | i18n }}
|
|
||||||
=
|
|
||||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
|
||||||
/{{ "month" | i18n }}
|
|
||||||
</small>
|
|
||||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption">
|
|
||||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
|
||||||
>{{ "additionalUsers" | i18n }}:</span
|
|
||||||
>
|
>
|
||||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
{{ "basePrice" | i18n }}:
|
||||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||||
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
|
{{ "monthAbbr" | i18n }}
|
||||||
{{ "monthAbbr" | i18n }} =
|
=
|
||||||
{{
|
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
/{{ "month" | i18n }}
|
||||||
| currency: "$"
|
</p>
|
||||||
}}
|
<p
|
||||||
/{{ "month" | i18n }}
|
class="tw-mb-0"
|
||||||
</small>
|
bitTypography="body2"
|
||||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption">
|
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||||
{{ "additionalStorageGb" | i18n }}:
|
>
|
||||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||||
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
|
>{{ "additionalUsers" | i18n }}:</span
|
||||||
{{ "monthAbbr" | i18n }} =
|
>
|
||||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
|
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||||
</small>
|
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||||
</ng-container>
|
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
|
||||||
</label>
|
{{ "monthAbbr" | i18n }} =
|
||||||
</div>
|
{{
|
||||||
</div>
|
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>
|
||||||
|
</bit-radio-group>
|
||||||
|
</bit-section>
|
||||||
|
</bit-section>
|
||||||
|
|
||||||
<!-- Secrets Manager -->
|
<!-- Secrets Manager -->
|
||||||
<div class="tw-my-10">
|
<bit-section>
|
||||||
<sm-subscribe
|
<sm-subscribe
|
||||||
*ngIf="planOffersSecretsManager && !hasProvider"
|
*ngIf="planOffersSecretsManager && !hasProvider"
|
||||||
[formGroup]="formGroup.controls.secretsManager"
|
[formGroup]="formGroup.controls.secretsManager"
|
||||||
[selectedPlan]="selectedSecretsManagerPlan"
|
[selectedPlan]="selectedSecretsManagerPlan"
|
||||||
[upgradeOrganization]="!createOrganization"
|
[upgradeOrganization]="!createOrganization"
|
||||||
></sm-subscribe>
|
></sm-subscribe>
|
||||||
</div>
|
</bit-section>
|
||||||
|
|
||||||
<!-- Payment info -->
|
<!-- Payment info -->
|
||||||
<div *ngIf="formGroup.value.product !== productTypes.Free">
|
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
|
||||||
<h2 class="mb-4">
|
<h2 bitTypography="h2">
|
||||||
{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}
|
{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}
|
||||||
</h2>
|
</h2>
|
||||||
<small class="text-muted font-italic mb-3 d-block">
|
<p class="tw-text-muted tw-italic tw-mb-3 tw-block" bitTypography="body2">
|
||||||
{{ paymentDesc }}
|
{{ paymentDesc }}
|
||||||
</small>
|
</p>
|
||||||
<app-payment
|
<app-payment
|
||||||
*ngIf="createOrganization || upgradeRequiresPaymentMethod"
|
*ngIf="createOrganization || upgradeRequiresPaymentMethod"
|
||||||
[hideCredit]="true"
|
[hideCredit]="true"
|
||||||
></app-payment>
|
></app-payment>
|
||||||
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
|
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
|
||||||
<div id="price" class="my-4">
|
<div id="price" class="tw-my-4">
|
||||||
<div class="text-muted text-sm">
|
<div class="tw-text-muted tw-text-base">
|
||||||
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }}
|
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }}
|
||||||
<br />
|
<br />
|
||||||
<span *ngIf="planOffersSecretsManager && formGroup.value.secretsManager.enabled">
|
<span *ngIf="planOffersSecretsManager && formGroup.value.secretsManager.enabled">
|
||||||
@@ -405,8 +433,8 @@
|
|||||||
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
|
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-1 col-3 ml-0" />
|
<hr class="tw-my-1 tw-grid tw-grid-cols-3 tw-ml-0" />
|
||||||
<p class="text-lg">
|
<p class="tw-text-lg">
|
||||||
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{
|
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{
|
||||||
selectedPlanInterval | i18n
|
selectedPlanInterval | i18n
|
||||||
}}
|
}}
|
||||||
@@ -415,22 +443,29 @@
|
|||||||
<ng-container *ngIf="!createOrganization">
|
<ng-container *ngIf="!createOrganization">
|
||||||
<app-payment [showMethods]="false"></app-payment>
|
<app-payment [showMethods]="false"></app-payment>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</bit-section>
|
||||||
<div *ngIf="singleOrgPolicyBlock" class="mt-4">
|
<bit-section *ngIf="singleOrgPolicyBlock">
|
||||||
<app-callout [type]="'error'">{{ "singleOrgBlockCreateMessage" | i18n }}</app-callout>
|
<app-callout [type]="'error'">{{ "singleOrgBlockCreateMessage" | i18n }}</app-callout>
|
||||||
</div>
|
</bit-section>
|
||||||
<div class="mt-4">
|
<bit-section>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
bitButton
|
bitButton
|
||||||
[loading]="form.loading"
|
bitFormButton
|
||||||
[disabled]="!formGroup.valid"
|
[disabled]="!formGroup.valid"
|
||||||
>
|
>
|
||||||
{{ "submit" | i18n }}
|
{{ "submit" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" buttonType="secondary" bitButton (click)="cancel()" *ngIf="showCancel">
|
<button
|
||||||
|
type="button"
|
||||||
|
buttonType="secondary"
|
||||||
|
bitButton
|
||||||
|
bitFormButton
|
||||||
|
(click)="cancel()"
|
||||||
|
*ngIf="showCancel"
|
||||||
|
>
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</bit-section>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||||||
@Input() showCancel = false;
|
@Input() showCancel = false;
|
||||||
@Input() acceptingSponsorship = false;
|
@Input() acceptingSponsorship = false;
|
||||||
@Input() currentPlan: PlanResponse;
|
@Input() currentPlan: PlanResponse;
|
||||||
|
selectedFile: File;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
get product(): ProductType {
|
get product(): ProductType {
|
||||||
@@ -109,6 +110,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
|
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
|
||||||
|
|
||||||
|
selfHostedForm = this.formBuilder.group({
|
||||||
|
file: [null, [Validators.required]],
|
||||||
|
});
|
||||||
|
|
||||||
formGroup = this.formBuilder.group({
|
formGroup = this.formBuilder.group({
|
||||||
name: [""],
|
name: [""],
|
||||||
billingEmail: ["", [Validators.email]],
|
billingEmail: ["", [Validators.email]],
|
||||||
@@ -527,72 +532,72 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||||||
this.onCanceled.emit();
|
this.onCanceled.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
setSelectedFile(event: Event) {
|
||||||
|
const fileInputEl = <HTMLInputElement>event.target;
|
||||||
|
this.selectedFile = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
if (this.singleOrgPolicyBlock) {
|
if (this.singleOrgPolicyBlock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const doSubmit = async (): Promise<string> => {
|
||||||
|
let orgId: string = null;
|
||||||
|
if (this.createOrganization) {
|
||||||
|
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
|
||||||
|
const key = orgKey[0].encryptedString;
|
||||||
|
const collection = await this.cryptoService.encrypt(
|
||||||
|
this.i18nService.t("defaultCollection"),
|
||||||
|
orgKey[1],
|
||||||
|
);
|
||||||
|
const collectionCt = collection.encryptedString;
|
||||||
|
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
|
||||||
|
|
||||||
try {
|
if (this.selfHosted) {
|
||||||
const doSubmit = async (): Promise<string> => {
|
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
|
||||||
let orgId: string = null;
|
|
||||||
if (this.createOrganization) {
|
|
||||||
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
|
|
||||||
const key = orgKey[0].encryptedString;
|
|
||||||
const collection = await this.cryptoService.encrypt(
|
|
||||||
this.i18nService.t("defaultCollection"),
|
|
||||||
orgKey[1],
|
|
||||||
);
|
|
||||||
const collectionCt = collection.encryptedString;
|
|
||||||
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
|
|
||||||
|
|
||||||
if (this.selfHosted) {
|
|
||||||
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
|
|
||||||
} else {
|
|
||||||
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
this.i18nService.t("organizationCreated"),
|
|
||||||
this.i18nService.t("organizationReadyToGo"),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
orgId = await this.updateOrganization(orgId);
|
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("organizationUpgraded"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.apiService.refreshIdentityToken();
|
this.platformUtilsService.showToast(
|
||||||
await this.syncService.fullSync(true);
|
"success",
|
||||||
|
this.i18nService.t("organizationCreated"),
|
||||||
|
this.i18nService.t("organizationReadyToGo"),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
orgId = await this.updateOrganization(orgId);
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("organizationUpgraded"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
|
await this.apiService.refreshIdentityToken();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.syncService.fullSync(true);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/organizations/" + orgId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isInTrialFlow) {
|
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
|
||||||
this.onTrialBillingSuccess.emit({
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
orgId: orgId,
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
subLabelText: this.billingSubLabelText(),
|
this.router.navigate(["/organizations/" + orgId]);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return orgId;
|
if (this.isInTrialFlow) {
|
||||||
};
|
this.onTrialBillingSuccess.emit({
|
||||||
|
orgId: orgId,
|
||||||
|
subLabelText: this.billingSubLabelText(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.formPromise = doSubmit();
|
return orgId;
|
||||||
const organizationId = await this.formPromise;
|
};
|
||||||
this.onSuccess.emit({ organizationId: organizationId });
|
|
||||||
// TODO: No one actually listening to this message?
|
this.formPromise = doSubmit();
|
||||||
this.messagingService.send("organizationCreated", { organizationId });
|
const organizationId = await this.formPromise;
|
||||||
} catch (e) {
|
this.onSuccess.emit({ organizationId: organizationId });
|
||||||
this.logService.error(e);
|
// TODO: No one actually listening to this message?
|
||||||
}
|
this.messagingService.send("organizationCreated", { organizationId });
|
||||||
}
|
};
|
||||||
|
|
||||||
private async updateOrganization(orgId: string) {
|
private async updateOrganization(orgId: string) {
|
||||||
const request = new OrganizationUpgradeRequest();
|
const request = new OrganizationUpgradeRequest();
|
||||||
@@ -693,14 +698,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
|
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
|
||||||
const fileEl = document.getElementById("file") as HTMLInputElement;
|
if (!this.selectedFile) {
|
||||||
const files = fileEl.files;
|
|
||||||
if (files == null || files.length === 0) {
|
|
||||||
throw new Error(this.i18nService.t("selectFile"));
|
throw new Error(this.i18nService.t("selectFile"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("license", files[0]);
|
fd.append("license", this.selectedFile);
|
||||||
fd.append("key", key);
|
fd.append("key", key);
|
||||||
fd.append("collectionName", collectionCt);
|
fd.append("collectionName", collectionCt);
|
||||||
const response = await this.organizationApiService.createLicense(fd);
|
const response = await this.organizationApiService.createLicense(fd);
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export class VaultCipherRowComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected editCollections() {
|
protected editCollections() {
|
||||||
this.onEvent.emit({ type: "viewCollections", item: this.cipher });
|
this.onEvent.emit({ type: "viewCipherCollections", item: this.cipher });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected events() {
|
protected events() {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td bitCell [ngClass]="RowHeightClass" class="tw-text-right">
|
<td bitCell [ngClass]="RowHeightClass" class="tw-text-right">
|
||||||
<button
|
<button
|
||||||
*ngIf="canEditCollection || canDeleteCollection"
|
*ngIf="canEditCollection || canDeleteCollection || canViewCollectionInfo"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
[bitMenuTriggerFor]="collectionOptions"
|
[bitMenuTriggerFor]="collectionOptions"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -73,14 +73,28 @@
|
|||||||
appStopProp
|
appStopProp
|
||||||
></button>
|
></button>
|
||||||
<bit-menu #collectionOptions>
|
<bit-menu #collectionOptions>
|
||||||
<button *ngIf="canEditCollection" type="button" bitMenuItem (click)="edit()">
|
<ng-container *ngIf="canEditCollection">
|
||||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
<button type="button" bitMenuItem (click)="edit(false)">
|
||||||
{{ "editInfo" | i18n }}
|
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||||
</button>
|
{{ "editInfo" | i18n }}
|
||||||
<button *ngIf="canEditCollection" type="button" bitMenuItem (click)="access()">
|
</button>
|
||||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
<button type="button" bitMenuItem (click)="access(false)">
|
||||||
{{ "access" | i18n }}
|
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||||
</button>
|
{{ "access" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container
|
||||||
|
*ngIf="flexibleCollectionsV1Enabled && !canEditCollection && canViewCollectionInfo"
|
||||||
|
>
|
||||||
|
<button type="button" bitMenuItem (click)="edit(true)">
|
||||||
|
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||||
|
{{ "viewInfo" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitMenuItem (click)="access(true)">
|
||||||
|
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||||
|
{{ "viewAccess" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
<button *ngIf="canDeleteCollection" type="button" bitMenuItem (click)="deleteCollection()">
|
<button *ngIf="canDeleteCollection" type="button" bitMenuItem (click)="deleteCollection()">
|
||||||
<span class="tw-text-danger">
|
<span class="tw-text-danger">
|
||||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ export class VaultCollectionRowComponent {
|
|||||||
@Input() showGroups: boolean;
|
@Input() showGroups: boolean;
|
||||||
@Input() canEditCollection: boolean;
|
@Input() canEditCollection: boolean;
|
||||||
@Input() canDeleteCollection: boolean;
|
@Input() canDeleteCollection: boolean;
|
||||||
|
@Input() canViewCollectionInfo: boolean;
|
||||||
@Input() organizations: Organization[];
|
@Input() organizations: Organization[];
|
||||||
@Input() groups: GroupView[];
|
@Input() groups: GroupView[];
|
||||||
@Input() showPermissionsColumn: boolean;
|
@Input() showPermissionsColumn: boolean;
|
||||||
|
@Input() flexibleCollectionsV1Enabled: boolean;
|
||||||
|
|
||||||
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
@Output() onEvent = new EventEmitter<VaultItemEvent>();
|
||||||
|
|
||||||
@@ -71,12 +73,12 @@ export class VaultCollectionRowComponent {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected edit() {
|
protected edit(readonly: boolean) {
|
||||||
this.onEvent.next({ type: "editCollection", item: this.collection });
|
this.onEvent.next({ type: "editCollection", item: this.collection, readonly: readonly });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected access() {
|
protected access(readonly: boolean) {
|
||||||
this.onEvent.next({ type: "viewCollectionAccess", item: this.collection });
|
this.onEvent.next({ type: "viewCollectionAccess", item: this.collection, readonly: readonly });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected deleteCollection() {
|
protected deleteCollection() {
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { VaultItem } from "./vault-item";
|
|||||||
|
|
||||||
export type VaultItemEvent =
|
export type VaultItemEvent =
|
||||||
| { type: "viewAttachments"; item: CipherView }
|
| { type: "viewAttachments"; item: CipherView }
|
||||||
| { type: "viewCollections"; item: CipherView }
|
| { type: "viewCipherCollections"; item: CipherView }
|
||||||
| { type: "bulkEditCollectionAccess"; items: CollectionView[] }
|
| { type: "bulkEditCollectionAccess"; items: CollectionView[] }
|
||||||
| { type: "viewCollectionAccess"; item: CollectionView }
|
| { type: "viewCollectionAccess"; item: CollectionView; readonly: boolean }
|
||||||
| { type: "viewEvents"; item: CipherView }
|
| { type: "viewEvents"; item: CipherView }
|
||||||
| { type: "editCollection"; item: CollectionView }
|
| { type: "editCollection"; item: CollectionView; readonly: boolean }
|
||||||
| { type: "clone"; item: CipherView }
|
| { type: "clone"; item: CipherView }
|
||||||
| { type: "restore"; items: CipherView[] }
|
| { type: "restore"; items: CipherView[] }
|
||||||
| { type: "delete"; items: VaultItem[] }
|
| { type: "delete"; items: VaultItem[] }
|
||||||
|
|||||||
@@ -95,6 +95,8 @@
|
|||||||
[groups]="allGroups"
|
[groups]="allGroups"
|
||||||
[canDeleteCollection]="canDeleteCollection(item.collection)"
|
[canDeleteCollection]="canDeleteCollection(item.collection)"
|
||||||
[canEditCollection]="canEditCollection(item.collection)"
|
[canEditCollection]="canEditCollection(item.collection)"
|
||||||
|
[canViewCollectionInfo]="canViewCollectionInfo(item.collection)"
|
||||||
|
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled"
|
||||||
[checked]="selection.isSelected(item)"
|
[checked]="selection.isSelected(item)"
|
||||||
(checkedToggled)="selection.toggle(item)"
|
(checkedToggled)="selection.toggle(item)"
|
||||||
(onEvent)="event($event)"
|
(onEvent)="event($event)"
|
||||||
|
|||||||
@@ -165,6 +165,11 @@ export class VaultItemsComponent {
|
|||||||
return collection.canDelete(organization);
|
return collection.canDelete(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected canViewCollectionInfo(collection: CollectionView) {
|
||||||
|
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||||
|
return collection.canViewCollectionInfo(organization);
|
||||||
|
}
|
||||||
|
|
||||||
protected toggleAll() {
|
protected toggleAll() {
|
||||||
this.isAllSelected
|
this.isAllSelected
|
||||||
? this.selection.clear()
|
? this.selection.clear()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/mod
|
|||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
|
|
||||||
import { CollectionAccessSelectionView } from "../../../admin-console/organizations/core/views/collection-access-selection.view";
|
import { CollectionAccessSelectionView } from "../../../admin-console/organizations/core/views/collection-access-selection.view";
|
||||||
|
import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||||
|
|
||||||
export class CollectionAdminView extends CollectionView {
|
export class CollectionAdminView extends CollectionView {
|
||||||
groups: CollectionAccessSelectionView[] = [];
|
groups: CollectionAccessSelectionView[] = [];
|
||||||
@@ -89,4 +90,19 @@ export class CollectionAdminView extends CollectionView {
|
|||||||
canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||||
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups;
|
return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user can view collection info and access in a read-only state from the Admin Console
|
||||||
|
*/
|
||||||
|
override canViewCollectionInfo(org: Organization | undefined): boolean {
|
||||||
|
if (this.isUnassignedCollection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.manage || org?.isAdmin || org?.permissions.editAnyCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUnassignedCollection() {
|
||||||
|
return this.id === Unassigned;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
try {
|
try {
|
||||||
if (event.type === "viewAttachments") {
|
if (event.type === "viewAttachments") {
|
||||||
await this.editCipherAttachments(event.item);
|
await this.editCipherAttachments(event.item);
|
||||||
} else if (event.type === "viewCollections") {
|
} else if (event.type === "viewCipherCollections") {
|
||||||
await this.editCipherCollections(event.item);
|
await this.editCipherCollections(event.item);
|
||||||
} else if (event.type === "clone") {
|
} else if (event.type === "clone") {
|
||||||
await this.cloneCipher(event.item);
|
await this.cloneCipher(event.item);
|
||||||
|
|||||||
@@ -37,24 +37,44 @@
|
|||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
></button>
|
></button>
|
||||||
<bit-menu #editCollectionMenu>
|
<bit-menu #editCollectionMenu>
|
||||||
<button
|
<ng-container *ngIf="canEditCollection">
|
||||||
type="button"
|
<button
|
||||||
*ngIf="canEditCollection"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="editCollection(CollectionDialogTabType.Info)"
|
(click)="editCollection(CollectionDialogTabType.Info, false)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||||
|
{{ "editInfo" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="editCollection(CollectionDialogTabType.Access, false)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||||
|
{{ "access" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container
|
||||||
|
*ngIf="flexibleCollectionsV1Enabled && !canEditCollection && canViewCollectionInfo"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
<button
|
||||||
{{ "editInfo" | i18n }}
|
type="button"
|
||||||
</button>
|
bitMenuItem
|
||||||
<button
|
(click)="editCollection(CollectionDialogTabType.Info, true)"
|
||||||
type="button"
|
>
|
||||||
*ngIf="canEditCollection"
|
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||||
bitMenuItem
|
{{ "viewInfo" | i18n }}
|
||||||
(click)="editCollection(CollectionDialogTabType.Access)"
|
</button>
|
||||||
>
|
<button
|
||||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
type="button"
|
||||||
{{ "access" | i18n }}
|
bitMenuItem
|
||||||
</button>
|
(click)="editCollection(CollectionDialogTabType.Access, true)"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||||
|
{{ "viewAccess" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
<button type="button" *ngIf="canDeleteCollection" bitMenuItem (click)="deleteCollection()">
|
<button type="button" *ngIf="canDeleteCollection" bitMenuItem (click)="deleteCollection()">
|
||||||
<span class="tw-text-danger">
|
<span class="tw-text-danger">
|
||||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ export class VaultHeaderComponent implements OnInit {
|
|||||||
@Output() onAddCollection = new EventEmitter<void>();
|
@Output() onAddCollection = new EventEmitter<void>();
|
||||||
|
|
||||||
/** Emits an event when the edit collection button is clicked in the header */
|
/** Emits an event when the edit collection button is clicked in the header */
|
||||||
@Output() onEditCollection = new EventEmitter<{ tab: CollectionDialogTabType }>();
|
@Output() onEditCollection = new EventEmitter<{
|
||||||
|
tab: CollectionDialogTabType;
|
||||||
|
readonly: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
/** Emits an event when the delete collection button is clicked in the header */
|
/** Emits an event when the delete collection button is clicked in the header */
|
||||||
@Output() onDeleteCollection = new EventEmitter<void>();
|
@Output() onDeleteCollection = new EventEmitter<void>();
|
||||||
@@ -64,7 +67,7 @@ export class VaultHeaderComponent implements OnInit {
|
|||||||
protected CollectionDialogTabType = CollectionDialogTabType;
|
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||||
protected organizations$ = this.organizationService.organizations$;
|
protected organizations$ = this.organizationService.organizations$;
|
||||||
|
|
||||||
private flexibleCollectionsV1Enabled = false;
|
protected flexibleCollectionsV1Enabled = false;
|
||||||
private restrictProviderAccessFlag = false;
|
private restrictProviderAccessFlag = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -193,8 +196,8 @@ export class VaultHeaderComponent implements OnInit {
|
|||||||
this.onAddCollection.emit();
|
this.onAddCollection.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
async editCollection(tab: CollectionDialogTabType): Promise<void> {
|
async editCollection(tab: CollectionDialogTabType, readonly: boolean): Promise<void> {
|
||||||
this.onEditCollection.emit({ tab });
|
this.onEditCollection.emit({ tab, readonly });
|
||||||
}
|
}
|
||||||
|
|
||||||
get canDeleteCollection(): boolean {
|
get canDeleteCollection(): boolean {
|
||||||
@@ -207,6 +210,10 @@ export class VaultHeaderComponent implements OnInit {
|
|||||||
return this.collection.node.canDelete(this.organization);
|
return this.collection.node.canDelete(this.organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canViewCollectionInfo(): boolean {
|
||||||
|
return this.collection.node.canViewCollectionInfo(this.organization);
|
||||||
|
}
|
||||||
|
|
||||||
get canCreateCollection(): boolean {
|
get canCreateCollection(): boolean {
|
||||||
return this.organization?.canCreateNewCollections;
|
return this.organization?.canCreateNewCollections;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
[searchText]="currentSearchText$ | async"
|
[searchText]="currentSearchText$ | async"
|
||||||
(onAddCipher)="addCipher()"
|
(onAddCipher)="addCipher()"
|
||||||
(onAddCollection)="addCollection()"
|
(onAddCollection)="addCollection()"
|
||||||
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
|
(onEditCollection)="editCollection(selectedCollection.node, $event.tab, $event.readonly)"
|
||||||
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
|
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
|
||||||
(searchTextChanged)="filterSearchText($event)"
|
(searchTextChanged)="filterSearchText($event)"
|
||||||
></app-org-vault-header>
|
></app-org-vault-header>
|
||||||
|
|||||||
@@ -736,7 +736,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
try {
|
try {
|
||||||
if (event.type === "viewAttachments") {
|
if (event.type === "viewAttachments") {
|
||||||
await this.editCipherAttachments(event.item);
|
await this.editCipherAttachments(event.item);
|
||||||
} else if (event.type === "viewCollections") {
|
} else if (event.type === "viewCipherCollections") {
|
||||||
await this.editCipherCollections(event.item);
|
await this.editCipherCollections(event.item);
|
||||||
} else if (event.type === "clone") {
|
} else if (event.type === "clone") {
|
||||||
await this.cloneCipher(event.item);
|
await this.cloneCipher(event.item);
|
||||||
@@ -761,9 +761,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
} else if (event.type === "copyField") {
|
} else if (event.type === "copyField") {
|
||||||
await this.copy(event.item, event.field);
|
await this.copy(event.item, event.field);
|
||||||
} else if (event.type === "editCollection") {
|
} else if (event.type === "editCollection") {
|
||||||
await this.editCollection(event.item, CollectionDialogTabType.Info);
|
await this.editCollection(event.item, CollectionDialogTabType.Info, event.readonly);
|
||||||
} else if (event.type === "viewCollectionAccess") {
|
} else if (event.type === "viewCollectionAccess") {
|
||||||
await this.editCollection(event.item, CollectionDialogTabType.Access);
|
await this.editCollection(event.item, CollectionDialogTabType.Access, event.readonly);
|
||||||
} else if (event.type === "bulkEditCollectionAccess") {
|
} else if (event.type === "bulkEditCollectionAccess") {
|
||||||
await this.bulkEditCollectionAccess(event.items);
|
await this.bulkEditCollectionAccess(event.items);
|
||||||
} else if (event.type === "assignToCollections") {
|
} else if (event.type === "assignToCollections") {
|
||||||
@@ -1190,7 +1190,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
async editCollection(
|
async editCollection(
|
||||||
c: CollectionView,
|
c: CollectionView,
|
||||||
tab: CollectionDialogTabType,
|
tab: CollectionDialogTabType,
|
||||||
readonly: boolean = false,
|
readonly: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const dialog = openCollectionDialog(this.dialogService, {
|
const dialog = openCollectionDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -8081,5 +8081,11 @@
|
|||||||
},
|
},
|
||||||
"manageBillingFromProviderPortalMessage": {
|
"manageBillingFromProviderPortalMessage": {
|
||||||
"message": "Manage billing from the Provider Portal"
|
"message": "Manage billing from the Provider Portal"
|
||||||
|
},
|
||||||
|
"viewInfo": {
|
||||||
|
"message": "View info"
|
||||||
|
},
|
||||||
|
"viewAccess": {
|
||||||
|
"message": "View access"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p>
|
<p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-mb-auto tw-mx-auto tw-grid">
|
<div class="tw-mb-auto tw-mx-auto tw-flex tw-flex-col tw-items-center">
|
||||||
<div
|
<div
|
||||||
class="tw-rounded-xl tw-mb-9 tw-mx-auto sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"
|
class="tw-rounded-xl tw-mb-9 tw-mx-auto sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export enum FeatureFlag {
|
|||||||
FlexibleCollectionsV1 = "flexible-collections-v-1", // v-1 is intentional
|
FlexibleCollectionsV1 = "flexible-collections-v-1", // v-1 is intentional
|
||||||
VaultOnboarding = "vault-onboarding",
|
VaultOnboarding = "vault-onboarding",
|
||||||
GeneratorToolsModernization = "generator-tools-modernization",
|
GeneratorToolsModernization = "generator-tools-modernization",
|
||||||
KeyRotationImprovements = "key-rotation-improvements",
|
|
||||||
FlexibleCollectionsMigration = "flexible-collections-migration",
|
FlexibleCollectionsMigration = "flexible-collections-migration",
|
||||||
ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners",
|
ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners",
|
||||||
EnableConsolidatedBilling = "enable-consolidated-billing",
|
EnableConsolidatedBilling = "enable-consolidated-billing",
|
||||||
@@ -37,7 +36,6 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.FlexibleCollectionsV1]: FALSE,
|
[FeatureFlag.FlexibleCollectionsV1]: FALSE,
|
||||||
[FeatureFlag.VaultOnboarding]: FALSE,
|
[FeatureFlag.VaultOnboarding]: FALSE,
|
||||||
[FeatureFlag.GeneratorToolsModernization]: FALSE,
|
[FeatureFlag.GeneratorToolsModernization]: FALSE,
|
||||||
[FeatureFlag.KeyRotationImprovements]: FALSE,
|
|
||||||
[FeatureFlag.FlexibleCollectionsMigration]: FALSE,
|
[FeatureFlag.FlexibleCollectionsMigration]: FALSE,
|
||||||
[FeatureFlag.ShowPaymentMethodWarningBanners]: FALSE,
|
[FeatureFlag.ShowPaymentMethodWarningBanners]: FALSE,
|
||||||
[FeatureFlag.EnableConsolidatedBilling]: FALSE,
|
[FeatureFlag.EnableConsolidatedBilling]: FALSE,
|
||||||
|
|||||||
@@ -87,6 +87,13 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
: org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections;
|
: org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user can view collection info and access in a read-only state from the individual vault
|
||||||
|
*/
|
||||||
|
canViewCollectionInfo(org: Organization | undefined): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static fromJSON(obj: Jsonify<CollectionView>) {
|
static fromJSON(obj: Jsonify<CollectionView>) {
|
||||||
return Object.assign(new CollectionView(new Collection()), obj);
|
return Object.assign(new CollectionView(new Collection()), obj);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user