mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 14:34:02 +00:00
Merge branch 'ps/extension-refresh' into mer/pre-release-flag-on
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
<app-current-account *ngIf="showAcctSwitcher"></app-current-account>
|
||||
<app-current-account *ngIf="showAcctSwitcher && hasLoggedInAccount"></app-current-account>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
import { CurrentAccountComponent } from "../account-switching/current-account.component";
|
||||
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
|
||||
|
||||
import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon";
|
||||
|
||||
@@ -50,6 +51,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
|
||||
protected pageIcon: Icon;
|
||||
protected showReadonlyHostname: boolean;
|
||||
protected maxWidth: "md" | "3xl";
|
||||
protected hasLoggedInAccount: boolean = false;
|
||||
|
||||
protected theme: string;
|
||||
protected logo = ExtensionBitwardenLogo;
|
||||
@@ -59,6 +61,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
|
||||
private route: ActivatedRoute,
|
||||
private i18nService: I18nService,
|
||||
private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
private accountSwitcherService: AccountSwitcherService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -68,6 +71,12 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
|
||||
// Listen for page changes and update the page data appropriately
|
||||
this.listenForPageDataChanges();
|
||||
this.listenForServiceDataChanges();
|
||||
|
||||
this.accountSwitcherService.availableAccounts$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((accounts) => {
|
||||
this.hasLoggedInAccount = accounts.some((account) => account.id !== "addAccount");
|
||||
});
|
||||
}
|
||||
|
||||
private listenForPageDataChanges() {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { ButtonModule, I18nMockService } from "@bitwarden/components";
|
||||
|
||||
import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon";
|
||||
import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
|
||||
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service";
|
||||
import {
|
||||
@@ -45,6 +46,7 @@ const decorators = (options: {
|
||||
applicationVersion?: string;
|
||||
clientType?: ClientType;
|
||||
hostName?: string;
|
||||
accounts?: any[];
|
||||
}) => {
|
||||
return [
|
||||
componentWrapperDecorator(
|
||||
@@ -83,6 +85,13 @@ const decorators = (options: {
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AccountSwitcherService,
|
||||
useValue: {
|
||||
availableAccounts$: of(options.accounts || []),
|
||||
SPECIAL_ADD_ACCOUNT_ID: "addAccount",
|
||||
} as Partial<AccountSwitcherService>,
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {
|
||||
@@ -300,3 +309,64 @@ export const DynamicContentExample: Story = {
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
export const HasLoggedInAccountExample: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: "<router-outlet></router-outlet>",
|
||||
}),
|
||||
decorators: decorators({
|
||||
components: [DefaultPrimaryOutletExampleComponent],
|
||||
routes: [
|
||||
{
|
||||
path: "**",
|
||||
redirectTo: "has-logged-in-account",
|
||||
pathMatch: "full",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
children: [
|
||||
{
|
||||
path: "has-logged-in-account",
|
||||
data: {
|
||||
hasLoggedInAccount: true,
|
||||
showAcctSwitcher: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: DefaultPrimaryOutletExampleComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: DefaultSecondaryOutletExampleComponent,
|
||||
outlet: "secondary",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: DefaultEnvSelectorOutletExampleComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
accounts: [
|
||||
{
|
||||
name: "Test User",
|
||||
email: "testuser@bitwarden.com",
|
||||
id: "123e4567-e89b-12d3-a456-426614174000",
|
||||
server: "bitwarden.com",
|
||||
status: 2,
|
||||
isActive: false,
|
||||
},
|
||||
{
|
||||
name: "addAccount",
|
||||
id: "addAccount",
|
||||
isActive: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, switchMap, takeUntil } from "rxjs";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, switchMap, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
@@ -38,9 +40,13 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
private accountSwitcherService: AccountSwitcherService,
|
||||
private registerRouteService: RegisterRouteService,
|
||||
private toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
private route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.listenForUnauthUiRefreshFlagChanges();
|
||||
|
||||
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
const rememberEmail = this.loginEmailService.getRememberEmail();
|
||||
|
||||
@@ -70,6 +76,29 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
||||
private listenForUnauthUiRefreshFlagChanges() {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
|
||||
.pipe(
|
||||
tap(async (flag) => {
|
||||
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
|
||||
if (flag) {
|
||||
const uniqueQueryParams = {
|
||||
...this.route.queryParams,
|
||||
// adding a unique timestamp to the query params to force a reload
|
||||
t: new Date().getTime().toString(),
|
||||
};
|
||||
|
||||
await this.router.navigate(["/login"], {
|
||||
queryParams: uniqueQueryParams,
|
||||
});
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
get availableAccounts$() {
|
||||
return this.accountSwitcherService.availableAccounts$;
|
||||
}
|
||||
|
||||
@@ -58,11 +58,33 @@ export class BrowserApi {
|
||||
}
|
||||
|
||||
static async createWindow(options: chrome.windows.CreateData): Promise<chrome.windows.Window> {
|
||||
return new Promise((resolve) =>
|
||||
chrome.windows.create(options, (window) => {
|
||||
resolve(window);
|
||||
}),
|
||||
);
|
||||
return new Promise((resolve) => {
|
||||
chrome.windows.create(options, async (newWindow) => {
|
||||
if (!BrowserApi.isSafariApi) {
|
||||
return resolve(newWindow);
|
||||
}
|
||||
// Safari doesn't close the default extension popup when a new window is created so we need to
|
||||
// manually trigger the close by focusing the main window after the new window is created
|
||||
const allWindows = await new Promise<chrome.windows.Window[]>((resolve) => {
|
||||
chrome.windows.getAll({ windowTypes: ["normal"] }, (windows) => resolve(windows));
|
||||
});
|
||||
|
||||
const mainWindow = allWindows.find((window) => window.id !== newWindow.id);
|
||||
|
||||
// No main window found, resolve the new window
|
||||
if (mainWindow == null || !mainWindow.id) {
|
||||
return resolve(newWindow);
|
||||
}
|
||||
|
||||
// Focus the main window to close the extension popup
|
||||
chrome.windows.update(mainWindow.id, { focused: true }, () => {
|
||||
// Refocus the newly created window
|
||||
chrome.windows.update(newWindow.id, { focused: true }, () => {
|
||||
resolve(newWindow);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: $font-family-sans-serif;
|
||||
|
||||
36
apps/desktop/desktop_native/Cargo.lock
generated
36
apps/desktop/desktop_native/Cargo.lock
generated
@@ -39,9 +39,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
version = "1.0.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
@@ -1154,9 +1154,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "2.16.11"
|
||||
version = "2.16.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53575dfa17f208dd1ce3a2da2da4659aae393b256a472f2738a8586a6c4107fd"
|
||||
checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ctor",
|
||||
@@ -1867,18 +1867,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.61"
|
||||
version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.61"
|
||||
version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2487,9 +2487,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "4.3.1"
|
||||
version = "4.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "851238c133804e0aa888edf4a0229481c753544ca12a60fd1c3230c8a500fe40"
|
||||
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
@@ -2525,9 +2525,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "4.3.1"
|
||||
version = "4.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d5a3f12c20bd473be3194af6b49d50d7bb804ef3192dc70eddedb26b85d9da7"
|
||||
checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
@@ -2583,9 +2583,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "4.1.2"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1724a2b330760dc7d2a8402d841119dc869ef120b139d29862d6980e9c75bfc9"
|
||||
checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
@@ -2596,9 +2596,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "4.1.2"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55025a7a518ad14518fb243559c058a2e5b848b015e31f1d90414f36e3317859"
|
||||
checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
@@ -2609,9 +2609,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786"
|
||||
checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -23,7 +23,7 @@ sys = [
|
||||
|
||||
[dependencies]
|
||||
aes = "=0.8.4"
|
||||
anyhow = "=1.0.86"
|
||||
anyhow = "=1.0.93"
|
||||
arboard = { version = "=3.4.1", default-features = false, features = [
|
||||
"wayland-data-control",
|
||||
] }
|
||||
@@ -38,7 +38,7 @@ rand = "=0.8.5"
|
||||
retry = "=2.0.0"
|
||||
scopeguard = "=1.2.0"
|
||||
sha2 = "=0.10.8"
|
||||
thiserror = "=1.0.61"
|
||||
thiserror = "=1.0.68"
|
||||
tokio = { version = "=1.41.0", features = ["io-util", "sync", "macros"] }
|
||||
tokio-util = "=0.7.12"
|
||||
typenum = "=1.17.0"
|
||||
@@ -68,5 +68,5 @@ security-framework-sys = { version = "=2.11.0", optional = true }
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
gio = { version = "=0.19.5", optional = true }
|
||||
libsecret = { version = "=0.5.0", optional = true }
|
||||
zbus = { version = "=4.3.1", optional = true }
|
||||
zbus = { version = "=4.4.0", optional = true }
|
||||
zbus_polkit = { version = "=4.0.0", optional = true }
|
||||
|
||||
@@ -14,9 +14,9 @@ default = []
|
||||
manual_test = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "=1.0.86"
|
||||
anyhow = "=1.0.93"
|
||||
desktop_core = { path = "../core" }
|
||||
napi = { version = "=2.16.11", features = ["async"] }
|
||||
napi = { version = "=2.16.13", features = ["async"] }
|
||||
napi-derive = "=2.16.12"
|
||||
tokio = { version = "1.38.0" }
|
||||
tokio-util = "0.7.11"
|
||||
|
||||
@@ -7,7 +7,7 @@ version = "0.0.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "=1.0.86"
|
||||
anyhow = "=1.0.93"
|
||||
desktop_core = { path = "../core", default-features = false }
|
||||
futures = "0.3.30"
|
||||
log = "0.4.22"
|
||||
|
||||
@@ -58,30 +58,46 @@ async function run(context) {
|
||||
id = identities[0].id;
|
||||
}
|
||||
|
||||
console.log(`Signing proxy binary before the main bundle, using identity '${id}'`);
|
||||
console.log(
|
||||
`Signing proxy binary before the main bundle, using identity '${id}', for build ${context.electronPlatformName}`,
|
||||
);
|
||||
|
||||
const appName = context.packager.appInfo.productFilename;
|
||||
const appPath = `${context.appOutDir}/${appName}.app`;
|
||||
const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy");
|
||||
const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit");
|
||||
|
||||
const packageId = "com.bitwarden.desktop";
|
||||
const entitlementsName = "entitlements.desktop_proxy.plist";
|
||||
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
|
||||
child_process.execSync(
|
||||
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
|
||||
);
|
||||
|
||||
const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit");
|
||||
const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist";
|
||||
const inheritEntitlementsPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"resources",
|
||||
inheritEntitlementsName,
|
||||
);
|
||||
child_process.execSync(
|
||||
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`,
|
||||
);
|
||||
if (is_mas) {
|
||||
const entitlementsName = "entitlements.desktop_proxy.plist";
|
||||
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
|
||||
child_process.execSync(
|
||||
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
|
||||
);
|
||||
|
||||
const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist";
|
||||
const inheritEntitlementsPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"resources",
|
||||
inheritEntitlementsName,
|
||||
);
|
||||
child_process.execSync(
|
||||
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`,
|
||||
);
|
||||
} else {
|
||||
// For non-Appstore builds, we don't need the inherit binary as they are not sandboxed,
|
||||
// but we sign and include it anyway for consistency. It should be removed once DDG supports the proxy directly.
|
||||
const entitlementsName = "entitlements.mac.plist";
|
||||
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
|
||||
child_process.execSync(
|
||||
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
|
||||
);
|
||||
child_process.execSync(
|
||||
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${inheritProxyPath}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { Subject, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
@@ -14,8 +14,10 @@ import {
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -76,6 +78,7 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
||||
webAuthnLoginService: WebAuthnLoginServiceAbstraction,
|
||||
registerRouteService: RegisterRouteService,
|
||||
toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
devicesApiService,
|
||||
@@ -105,6 +108,8 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.listenForUnauthUiRefreshFlagChanges();
|
||||
|
||||
await super.ngOnInit();
|
||||
await this.getLoginWithDevice(this.loggedEmail);
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
@@ -137,6 +142,29 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
||||
this.componentDestroyed$.complete();
|
||||
}
|
||||
|
||||
private listenForUnauthUiRefreshFlagChanges() {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
|
||||
.pipe(
|
||||
tap(async (flag) => {
|
||||
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
|
||||
if (flag) {
|
||||
const uniqueQueryParams = {
|
||||
...this.route.queryParams,
|
||||
// adding a unique timestamp to the query params to force a reload
|
||||
t: new Date().getTime().toString(),
|
||||
};
|
||||
|
||||
await this.router.navigate(["/"], {
|
||||
queryParams: uniqueQueryParams,
|
||||
});
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async settings() {
|
||||
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||
EnvironmentComponent,
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
<bit-nav-item
|
||||
*ngIf="isAccessIntelligenceFeatureEnabled"
|
||||
[text]="'accessIntelligence' | i18n"
|
||||
route="access-intelligence"
|
||||
*ngIf="isRiskInsightsFeatureEnabled"
|
||||
[text]="'riskInsights' | i18n"
|
||||
route="risk-insights"
|
||||
></bit-nav-item>
|
||||
<bit-nav-group
|
||||
icon="bwi-billing"
|
||||
|
||||
@@ -51,7 +51,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
showPaymentAndHistory$: Observable<boolean>;
|
||||
hideNewOrgButton$: Observable<boolean>;
|
||||
organizationIsUnmanaged$: Observable<boolean>;
|
||||
isAccessIntelligenceFeatureEnabled = false;
|
||||
isRiskInsightsFeatureEnabled = false;
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
@@ -71,7 +71,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit() {
|
||||
document.body.classList.remove("layout_frontend");
|
||||
|
||||
this.isAccessIntelligenceFeatureEnabled = await this.configService.getFeatureFlag(
|
||||
this.isRiskInsightsFeatureEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.AccessIntelligence,
|
||||
);
|
||||
|
||||
|
||||
@@ -63,10 +63,10 @@ const routes: Routes = [
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "access-intelligence",
|
||||
path: "risk-insights",
|
||||
loadChildren: () =>
|
||||
import("../../tools/access-intelligence/access-intelligence.module").then(
|
||||
(m) => m.AccessIntelligenceModule,
|
||||
import("../../tools/risk-insights/risk-insights.module").then(
|
||||
(m) => m.RiskInsightsModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -123,20 +123,22 @@ export class AccountComponent implements OnInit, OnDestroy {
|
||||
this.canEditSubscription = organization.canEditSubscription;
|
||||
this.canUseApi = organization.useApi;
|
||||
|
||||
// Update disabled states - reactive forms prefers not using disabled attribute
|
||||
// Disabling these fields for self hosted orgs is deprecated
|
||||
// This block can be completely removed as part of
|
||||
// https://bitwarden.atlassian.net/browse/PM-10863
|
||||
if (!this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled) {
|
||||
if (!this.selfHosted) {
|
||||
this.formGroup.get("orgName").enable();
|
||||
this.collectionManagementFormGroup.get("limitCollectionCreationDeletion").enable();
|
||||
this.collectionManagementFormGroup.get("allowAdminAccessToAllCollectionItems").enable();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.selfHosted && this.canEditSubscription) {
|
||||
this.formGroup.get("billingEmail").enable();
|
||||
// Update disabled states - reactive forms prefers not using disabled attribute
|
||||
if (!this.selfHosted) {
|
||||
this.formGroup.get("orgName").enable();
|
||||
if (this.canEditSubscription) {
|
||||
this.formGroup.get("billingEmail").enable();
|
||||
}
|
||||
}
|
||||
|
||||
// Org Response
|
||||
|
||||
@@ -48,16 +48,7 @@
|
||||
}}</span>
|
||||
</dd>
|
||||
<dt>{{ "nextCharge" | i18n }}</dt>
|
||||
<dd *ngIf="!enableTimeThreshold">
|
||||
{{
|
||||
nextInvoice
|
||||
? (nextInvoice.date | date: "mediumDate") +
|
||||
", " +
|
||||
(nextInvoice.amount | currency: "$")
|
||||
: "-"
|
||||
}}
|
||||
</dd>
|
||||
<dd *ngIf="enableTimeThreshold">
|
||||
<dd>
|
||||
{{
|
||||
nextInvoice
|
||||
? (sub.subscription.periodEndDate | date: "mediumDate") +
|
||||
|
||||
@@ -38,13 +38,9 @@ export class UserSubscriptionComponent implements OnInit {
|
||||
sub: SubscriptionResponse;
|
||||
selfHosted = false;
|
||||
cloudWebVaultUrl: string;
|
||||
enableTimeThreshold: boolean;
|
||||
|
||||
cancelPromise: Promise<any>;
|
||||
reinstatePromise: Promise<any>;
|
||||
protected enableTimeThreshold$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.EnableTimeThreshold,
|
||||
);
|
||||
|
||||
protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
|
||||
@@ -69,7 +65,6 @@ export class UserSubscriptionComponent implements OnInit {
|
||||
async ngOnInit() {
|
||||
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
||||
await this.load();
|
||||
this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$);
|
||||
this.firstLoaded = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,10 +48,7 @@
|
||||
<dt [ngClass]="{ 'tw-text-danger': isExpired }">
|
||||
{{ "subscriptionExpiration" | i18n }}
|
||||
</dt>
|
||||
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="!enableTimeThreshold">
|
||||
{{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }}
|
||||
</dd>
|
||||
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="enableTimeThreshold">
|
||||
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
|
||||
{{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") : "-" }}
|
||||
</dd>
|
||||
</ng-container>
|
||||
|
||||
@@ -52,7 +52,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
loading = true;
|
||||
locale: string;
|
||||
showUpdatedSubscriptionStatusSection$: Observable<boolean>;
|
||||
enableTimeThreshold: boolean;
|
||||
preSelectedProductTier: ProductTierType = ProductTierType.Free;
|
||||
showSubscription = true;
|
||||
showSelfHost = false;
|
||||
@@ -65,10 +64,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
FeatureFlag.EnableConsolidatedBilling,
|
||||
);
|
||||
|
||||
protected enableTimeThreshold$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.EnableTimeThreshold,
|
||||
);
|
||||
|
||||
protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.EnableUpgradePasswordManagerSub,
|
||||
);
|
||||
@@ -117,7 +112,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AC1795_UpdatedSubscriptionStatusSection,
|
||||
);
|
||||
this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -298,9 +292,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
return this.i18nService.t("subscriptionUpgrade", this.sub.seats.toString());
|
||||
}
|
||||
} else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) {
|
||||
if (!this.enableTimeThreshold) {
|
||||
return this.i18nService.t("subscriptionMaxReached", this.sub.seats.toString());
|
||||
}
|
||||
const seatAdjustmentMessage = this.sub.plan.isAnnual
|
||||
? "annualSubscriptionUserSeatsMessage"
|
||||
: "monthlySubscriptionUserSeatsMessage";
|
||||
@@ -311,21 +302,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
} else if (this.userOrg.productTierType === ProductTierType.TeamsStarter) {
|
||||
return this.i18nService.t("subscriptionUserSeatsWithoutAdditionalSeatsOption", 10);
|
||||
} else if (this.sub.maxAutoscaleSeats == null) {
|
||||
if (!this.enableTimeThreshold) {
|
||||
return this.i18nService.t("subscriptionUserSeatsUnlimitedAutoscale");
|
||||
}
|
||||
|
||||
const seatAdjustmentMessage = this.sub.plan.isAnnual
|
||||
? "annualSubscriptionUserSeatsMessage"
|
||||
: "monthlySubscriptionUserSeatsMessage";
|
||||
return this.i18nService.t(seatAdjustmentMessage);
|
||||
} else {
|
||||
if (!this.enableTimeThreshold) {
|
||||
return this.i18nService.t(
|
||||
"subscriptionUserSeatsLimitedAutoscale",
|
||||
this.sub.maxAutoscaleSeats.toString(),
|
||||
);
|
||||
}
|
||||
const seatAdjustmentMessage = this.sub.plan.isAnnual
|
||||
? "annualSubscriptionUserSeatsMessage"
|
||||
: "monthlySubscriptionUserSeatsMessage";
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
|
||||
import { AccessIntelligenceComponent } from "./access-intelligence.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [AccessIntelligenceComponent, AccessIntelligenceRoutingModule],
|
||||
})
|
||||
export class AccessIntelligenceModule {}
|
||||
@@ -1,120 +0,0 @@
|
||||
<p>{{ "passwordsReportDesc" | i18n }}</p>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
|
||||
<div class="tw-flex tw-gap-6">
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskMembers' | i18n"
|
||||
[value]="totalMembersMap.size - 3"
|
||||
[maxValue]="totalMembersMap.size"
|
||||
>
|
||||
</tools-card>
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskApplications' | i18n"
|
||||
[value]="totalMembersMap.size - 1"
|
||||
[maxValue]="totalMembersMap.size"
|
||||
>
|
||||
</tools-card>
|
||||
</div>
|
||||
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
|
||||
<bit-search class="tw-grow" [formControl]="searchControl"></bit-search>
|
||||
<button class="tw-rounded-lg" type="button" buttonType="secondary" bitButton>
|
||||
<i class="bwi bwi-star-f tw-mr-2"></i>
|
||||
{{ "markAppAsCritical" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
|
||||
<div class="tw-flex tw-gap-6">
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskMembers' | i18n"
|
||||
[value]="totalMembersMap.size - 3"
|
||||
[maxValue]="totalMembersMap.size"
|
||||
>
|
||||
</tools-card>
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskApplications' | i18n"
|
||||
[value]="totalMembersMap.size - 1"
|
||||
[maxValue]="totalMembersMap.size"
|
||||
>
|
||||
</tools-card>
|
||||
</div>
|
||||
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
|
||||
<bit-search class="tw-grow" [formControl]="searchControl"></bit-search>
|
||||
<button
|
||||
class="tw-rounded-lg"
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
[disabled]="!selectedIds.size"
|
||||
bitButton
|
||||
[bitAction]="markAppsAsCritical"
|
||||
appA11yTitle="{{ 'markAppAsCritical' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-star-f tw-mr-2"></i>
|
||||
{{ "markAppAsCritical" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr bitRow>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
|
||||
<td bitCell>
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
[checked]="selectedIds.has(r.id)"
|
||||
(change)="onCheckboxChange(r.id, $event)"
|
||||
/>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container>
|
||||
<span>{{ r.name }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ r.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span
|
||||
bitBadge
|
||||
*ngIf="passwordStrengthMap.has(r.id)"
|
||||
[variant]="passwordStrengthMap.get(r.id)[1]"
|
||||
>
|
||||
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
|
||||
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
|
||||
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right" data-testid="total-membership">
|
||||
{{ totalMembersMap.get(r.id) || 0 }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -12,8 +12,8 @@ import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { AccessIntelligenceTabType } from "./access-intelligence.component";
|
||||
import { applicationTableMockData } from "./application-table.mock";
|
||||
import { RiskInsightsTabType } from "./risk-insights.component";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -49,8 +49,8 @@ export class CriticalApplicationsComponent implements OnInit {
|
||||
}
|
||||
|
||||
goToAllAppsTab = async () => {
|
||||
await this.router.navigate([`organizations/${this.organizationId}/access-intelligence`], {
|
||||
queryParams: { tabIndex: AccessIntelligenceTabType.AllApps },
|
||||
await this.router.navigate([`organizations/${this.organizationId}/risk-insights`], {
|
||||
queryParams: { tabIndex: RiskInsightsTabType.AllApps },
|
||||
queryParamsHandling: "merge",
|
||||
});
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
|
||||
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -6,7 +6,7 @@ import { map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
|
||||
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -0,0 +1,64 @@
|
||||
<p>{{ "passwordsReportDesc" | i18n }}</p>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr bitRow>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
|
||||
<td bitCell>
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
[checked]="selectedIds.has(r.id)"
|
||||
(change)="onCheckboxChange(r.id, $event)"
|
||||
/>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container>
|
||||
<span>{{ r.name }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ r.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span
|
||||
bitBadge
|
||||
*ngIf="passwordStrengthMap.has(r.id)"
|
||||
[variant]="passwordStrengthMap.get(r.id)[1]"
|
||||
>
|
||||
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
|
||||
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
|
||||
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right" data-testid="total-membership">
|
||||
{{ totalMembersMap.get(r.id) || 0 }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@ import { ActivatedRoute } from "@angular/router";
|
||||
import { debounceTime, map } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
|
||||
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
@@ -4,7 +4,7 @@ import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
|
||||
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
@@ -6,7 +6,7 @@ import { map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
|
||||
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
@@ -4,15 +4,15 @@ import { RouterModule, Routes } from "@angular/router";
|
||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import { AccessIntelligenceComponent } from "./access-intelligence.component";
|
||||
import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: AccessIntelligenceComponent,
|
||||
component: RiskInsightsComponent,
|
||||
canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)],
|
||||
data: {
|
||||
titleId: "accessIntelligence",
|
||||
titleId: "RiskInsights",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -21,4 +21,4 @@ const routes: Routes = [
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AccessIntelligenceRoutingModule {}
|
||||
export class RiskInsightsRoutingModule {}
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="tw-mb-1 text-primary" bitTypography="body1">{{ "accessIntelligence" | i18n }}</div>
|
||||
<div class="tw-mb-1 text-primary" bitTypography="body1">{{ "riskInsights" | i18n }}</div>
|
||||
<h1 bitTypography="h1">{{ "passwordRisk" | i18n }}</h1>
|
||||
<div class="tw-text-muted">{{ "discoverAtRiskPasswords" | i18n }}</div>
|
||||
<div class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-2 tw-my-4">
|
||||
@@ -15,7 +15,7 @@ import { PasswordHealthMembersURIComponent } from "./password-health-members-uri
|
||||
import { PasswordHealthMembersComponent } from "./password-health-members.component";
|
||||
import { PasswordHealthComponent } from "./password-health.component";
|
||||
|
||||
export enum AccessIntelligenceTabType {
|
||||
export enum RiskInsightsTabType {
|
||||
AllApps = 0,
|
||||
CriticalApps = 1,
|
||||
NotifiedMembers = 2,
|
||||
@@ -23,7 +23,7 @@ export enum AccessIntelligenceTabType {
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "./access-intelligence.component.html",
|
||||
templateUrl: "./risk-insights.component.html",
|
||||
imports: [
|
||||
AllApplicationsComponent,
|
||||
AsyncActionsModule,
|
||||
@@ -39,8 +39,8 @@ export enum AccessIntelligenceTabType {
|
||||
TabsModule,
|
||||
],
|
||||
})
|
||||
export class AccessIntelligenceComponent {
|
||||
tabIndex: AccessIntelligenceTabType;
|
||||
export class RiskInsightsComponent {
|
||||
tabIndex: RiskInsightsTabType;
|
||||
dataLastUpdated = new Date();
|
||||
|
||||
apps: any[] = [];
|
||||
@@ -70,7 +70,7 @@ export class AccessIntelligenceComponent {
|
||||
private router: Router,
|
||||
) {
|
||||
route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
|
||||
this.tabIndex = !isNaN(tabIndex) ? tabIndex : AccessIntelligenceTabType.AllApps;
|
||||
this.tabIndex = !isNaN(tabIndex) ? tabIndex : RiskInsightsTabType.AllApps;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { RiskInsightsRoutingModule } from "./risk-insights-routing.module";
|
||||
import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [RiskInsightsComponent, RiskInsightsRoutingModule],
|
||||
})
|
||||
export class RiskInsightsModule {}
|
||||
@@ -5,8 +5,8 @@
|
||||
"criticalApplications": {
|
||||
"message": "Critical applications"
|
||||
},
|
||||
"accessIntelligence": {
|
||||
"message": "Access Intelligence"
|
||||
"riskInsights": {
|
||||
"message": "Risk Insights"
|
||||
},
|
||||
"passwordRisk": {
|
||||
"message": "Password Risk"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Inject, Injectable } from "@angular/core";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mockCiphers } from "@bitwarden/bit-common/tools/reports/access-intelligence/services/ciphers.mock";
|
||||
import { mockCiphers } from "@bitwarden/bit-common/tools/reports/risk-insights/services/ciphers.mock";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mockMemberCipherDetailsResponse } from "@bitwarden/bit-common/tools/reports/access-intelligence/services/member-cipher-details-response.mock";
|
||||
import { mockMemberCipherDetailsResponse } from "@bitwarden/bit-common/tools/reports/risk-insights/services/member-cipher-details-response.mock";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
@@ -38,18 +38,20 @@
|
||||
<div class="box-content">
|
||||
<div
|
||||
class="environment-selector-dialog"
|
||||
data-testid="environment-selector-dialog"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<ng-container *ngFor="let region of availableRegions">
|
||||
<ng-container *ngFor="let region of availableRegions; let i = index">
|
||||
<button
|
||||
type="button"
|
||||
class="environment-selector-dialog-item"
|
||||
(click)="toggle(region.key)"
|
||||
[attr.aria-pressed]="data.selectedRegion === region ? 'true' : 'false'"
|
||||
[attr.data-testid]="'environment-selector-dialog-item-' + i"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
@@ -66,6 +68,7 @@
|
||||
class="environment-selector-dialog-item"
|
||||
(click)="toggle(ServerEnvironmentType.SelfHosted)"
|
||||
[attr.aria-pressed]="data.selectedRegion ? 'false' : 'true'"
|
||||
data-testid="environment-selector-dialog-item-self-hosted"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-sm bwi-check"
|
||||
|
||||
@@ -699,7 +699,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected deleteCipher() {
|
||||
const asAdmin = this.organization?.canEditAllCiphers;
|
||||
const asAdmin = this.organization?.canEditAllCiphers || !this.cipher.collectionIds;
|
||||
return this.cipher.isDeleted
|
||||
? this.cipherService.deleteWithServer(this.cipher.id, asAdmin)
|
||||
: this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom, Subject, take, takeUntil } from "rxjs";
|
||||
import { firstValueFrom, Subject, take, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -19,9 +19,11 @@ import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -139,12 +141,16 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private logService: LogService,
|
||||
private validationService: ValidationService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
this.loginViaAuthRequestSupported = this.loginComponentService.isLoginViaAuthRequestSupported();
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
// TODO: remove this when the UnauthenticatedExtensionUIRefresh feature flag is removed.
|
||||
this.listenForUnauthUiRefreshFlagChanges();
|
||||
|
||||
await this.defaultOnInit();
|
||||
|
||||
if (this.clientType === ClientType.Desktop) {
|
||||
@@ -162,6 +168,29 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private listenForUnauthUiRefreshFlagChanges() {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
|
||||
.pipe(
|
||||
tap(async (flag) => {
|
||||
// If the flag is turned OFF, we must force a reload to ensure the correct UI is shown
|
||||
if (!flag) {
|
||||
const uniqueQueryParams = {
|
||||
...this.activatedRoute.queryParams,
|
||||
// adding a unique timestamp to the query params to force a reload
|
||||
t: new Date().getTime().toString(), // Adding a unique timestamp as a query parameter
|
||||
};
|
||||
|
||||
await this.router.navigate(["/"], {
|
||||
queryParams: uniqueQueryParams,
|
||||
});
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
if (this.clientType === ClientType.Desktop) {
|
||||
if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<form [formGroup]="formGroup" *ngIf="!hideEnvSelector">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "creatingAccountOn" | i18n }}</bit-label>
|
||||
<bit-select formControlName="selectedRegion">
|
||||
<bit-select formControlName="selectedRegion" (closed)="onSelectClosed()">
|
||||
<bit-option
|
||||
*ngFor="let regionConfig of availableRegionConfigs"
|
||||
[value]="regionConfig"
|
||||
|
||||
@@ -109,6 +109,9 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for changes to the selected region and updates the form value and emits the selected region.
|
||||
*/
|
||||
private listenForSelectedRegionChanges() {
|
||||
this.selectedRegion.valueChanges
|
||||
.pipe(
|
||||
@@ -124,16 +127,12 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
if (selectedRegion === Region.SelfHosted) {
|
||||
return from(SelfHostedEnvConfigDialogComponent.open(this.dialogService)).pipe(
|
||||
tap((result: boolean | undefined) =>
|
||||
this.handleSelfHostedEnvConfigDialogResult(result, prevSelectedRegion),
|
||||
),
|
||||
);
|
||||
if (selectedRegion !== Region.SelfHosted) {
|
||||
this.selectedRegionChange.emit(selectedRegion);
|
||||
return from(this.environmentService.setEnvironment(selectedRegion.key));
|
||||
}
|
||||
|
||||
this.selectedRegionChange.emit(selectedRegion);
|
||||
return from(this.environmentService.setEnvironment(selectedRegion.key));
|
||||
return of(null);
|
||||
},
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
@@ -170,6 +169,17 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the event when the select is closed.
|
||||
* If the selected region is self-hosted, opens the self-hosted environment settings dialog.
|
||||
*/
|
||||
protected async onSelectClosed() {
|
||||
if (this.selectedRegion.value === Region.SelfHosted) {
|
||||
const result = await SelfHostedEnvConfigDialogComponent.open(this.dialogService);
|
||||
return this.handleSelfHostedEnvConfigDialogResult(result, this.selectedRegion.value);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
@@ -17,7 +17,6 @@ export enum FeatureFlag {
|
||||
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
||||
MemberAccessReport = "ac-2059-member-access-report",
|
||||
TwoFactorComponentRefactor = "two-factor-component-refactor",
|
||||
EnableTimeThreshold = "PM-5864-dollar-threshold",
|
||||
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
||||
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
|
||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
||||
@@ -63,7 +62,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
||||
[FeatureFlag.MemberAccessReport]: FALSE,
|
||||
[FeatureFlag.TwoFactorComponentRefactor]: FALSE,
|
||||
[FeatureFlag.EnableTimeThreshold]: FALSE,
|
||||
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
||||
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
||||
|
||||
@@ -1847,8 +1847,8 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
const [requestHeaders, requestBody] = await this.buildHeadersAndBody(
|
||||
authed,
|
||||
hasResponse,
|
||||
alterHeaders,
|
||||
body,
|
||||
alterHeaders,
|
||||
);
|
||||
|
||||
const requestInit: RequestInit = {
|
||||
|
||||
@@ -22,6 +22,7 @@ export type ObjectKey<State, Secret = State, Disclosed = Record<string, never>>
|
||||
classifier: Classifier<State, Disclosed, Secret>;
|
||||
format: "plain" | "classified";
|
||||
options: UserKeyDefinitionOptions<State>;
|
||||
initial?: State;
|
||||
};
|
||||
|
||||
export function isObjectKey(key: any): key is ObjectKey<unknown> {
|
||||
|
||||
@@ -254,17 +254,18 @@ export class UserStateSubject<
|
||||
withConstraints,
|
||||
map(([loadedState, constraints]) => {
|
||||
// bypass nulls
|
||||
if (!loadedState) {
|
||||
if (!loadedState && !this.objectKey?.initial) {
|
||||
return {
|
||||
constraints: {} as Constraints<State>,
|
||||
state: null,
|
||||
} satisfies Constrained<State>;
|
||||
}
|
||||
|
||||
const unconstrained = loadedState ?? structuredClone(this.objectKey.initial);
|
||||
const calibration = isDynamic(constraints)
|
||||
? constraints.calibrate(loadedState)
|
||||
? constraints.calibrate(unconstrained)
|
||||
: constraints;
|
||||
const adjusted = calibration.adjust(loadedState);
|
||||
const adjusted = calibration.adjust(unconstrained);
|
||||
|
||||
return {
|
||||
constraints: calibration.constraints,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(blur)="onBlur()"
|
||||
[labelForId]="labelForId"
|
||||
[clearable]="false"
|
||||
(close)="onClose()"
|
||||
appendTo="body"
|
||||
>
|
||||
<ng-template ng-option-tmp let-item="item">
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
QueryList,
|
||||
Self,
|
||||
ViewChild,
|
||||
Output,
|
||||
EventEmitter,
|
||||
} from "@angular/core";
|
||||
import { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
|
||||
import { NgSelectComponent } from "@ng-select/ng-select";
|
||||
@@ -31,6 +33,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
/** Optional: Options can be provided using an array input or using `bit-option` */
|
||||
@Input() items: Option<T>[] = [];
|
||||
@Input() placeholder = this.i18nService.t("selectPlaceholder");
|
||||
@Output() closed = new EventEmitter();
|
||||
|
||||
protected selectedValue: T;
|
||||
protected selectedOption: Option<T>;
|
||||
@@ -156,4 +159,9 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
private findSelectedOption(items: Option<T>[], value: T): Option<T> | undefined {
|
||||
return items.find((item) => item.value === value);
|
||||
}
|
||||
|
||||
/**Emits the closed event. */
|
||||
protected onClose() {
|
||||
this.closed.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ export const Table = (args) => (
|
||||
{Row("info-600")}
|
||||
{Row("info-700")}
|
||||
</tbody>
|
||||
<tbody>
|
||||
{Row("notification-100")}
|
||||
{Row("notification-600")}
|
||||
</tbody>
|
||||
<tbody>
|
||||
{Row("art-primary")}
|
||||
{Row("art-accent")}
|
||||
|
||||
@@ -59,6 +59,7 @@ export class ToggleComponent<TValue> implements AfterContentChecked {
|
||||
"tw-leading-5",
|
||||
"tw-transition",
|
||||
"tw-text-center",
|
||||
"tw-text-sm",
|
||||
"tw-border-primary-600",
|
||||
"!tw-text-primary-600",
|
||||
"tw-border-solid",
|
||||
@@ -85,7 +86,7 @@ export class ToggleComponent<TValue> implements AfterContentChecked {
|
||||
"peer-checked/toggle-input:tw-border-primary-600",
|
||||
"peer-checked/toggle-input:!tw-text-contrast",
|
||||
"tw-py-1.5",
|
||||
"tw-px-4",
|
||||
"tw-px-3",
|
||||
|
||||
// Fix for bootstrap styles that add bottom margin
|
||||
"!tw-mb-0",
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
--color-success-600: 12 128 24;
|
||||
--color-success-700: 11 111 21;
|
||||
|
||||
--color-notification-100: 255 225 247;
|
||||
--color-notification-600: 192 17 118;
|
||||
|
||||
--color-art-primary: 2 15 102;
|
||||
--color-art-accent: 44 221 223;
|
||||
|
||||
@@ -92,6 +95,9 @@
|
||||
--color-info-600: 121 161 233;
|
||||
--color-info-700: 219 229 246;
|
||||
|
||||
--color-notification-100: 117 37 83;
|
||||
--color-notification-600: 255 143 208;
|
||||
|
||||
--color-art-primary: 243 246 249;
|
||||
--color-art-accent: 44 221 233;
|
||||
|
||||
|
||||
@@ -58,6 +58,10 @@ module.exports = {
|
||||
600: rgba("--color-info-600"),
|
||||
700: rgba("--color-info-700"),
|
||||
},
|
||||
notification: {
|
||||
100: rgba("--color-notification-100"),
|
||||
600: rgba("--color-notification-600"),
|
||||
},
|
||||
art: {
|
||||
primary: rgba("--color-art-primary"),
|
||||
accent: rgba("--color-art-accent"),
|
||||
@@ -116,6 +120,9 @@ module.exports = {
|
||||
300: rgba("--color-secondary-300"),
|
||||
700: rgba("--color-secondary-700"),
|
||||
},
|
||||
notification: {
|
||||
600: rgba("--color-notification-600"),
|
||||
},
|
||||
},
|
||||
ringOffsetColor: ({ theme }) => ({
|
||||
DEFAULT: theme("colors.background"),
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<form class="box" [formGroup]="settings" class="tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "domainName" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="catchallDomain" type="text" />
|
||||
<input
|
||||
bitInput
|
||||
formControlName="catchallDomain"
|
||||
type="text"
|
||||
(change)="save('catchallDomain')"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, skip, Subject, takeUntil } from "rxjs";
|
||||
import { BehaviorSubject, map, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
/** Splits an email into a username, subaddress, and domain named group.
|
||||
* Subaddress is optional.
|
||||
*/
|
||||
export const DOMAIN_PARSER = new RegExp("[^@]+@(?<domain>.+)");
|
||||
|
||||
/** Options group for catchall emails */
|
||||
@Component({
|
||||
selector: "tools-catchall-settings",
|
||||
@@ -60,7 +65,19 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy {
|
||||
// the first emission is the current value; subsequent emissions are updates
|
||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||
|
||||
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||
// now that outputs are set up, connect inputs
|
||||
this.saveSettings
|
||||
.pipe(
|
||||
withLatestFrom(this.settings.valueChanges),
|
||||
map(([, settings]) => settings),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe(settings);
|
||||
}
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
private singleUserId$() {
|
||||
@@ -78,6 +95,7 @@ export class CatchallSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
buttonType="main"
|
||||
(click)="generate('user request')"
|
||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
>
|
||||
{{ credentialTypeGenerateLabel$ | async }}
|
||||
</button>
|
||||
@@ -33,16 +34,19 @@
|
||||
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
||||
[appCopyClick]="value$ | async"
|
||||
[valueLabel]="credentialTypeLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
></button>
|
||||
</div>
|
||||
</bit-card>
|
||||
<tools-password-settings
|
||||
#passwordSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'password'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
<tools-passphrase-settings
|
||||
#passphraseSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'passphrase'"
|
||||
[userId]="userId$ | async"
|
||||
@@ -80,21 +84,25 @@
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
#catchallSettings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'catchall'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('catchall settings')"
|
||||
/>
|
||||
<tools-forwarder-settings
|
||||
#forwarderSettings
|
||||
*ngIf="!!(forwarderId$ | async)"
|
||||
[forwarder]="forwarderId$ | async"
|
||||
[userId]="this.userId$ | async"
|
||||
/>
|
||||
<tools-subaddress-settings
|
||||
#subaddressSettings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'subaddress'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
<tools-username-settings
|
||||
#usernameSettings
|
||||
*ngIf="(showAlgorithm$ | async)?.id === 'username'"
|
||||
[userId]="userId$ | async"
|
||||
(onUpdated)="generate('username settings')"
|
||||
|
||||
@@ -202,9 +202,8 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
});
|
||||
|
||||
// normalize cascade selections; introduce subjects to allow changes
|
||||
// from user selections and changes from preference updates to
|
||||
// update the template
|
||||
// these subjects normalize cascade selections to ensure the current
|
||||
// cascade is always well-known.
|
||||
type CascadeValue = { nav: string; algorithm?: CredentialAlgorithm };
|
||||
const activeRoot$ = new Subject<CascadeValue>();
|
||||
const activeIdentifier$ = new Subject<CascadeValue>();
|
||||
@@ -385,7 +384,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
if (!a || a.onlyOnRequest) {
|
||||
this.value$.next("-");
|
||||
} else {
|
||||
this.generate("autogenerate");
|
||||
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -495,7 +494,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
* @param requestor a label used to trace generation request
|
||||
* origin in the debugger.
|
||||
*/
|
||||
protected generate(requestor: string) {
|
||||
protected async generate(requestor: string) {
|
||||
this.generate$.next(requestor);
|
||||
}
|
||||
|
||||
@@ -510,6 +509,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed = new Subject<void>();
|
||||
ngOnDestroy() {
|
||||
this.destroyed.next();
|
||||
this.destroyed.complete();
|
||||
|
||||
// finalize subjects
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
<form class="box" [formGroup]="settings" class="tw-container">
|
||||
<bit-form-field *ngIf="displayDomain">
|
||||
<bit-label>{{ "forwarderDomainName" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="domain" type="text" placeholder="example.com" />
|
||||
<input
|
||||
bitInput
|
||||
formControlName="domain"
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
(change)="save('domain')"
|
||||
/>
|
||||
<bit-hint>{{ "forwarderDomainNameHint" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="displayToken">
|
||||
<bit-label>{{ "apiKey" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="token" type="password" />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
(change)="save('token')"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="displayBaseUrl" disableMargin>
|
||||
<bit-label>{{ "selfHostBaseUrl" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="baseUrl" type="text" />
|
||||
<input bitInput formControlName="baseUrl" type="text" (change)="save('baseUrl')" />
|
||||
</bit-form-field>
|
||||
</form>
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
skip,
|
||||
Subject,
|
||||
switchAll,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
@@ -33,7 +32,7 @@ import {
|
||||
toCredentialGeneratorConfiguration,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch, toValidators } from "./util";
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
const Controls = Object.freeze({
|
||||
domain: "domain",
|
||||
@@ -117,35 +116,17 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
this.settings.patchValue(settings as any, { emitEvent: false });
|
||||
});
|
||||
|
||||
// bind policy to the reactive form
|
||||
forwarder$
|
||||
.pipe(
|
||||
switchMap((forwarder) => {
|
||||
const constraints$ = this.generatorService
|
||||
.policy$(forwarder, { userId$: singleUserId$ })
|
||||
.pipe(map(({ constraints }) => [constraints, forwarder] as const));
|
||||
|
||||
return constraints$;
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe(([constraints, forwarder]) => {
|
||||
for (const name in Controls) {
|
||||
const control = this.settings.get(name);
|
||||
if (forwarder.request.includes(name as any)) {
|
||||
control.enable({ emitEvent: false });
|
||||
control.setValidators(
|
||||
// the configuration's type erasure affects `toValidators` as well
|
||||
toValidators(name, forwarder, constraints),
|
||||
);
|
||||
} else {
|
||||
control.disable({ emitEvent: false });
|
||||
control.clearValidators();
|
||||
}
|
||||
// enable requested forwarder inputs
|
||||
forwarder$.pipe(takeUntil(this.destroyed$)).subscribe((forwarder) => {
|
||||
for (const name in Controls) {
|
||||
const control = this.settings.get(name);
|
||||
if (forwarder.request.includes(name as any)) {
|
||||
control.enable({ emitEvent: false });
|
||||
} else {
|
||||
control.disable({ emitEvent: false });
|
||||
}
|
||||
|
||||
this.settings.updateValueAndValidity({ emitEvent: false });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// the first emission is the current value; subsequent emissions are updates
|
||||
settings$$
|
||||
@@ -157,13 +138,18 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
.subscribe(this.onUpdated);
|
||||
|
||||
// now that outputs are set up, connect inputs
|
||||
this.settings.valueChanges
|
||||
.pipe(withLatestFrom(settings$$), takeUntil(this.destroyed$))
|
||||
.subscribe(([value, settings]) => {
|
||||
this.saveSettings
|
||||
.pipe(withLatestFrom(this.settings.valueChanges, settings$$), takeUntil(this.destroyed$))
|
||||
.subscribe(([, value, settings]) => {
|
||||
settings.next(value);
|
||||
});
|
||||
}
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.refresh$.complete();
|
||||
if ("forwarder" in changes) {
|
||||
@@ -192,6 +178,7 @@ export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
@@ -79,6 +80,7 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
||||
I18nService,
|
||||
EncryptService,
|
||||
KeyService,
|
||||
AccountService,
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
<bit-card>
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "numWords" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="numWords" id="num-words" type="number" />
|
||||
<input
|
||||
bitInput
|
||||
formControlName="numWords"
|
||||
id="num-words"
|
||||
type="number"
|
||||
(change)="save('numWords')"
|
||||
/>
|
||||
<bit-hint>{{ numWordsBoundariesHint$ | async }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</bit-card>
|
||||
@@ -16,14 +22,33 @@
|
||||
<bit-card>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "wordSeparator" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="wordSeparator" id="word-separator" type="text" />
|
||||
<input
|
||||
bitInput
|
||||
formControlName="wordSeparator"
|
||||
id="word-separator"
|
||||
type="text"
|
||||
[maxlength]="wordSeparatorMaxLength"
|
||||
(change)="save('wordSeparator')"
|
||||
/>
|
||||
</bit-form-field>
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="capitalize" id="capitalize" type="checkbox" />
|
||||
<input
|
||||
bitCheckbox
|
||||
formControlName="capitalize"
|
||||
id="capitalize"
|
||||
type="checkbox"
|
||||
(change)="save('capitalize')"
|
||||
/>
|
||||
<bit-label>{{ "capitalize" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control [disableMargin]="!policyInEffect">
|
||||
<input bitCheckbox formControlName="includeNumber" id="include-number" type="checkbox" />
|
||||
<input
|
||||
bitCheckbox
|
||||
formControlName="includeNumber"
|
||||
id="include-number"
|
||||
type="checkbox"
|
||||
(change)="save('includeNumber')"
|
||||
/>
|
||||
<bit-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<p *ngIf="policyInEffect" bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, skip, takeUntil, Subject, ReplaySubject } from "rxjs";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
skip,
|
||||
takeUntil,
|
||||
Subject,
|
||||
map,
|
||||
withLatestFrom,
|
||||
ReplaySubject,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -12,7 +20,7 @@ import {
|
||||
PassphraseGenerationOptions,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch, toValidators } from "./util";
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
const Controls = Object.freeze({
|
||||
numWords: "numWords",
|
||||
@@ -81,21 +89,12 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||
// the first emission is the current value; subsequent emissions are updates
|
||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||
|
||||
// dynamic policy enforcement
|
||||
// explain policy & disable policy-overridden fields
|
||||
this.generatorService
|
||||
.policy$(Generators.passphrase, { userId$: singleUserId$ })
|
||||
.pipe(takeUntil(this.destroyed$))
|
||||
.subscribe(({ constraints }) => {
|
||||
this.settings
|
||||
.get(Controls.numWords)
|
||||
.setValidators(toValidators(Controls.numWords, Generators.passphrase, constraints));
|
||||
|
||||
this.settings
|
||||
.get(Controls.wordSeparator)
|
||||
.setValidators(toValidators(Controls.wordSeparator, Generators.passphrase, constraints));
|
||||
|
||||
this.settings.updateValueAndValidity({ emitEvent: false });
|
||||
|
||||
this.wordSeparatorMaxLength = constraints.wordSeparator.maxLength;
|
||||
this.policyInEffect = constraints.policyInEffect;
|
||||
|
||||
this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly);
|
||||
@@ -110,7 +109,21 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
// now that outputs are set up, connect inputs
|
||||
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||
this.saveSettings
|
||||
.pipe(
|
||||
withLatestFrom(this.settings.valueChanges),
|
||||
map(([, settings]) => settings),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe(settings);
|
||||
}
|
||||
|
||||
/** attribute binding for wordSeparator[maxlength] */
|
||||
protected wordSeparatorMaxLength: number;
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
/** display binding for enterprise policy notice */
|
||||
@@ -144,6 +157,7 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
buttonType="main"
|
||||
(click)="generate('user request')"
|
||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
>
|
||||
{{ credentialTypeGenerateLabel$ | async }}
|
||||
</button>
|
||||
@@ -31,10 +32,12 @@
|
||||
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
||||
[appCopyClick]="value$ | async"
|
||||
[valueLabel]="credentialTypeLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
></button>
|
||||
</div>
|
||||
</bit-card>
|
||||
<tools-password-settings
|
||||
#passwordSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(algorithm$ | async)?.id === 'password'"
|
||||
[userId]="this.userId$ | async"
|
||||
@@ -42,6 +45,7 @@
|
||||
(onUpdated)="generate('password settings')"
|
||||
/>
|
||||
<tools-passphrase-settings
|
||||
#passphraseSettings
|
||||
class="tw-mt-6"
|
||||
*ngIf="(algorithm$ | async)?.id === 'passphrase'"
|
||||
[userId]="this.userId$ | async"
|
||||
|
||||
@@ -22,11 +22,11 @@ import { Option } from "@bitwarden/components/src/select/option";
|
||||
import {
|
||||
CredentialGeneratorService,
|
||||
Generators,
|
||||
PasswordAlgorithm,
|
||||
GeneratedCredential,
|
||||
CredentialAlgorithm,
|
||||
isPasswordAlgorithm,
|
||||
AlgorithmInfo,
|
||||
isSameAlgorithm,
|
||||
} from "@bitwarden/generator-core";
|
||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||
|
||||
@@ -57,7 +57,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
@Input({ transform: coerceBooleanProperty }) disableMargin = false;
|
||||
|
||||
/** tracks the currently selected credential type */
|
||||
protected credentialType$ = new BehaviorSubject<PasswordAlgorithm>(null);
|
||||
protected credentialType$ = new BehaviorSubject<CredentialAlgorithm>(null);
|
||||
|
||||
/** Emits the last generated value. */
|
||||
protected readonly value$ = new BehaviorSubject<string>("");
|
||||
@@ -72,14 +72,14 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
* @param requestor a label used to trace generation request
|
||||
* origin in the debugger.
|
||||
*/
|
||||
protected generate(requestor: string) {
|
||||
protected async generate(requestor: string) {
|
||||
this.generate$.next(requestor);
|
||||
}
|
||||
|
||||
/** Tracks changes to the selected credential type
|
||||
* @param type the new credential type
|
||||
*/
|
||||
protected onCredentialTypeChanged(type: PasswordAlgorithm) {
|
||||
protected onCredentialTypeChanged(type: CredentialAlgorithm) {
|
||||
// break subscription cycle
|
||||
if (this.credentialType$.value !== type) {
|
||||
this.zone.run(() => {
|
||||
@@ -169,29 +169,34 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
||||
preferences.next(preference);
|
||||
});
|
||||
|
||||
// populate the form with the user's preferences to kick off interactivity
|
||||
preferences.pipe(takeUntil(this.destroyed)).subscribe(({ password }) => {
|
||||
// update navigation
|
||||
this.onCredentialTypeChanged(password.algorithm);
|
||||
|
||||
// load algorithm metadata
|
||||
const algorithm = this.generatorService.algorithm(password.algorithm);
|
||||
|
||||
// update subjects within the angular zone so that the
|
||||
// template bindings refresh immediately
|
||||
this.zone.run(() => {
|
||||
this.algorithm$.next(algorithm);
|
||||
});
|
||||
});
|
||||
|
||||
// generate on load unless the generator prohibits it
|
||||
this.algorithm$
|
||||
// update active algorithm
|
||||
preferences
|
||||
.pipe(
|
||||
distinctUntilChanged((prev, next) => prev.id === next.id),
|
||||
filter((a) => !a.onlyOnRequest),
|
||||
map(({ password }) => this.generatorService.algorithm(password.algorithm)),
|
||||
distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)),
|
||||
takeUntil(this.destroyed),
|
||||
)
|
||||
.subscribe(() => this.generate("autogenerate"));
|
||||
.subscribe((algorithm) => {
|
||||
// update navigation
|
||||
this.onCredentialTypeChanged(algorithm.id);
|
||||
|
||||
// update subjects within the angular zone so that the
|
||||
// template bindings refresh immediately
|
||||
this.zone.run(() => {
|
||||
this.algorithm$.next(algorithm);
|
||||
});
|
||||
});
|
||||
|
||||
// generate on load unless the generator prohibits it
|
||||
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
||||
this.zone.run(() => {
|
||||
if (!a || a.onlyOnRequest) {
|
||||
this.value$.next("-");
|
||||
} else {
|
||||
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private typeToGenerator$(type: CredentialAlgorithm) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<bit-card>
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "length" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="length" type="number" />
|
||||
<input bitInput formControlName="length" type="number" (change)="save('length')" />
|
||||
<bit-hint>{{ lengthBoundariesHint$ | async }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</bit-card>
|
||||
@@ -21,7 +21,12 @@
|
||||
attr.aria-description="{{ 'uppercaseDescription' | i18n }}"
|
||||
title="{{ 'uppercaseDescription' | i18n }}"
|
||||
>
|
||||
<input bitCheckbox type="checkbox" formControlName="uppercase" />
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
formControlName="uppercase"
|
||||
(change)="save('uppercase')"
|
||||
/>
|
||||
<bit-label>{{ "uppercaseLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control
|
||||
@@ -29,7 +34,12 @@
|
||||
attr.aria-description="{{ 'lowercaseDescription' | i18n }}"
|
||||
title="{{ 'lowercaseDescription' | i18n }}"
|
||||
>
|
||||
<input bitCheckbox type="checkbox" formControlName="lowercase" />
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
formControlName="lowercase"
|
||||
(change)="save('lowercase')"
|
||||
/>
|
||||
<bit-label>{{ "lowercaseLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control
|
||||
@@ -37,7 +47,7 @@
|
||||
attr.aria-description="{{ 'numbersDescription' | i18n }}"
|
||||
title="{{ 'numbersDescription' | i18n }}"
|
||||
>
|
||||
<input bitCheckbox type="checkbox" formControlName="number" />
|
||||
<input bitCheckbox type="checkbox" formControlName="number" (change)="save('number')" />
|
||||
<bit-label>{{ "numbersLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control
|
||||
@@ -45,22 +55,42 @@
|
||||
attr.aria-description="{{ 'specialCharactersDescription' | i18n }}"
|
||||
title="{{ 'specialCharactersDescription' | i18n }}"
|
||||
>
|
||||
<input bitCheckbox type="checkbox" formControlName="special" />
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
formControlName="special"
|
||||
(change)="save('special')"
|
||||
/>
|
||||
<bit-label>{{ "specialCharactersLabel" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<div class="tw-flex">
|
||||
<bit-form-field class="tw-w-full tw-basis-1/2 tw-mr-4">
|
||||
<bit-label>{{ "minNumbers" | i18n }}</bit-label>
|
||||
<input bitInput type="number" formControlName="minNumber" />
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
formControlName="minNumber"
|
||||
(change)="save('minNumbers')"
|
||||
/>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-w-full tw-basis-1/2">
|
||||
<bit-label>{{ "minSpecial" | i18n }}</bit-label>
|
||||
<input bitInput type="number" formControlName="minSpecial" />
|
||||
<input
|
||||
bitInput
|
||||
type="number"
|
||||
formControlName="minSpecial"
|
||||
(change)="save('minSpecial')"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-control [disableMargin]="!policyInEffect">
|
||||
<input bitCheckbox type="checkbox" formControlName="avoidAmbiguous" />
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
formControlName="avoidAmbiguous"
|
||||
(change)="save('avoidAmbiguous')"
|
||||
/>
|
||||
<bit-label>{{ "avoidAmbiguous" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<p *ngIf="policyInEffect" bitTypography="helper">{{ "generatorPolicyInEffect" | i18n }}</p>
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, takeUntil, Subject, map, filter, tap, skip, ReplaySubject } from "rxjs";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
takeUntil,
|
||||
Subject,
|
||||
map,
|
||||
filter,
|
||||
tap,
|
||||
skip,
|
||||
ReplaySubject,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -12,7 +22,7 @@ import {
|
||||
PasswordGenerationOptions,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { completeOnAccountSwitch, toValidators } from "./util";
|
||||
import { completeOnAccountSwitch } from "./util";
|
||||
|
||||
const Controls = Object.freeze({
|
||||
length: "length",
|
||||
@@ -118,23 +128,11 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
this.settings.patchValue(s, { emitEvent: false });
|
||||
});
|
||||
|
||||
// bind policy to the template
|
||||
// explain policy & disable policy-overridden fields
|
||||
this.generatorService
|
||||
.policy$(Generators.password, { userId$: singleUserId$ })
|
||||
.pipe(takeUntil(this.destroyed$))
|
||||
.subscribe(({ constraints }) => {
|
||||
this.settings
|
||||
.get(Controls.length)
|
||||
.setValidators(toValidators(Controls.length, Generators.password, constraints));
|
||||
|
||||
this.minNumber.setValidators(
|
||||
toValidators(Controls.minNumber, Generators.password, constraints),
|
||||
);
|
||||
|
||||
this.minSpecial.setValidators(
|
||||
toValidators(Controls.minSpecial, Generators.password, constraints),
|
||||
);
|
||||
|
||||
this.policyInEffect = constraints.policyInEffect;
|
||||
|
||||
const toggles = [
|
||||
@@ -153,8 +151,8 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
const boundariesHint = this.i18nService.t(
|
||||
"generatorBoundariesHint",
|
||||
constraints.length.min,
|
||||
constraints.length.max,
|
||||
constraints.length.min?.toString(),
|
||||
constraints.length.max?.toString(),
|
||||
);
|
||||
this.lengthBoundariesHint.next(boundariesHint);
|
||||
});
|
||||
@@ -201,9 +199,10 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||
|
||||
// now that outputs are set up, connect inputs
|
||||
this.settings.valueChanges
|
||||
this.saveSettings
|
||||
.pipe(
|
||||
map((settings) => {
|
||||
withLatestFrom(this.settings.valueChanges),
|
||||
map(([, settings]) => {
|
||||
// interface is "avoid" while storage is "include"
|
||||
const s: any = { ...settings };
|
||||
s.ambiguous = s.avoidAmbiguous;
|
||||
@@ -215,6 +214,11 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
.subscribe(settings);
|
||||
}
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
/** display binding for enterprise policy notice */
|
||||
protected policyInEffect: boolean;
|
||||
|
||||
@@ -246,6 +250,7 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<form class="box" [formGroup]="settings" class="tw-container">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="subaddressEmail" type="text" />
|
||||
<input
|
||||
bitInput
|
||||
formControlName="subaddressEmail"
|
||||
type="text"
|
||||
(change)="save('subaddressEmail')"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
|
||||
@@ -53,28 +53,25 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy {
|
||||
const singleUserId$ = this.singleUserId$();
|
||||
const settings = await this.generatorService.settings(Generators.subaddress, { singleUserId$ });
|
||||
|
||||
settings
|
||||
.pipe(
|
||||
withLatestFrom(this.accountService.activeAccount$),
|
||||
map(([settings, activeAccount]) => {
|
||||
// if the subaddress isn't specified, copy it from
|
||||
// the user's settings
|
||||
if ((settings.subaddressEmail ?? "").length < 1) {
|
||||
settings.subaddressEmail = activeAccount.email;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe((s) => {
|
||||
this.settings.patchValue(s, { emitEvent: false });
|
||||
});
|
||||
settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => {
|
||||
this.settings.patchValue(s, { emitEvent: false });
|
||||
});
|
||||
|
||||
// the first emission is the current value; subsequent emissions are updates
|
||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||
|
||||
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||
this.saveSettings
|
||||
.pipe(
|
||||
withLatestFrom(this.settings.valueChanges),
|
||||
map(([, settings]) => settings),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe(settings);
|
||||
}
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
private singleUserId$() {
|
||||
@@ -92,6 +89,7 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
buttonType="main"
|
||||
(click)="generate('user request')"
|
||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
>
|
||||
{{ credentialTypeGenerateLabel$ | async }}
|
||||
</button>
|
||||
@@ -20,6 +21,7 @@
|
||||
[appA11yTitle]="credentialTypeCopyLabel$ | async"
|
||||
[appCopyClick]="value$ | async"
|
||||
[valueLabel]="credentialTypeLabel$ | async"
|
||||
[disabled]="!(algorithm$ | async)"
|
||||
>
|
||||
{{ credentialTypeCopyLabel$ | async }}
|
||||
</button>
|
||||
@@ -57,21 +59,25 @@
|
||||
</bit-form-field>
|
||||
</form>
|
||||
<tools-catchall-settings
|
||||
#catchallSettings
|
||||
*ngIf="(algorithm$ | async)?.id === 'catchall'"
|
||||
[userId]="this.userId$ | async"
|
||||
(onUpdated)="generate('catchall settings')"
|
||||
/>
|
||||
<tools-forwarder-settings
|
||||
#forwarderSettings
|
||||
*ngIf="!!(forwarderId$ | async)"
|
||||
[forwarder]="forwarderId$ | async"
|
||||
[userId]="this.userId$ | async"
|
||||
/>
|
||||
<tools-subaddress-settings
|
||||
#subaddressSettings
|
||||
*ngIf="(algorithm$ | async)?.id === 'subaddress'"
|
||||
[userId]="this.userId$ | async"
|
||||
(onUpdated)="generate('subaddress settings')"
|
||||
/>
|
||||
<tools-username-settings
|
||||
#usernameSettings
|
||||
*ngIf="(algorithm$ | async)?.id === 'username'"
|
||||
[userId]="this.userId$ | async"
|
||||
(onUpdated)="generate('username settings')"
|
||||
|
||||
@@ -322,7 +322,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
if (!a || a.onlyOnRequest) {
|
||||
this.value$.next("-");
|
||||
} else {
|
||||
this.generate("autogenerate");
|
||||
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -414,7 +414,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
* @param requestor a label used to trace generation request
|
||||
* origin in the debugger.
|
||||
*/
|
||||
protected generate(requestor: string) {
|
||||
protected async generate(requestor: string) {
|
||||
this.generate$.next(requestor);
|
||||
}
|
||||
|
||||
@@ -429,6 +429,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed = new Subject<void>();
|
||||
ngOnDestroy() {
|
||||
this.destroyed.next();
|
||||
this.destroyed.complete();
|
||||
|
||||
// finalize subjects
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
<form class="box" [formGroup]="settings" class="tw-container">
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="wordCapitalize" type="checkbox" />
|
||||
<input
|
||||
bitCheckbox
|
||||
formControlName="wordCapitalize"
|
||||
type="checkbox"
|
||||
(change)="save('wordCapitalize')"
|
||||
/>
|
||||
<bit-label>{{ "capitalize" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="wordIncludeNumber" type="checkbox" />
|
||||
<input
|
||||
bitCheckbox
|
||||
formControlName="wordIncludeNumber"
|
||||
type="checkbox"
|
||||
(change)="save('wordIncludeNumber')"
|
||||
/>
|
||||
<bit-label>{{ "includeNumber" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, skip, Subject, takeUntil } from "rxjs";
|
||||
import { BehaviorSubject, map, skip, Subject, takeUntil, withLatestFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -61,7 +61,18 @@ export class UsernameSettingsComponent implements OnInit, OnDestroy {
|
||||
// the first emission is the current value; subsequent emissions are updates
|
||||
settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated);
|
||||
|
||||
this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings);
|
||||
this.saveSettings
|
||||
.pipe(
|
||||
withLatestFrom(this.settings.valueChanges),
|
||||
map(([, settings]) => settings),
|
||||
takeUntil(this.destroyed$),
|
||||
)
|
||||
.subscribe(settings);
|
||||
}
|
||||
|
||||
private saveSettings = new Subject<string>();
|
||||
save(site: string = "component api call") {
|
||||
this.saveSettings.next(site);
|
||||
}
|
||||
|
||||
private singleUserId$() {
|
||||
@@ -79,6 +90,7 @@ export class UsernameSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
private readonly destroyed$ = new Subject<void>();
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function toValidators<Policy, Settings>(
|
||||
}
|
||||
|
||||
const max = getConstraint("max", config, runtime);
|
||||
if (max === undefined) {
|
||||
if (max !== undefined) {
|
||||
validators.push(Validators.max(max));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { GENERATOR_DISK } from "@bitwarden/common/platform/state";
|
||||
import { ApiSettings } from "@bitwarden/common/tools/integration/rpc";
|
||||
import { PublicClassifier } from "@bitwarden/common/tools/public-classifier";
|
||||
import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint";
|
||||
import { ObjectKey } from "@bitwarden/common/tools/state/object-key";
|
||||
|
||||
import {
|
||||
EmailRandomizer,
|
||||
@@ -19,12 +22,12 @@ import {
|
||||
PasswordGeneratorOptionsEvaluator,
|
||||
passwordLeastPrivilege,
|
||||
} from "../policies";
|
||||
import { CatchallConstraints } from "../policies/catchall-constraints";
|
||||
import { SubaddressConstraints } from "../policies/subaddress-constraints";
|
||||
import {
|
||||
CATCHALL_SETTINGS,
|
||||
EFF_USERNAME_SETTINGS,
|
||||
PASSPHRASE_SETTINGS,
|
||||
PASSWORD_SETTINGS,
|
||||
SUBADDRESS_SETTINGS,
|
||||
} from "../strategies/storage";
|
||||
import {
|
||||
CatchallGenerationOptions,
|
||||
@@ -178,79 +181,115 @@ const USERNAME = Object.freeze({
|
||||
},
|
||||
} satisfies CredentialGeneratorConfiguration<EffUsernameGenerationOptions, NoPolicy>);
|
||||
|
||||
const CATCHALL = Object.freeze({
|
||||
id: "catchall",
|
||||
category: "email",
|
||||
nameKey: "catchallEmail",
|
||||
descriptionKey: "catchallEmailDesc",
|
||||
generateKey: "generateEmail",
|
||||
generatedValueKey: "email",
|
||||
copyKey: "copyEmail",
|
||||
onlyOnRequest: false,
|
||||
request: [],
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<CatchallGenerationOptions> {
|
||||
return new EmailRandomizer(dependencies.randomizer);
|
||||
const CATCHALL: CredentialGeneratorConfiguration<CatchallGenerationOptions, NoPolicy> =
|
||||
Object.freeze({
|
||||
id: "catchall",
|
||||
category: "email",
|
||||
nameKey: "catchallEmail",
|
||||
descriptionKey: "catchallEmailDesc",
|
||||
generateKey: "generateEmail",
|
||||
generatedValueKey: "email",
|
||||
copyKey: "copyEmail",
|
||||
onlyOnRequest: false,
|
||||
request: [],
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<CatchallGenerationOptions> {
|
||||
return new EmailRandomizer(dependencies.randomizer);
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultCatchallOptions,
|
||||
constraints: { catchallDomain: { minLength: 1 } },
|
||||
account: CATCHALL_SETTINGS,
|
||||
},
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {},
|
||||
combine(_acc: NoPolicy, _policy: Policy) {
|
||||
return {};
|
||||
settings: {
|
||||
initial: DefaultCatchallOptions,
|
||||
constraints: { catchallDomain: { minLength: 1 } },
|
||||
account: {
|
||||
key: "catchallGeneratorSettings",
|
||||
target: "object",
|
||||
format: "plain",
|
||||
classifier: new PublicClassifier<CatchallGenerationOptions>([
|
||||
"catchallType",
|
||||
"catchallDomain",
|
||||
]),
|
||||
state: GENERATOR_DISK,
|
||||
initial: {
|
||||
catchallType: "random",
|
||||
catchallDomain: "",
|
||||
},
|
||||
options: {
|
||||
deserializer: (value) => value,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
} satisfies ObjectKey<CatchallGenerationOptions>,
|
||||
},
|
||||
createEvaluator(_policy: NoPolicy) {
|
||||
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {},
|
||||
combine(_acc: NoPolicy, _policy: Policy) {
|
||||
return {};
|
||||
},
|
||||
createEvaluator(_policy: NoPolicy) {
|
||||
return new DefaultPolicyEvaluator<CatchallGenerationOptions>();
|
||||
},
|
||||
toConstraints(_policy: NoPolicy, email: string) {
|
||||
return new CatchallConstraints(email);
|
||||
},
|
||||
},
|
||||
toConstraints(_policy: NoPolicy) {
|
||||
return new IdentityConstraint<CatchallGenerationOptions>();
|
||||
},
|
||||
},
|
||||
} satisfies CredentialGeneratorConfiguration<CatchallGenerationOptions, NoPolicy>);
|
||||
});
|
||||
|
||||
const SUBADDRESS = Object.freeze({
|
||||
id: "subaddress",
|
||||
category: "email",
|
||||
nameKey: "plusAddressedEmail",
|
||||
descriptionKey: "plusAddressedEmailDesc",
|
||||
generateKey: "generateEmail",
|
||||
generatedValueKey: "email",
|
||||
copyKey: "copyEmail",
|
||||
onlyOnRequest: false,
|
||||
request: [],
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<SubaddressGenerationOptions> {
|
||||
return new EmailRandomizer(dependencies.randomizer);
|
||||
const SUBADDRESS: CredentialGeneratorConfiguration<SubaddressGenerationOptions, NoPolicy> =
|
||||
Object.freeze({
|
||||
id: "subaddress",
|
||||
category: "email",
|
||||
nameKey: "plusAddressedEmail",
|
||||
descriptionKey: "plusAddressedEmailDesc",
|
||||
generateKey: "generateEmail",
|
||||
generatedValueKey: "email",
|
||||
copyKey: "copyEmail",
|
||||
onlyOnRequest: false,
|
||||
request: [],
|
||||
engine: {
|
||||
create(
|
||||
dependencies: GeneratorDependencyProvider,
|
||||
): CredentialGenerator<SubaddressGenerationOptions> {
|
||||
return new EmailRandomizer(dependencies.randomizer);
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
initial: DefaultSubaddressOptions,
|
||||
constraints: {},
|
||||
account: SUBADDRESS_SETTINGS,
|
||||
},
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {},
|
||||
combine(_acc: NoPolicy, _policy: Policy) {
|
||||
return {};
|
||||
settings: {
|
||||
initial: DefaultSubaddressOptions,
|
||||
constraints: {},
|
||||
account: {
|
||||
key: "subaddressGeneratorSettings",
|
||||
target: "object",
|
||||
format: "plain",
|
||||
classifier: new PublicClassifier<SubaddressGenerationOptions>([
|
||||
"subaddressType",
|
||||
"subaddressEmail",
|
||||
]),
|
||||
state: GENERATOR_DISK,
|
||||
initial: {
|
||||
subaddressType: "random",
|
||||
subaddressEmail: "",
|
||||
},
|
||||
options: {
|
||||
deserializer: (value) => value,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
} satisfies ObjectKey<SubaddressGenerationOptions>,
|
||||
},
|
||||
createEvaluator(_policy: NoPolicy) {
|
||||
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
||||
policy: {
|
||||
type: PolicyType.PasswordGenerator,
|
||||
disabledValue: {},
|
||||
combine(_acc: NoPolicy, _policy: Policy) {
|
||||
return {};
|
||||
},
|
||||
createEvaluator(_policy: NoPolicy) {
|
||||
return new DefaultPolicyEvaluator<SubaddressGenerationOptions>();
|
||||
},
|
||||
toConstraints(_policy: NoPolicy, email: string) {
|
||||
return new SubaddressConstraints(email);
|
||||
},
|
||||
},
|
||||
toConstraints(_policy: NoPolicy) {
|
||||
return new IdentityConstraint<SubaddressGenerationOptions>();
|
||||
},
|
||||
},
|
||||
} satisfies CredentialGeneratorConfiguration<SubaddressGenerationOptions, NoPolicy>);
|
||||
});
|
||||
|
||||
export function toCredentialGeneratorConfiguration<Settings extends ApiSettings = ApiSettings>(
|
||||
configuration: ForwarderConfiguration<Settings>,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Constraints, StateConstraints } from "@bitwarden/common/tools/types";
|
||||
|
||||
import { CatchallGenerationOptions } from "../types";
|
||||
|
||||
/** Parses the domain part of an email address
|
||||
*/
|
||||
const DOMAIN_PARSER = new RegExp("[^@]+@(?<domain>.+)");
|
||||
|
||||
/** A constraint that sets the catchall domain using a fixed email address */
|
||||
export class CatchallConstraints implements StateConstraints<CatchallGenerationOptions> {
|
||||
/** Creates a catchall constraints
|
||||
* @param email - the email address containing the domain.
|
||||
*/
|
||||
constructor(email: string) {
|
||||
if (!email) {
|
||||
this.domain = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = DOMAIN_PARSER.exec(email);
|
||||
if (parsed && parsed.groups?.domain) {
|
||||
this.domain = parsed.groups.domain;
|
||||
}
|
||||
}
|
||||
private domain: string;
|
||||
|
||||
constraints: Readonly<Constraints<CatchallGenerationOptions>> = {};
|
||||
|
||||
adjust(state: CatchallGenerationOptions) {
|
||||
const currentDomain = (state.catchallDomain ?? "").trim();
|
||||
|
||||
if (currentDomain !== "") {
|
||||
return state;
|
||||
}
|
||||
|
||||
const options = { ...state };
|
||||
options.catchallDomain = this.domain;
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
fix(state: CatchallGenerationOptions) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Constraints, StateConstraints } from "@bitwarden/common/tools/types";
|
||||
|
||||
import { SubaddressGenerationOptions } from "../types";
|
||||
|
||||
/** A constraint that sets the subaddress email using a fixed email address */
|
||||
export class SubaddressConstraints implements StateConstraints<SubaddressGenerationOptions> {
|
||||
/** Creates a catchall constraints
|
||||
* @param email - the email address containing the domain.
|
||||
*/
|
||||
constructor(readonly email: string) {
|
||||
if (!email) {
|
||||
this.email = "";
|
||||
}
|
||||
}
|
||||
|
||||
constraints: Readonly<Constraints<SubaddressGenerationOptions>> = {};
|
||||
|
||||
adjust(state: SubaddressGenerationOptions) {
|
||||
const currentDomain = (state.subaddressEmail ?? "").trim();
|
||||
|
||||
if (currentDomain !== "") {
|
||||
return state;
|
||||
}
|
||||
|
||||
const options = { ...state };
|
||||
options.subaddressEmail = this.email;
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
fix(state: SubaddressGenerationOptions) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,12 @@ export function mapPolicyToEvaluator<Policy, Evaluator>(
|
||||
*/
|
||||
export function mapPolicyToConstraints<Policy, Evaluator>(
|
||||
configuration: PolicyConfiguration<Policy, Evaluator>,
|
||||
email: string,
|
||||
) {
|
||||
return pipe(
|
||||
reduceCollection(configuration.combine, configuration.disabledValue),
|
||||
distinctIfShallowMatch(),
|
||||
map(configuration.toConstraints),
|
||||
map((policy) => configuration.toConstraints(policy, email)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||
|
||||
@@ -223,6 +224,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||
|
||||
@@ -248,6 +250,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||
|
||||
@@ -276,6 +279,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const website$ = new BehaviorSubject("some website");
|
||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { website$ }));
|
||||
@@ -297,6 +301,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const website$ = new BehaviorSubject("some website");
|
||||
let error = null;
|
||||
@@ -322,6 +327,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const website$ = new BehaviorSubject("some website");
|
||||
let completed = false;
|
||||
@@ -348,6 +354,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ }));
|
||||
@@ -368,6 +375,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.pipe(filter((u) => !!u));
|
||||
@@ -392,6 +400,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId$ = new BehaviorSubject(SomeUser);
|
||||
let error = null;
|
||||
@@ -417,6 +426,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId$ = new BehaviorSubject(SomeUser);
|
||||
let completed = false;
|
||||
@@ -443,6 +453,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const on$ = new Subject<void>();
|
||||
const results: any[] = [];
|
||||
@@ -485,6 +496,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const on$ = new Subject<void>();
|
||||
let error: any = null;
|
||||
@@ -511,6 +523,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const on$ = new Subject<void>();
|
||||
let complete = false;
|
||||
@@ -542,6 +555,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = generator.algorithms("password");
|
||||
@@ -563,6 +577,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = generator.algorithms("username");
|
||||
@@ -583,6 +598,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = generator.algorithms("email");
|
||||
@@ -604,6 +620,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = generator.algorithms(["username", "email"]);
|
||||
@@ -629,6 +646,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(generator.algorithms$("password"));
|
||||
@@ -646,6 +664,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(generator.algorithms$("username"));
|
||||
@@ -662,6 +681,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(generator.algorithms$("email"));
|
||||
@@ -679,6 +699,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(generator.algorithms$(["username", "email"]));
|
||||
@@ -701,6 +722,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(generator.algorithms$(["password"]));
|
||||
@@ -726,6 +748,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const results: any = [];
|
||||
const sub = generator.algorithms$("password").subscribe((r) => results.push(r));
|
||||
@@ -763,6 +786,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||
|
||||
@@ -784,6 +808,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
@@ -814,6 +839,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
@@ -840,6 +866,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
@@ -866,6 +893,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
@@ -898,6 +926,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||
@@ -916,6 +945,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||
@@ -936,6 +966,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
|
||||
@@ -961,6 +992,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const results: any = [];
|
||||
const sub = generator.settings$(SomeConfiguration).subscribe((r) => results.push(r));
|
||||
@@ -986,6 +1018,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||
|
||||
@@ -1007,6 +1040,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
@@ -1034,6 +1068,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
@@ -1060,6 +1095,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
@@ -1086,6 +1122,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
@@ -1118,6 +1155,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const subject = await generator.settings(SomeConfiguration, { singleUserId$ });
|
||||
|
||||
@@ -1139,6 +1177,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
let completed = false;
|
||||
@@ -1165,6 +1204,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||
|
||||
@@ -1182,6 +1222,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||
const policy$ = new BehaviorSubject([somePolicy]);
|
||||
@@ -1201,6 +1242,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
@@ -1230,6 +1272,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
@@ -1260,6 +1303,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
@@ -1286,6 +1330,7 @@ describe("CredentialGeneratorService", () => {
|
||||
i18nService,
|
||||
encryptService,
|
||||
keyService,
|
||||
accountService,
|
||||
);
|
||||
const userId = new BehaviorSubject(SomeUser);
|
||||
const userId$ = userId.asObservable();
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Simplify } from "type-fest";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
@@ -98,6 +99,7 @@ export class CredentialGeneratorService {
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly encryptService: EncryptService,
|
||||
private readonly keyService: KeyService,
|
||||
private readonly accountService: AccountService,
|
||||
) {}
|
||||
|
||||
private getDependencyProvider(): GeneratorDependencyProvider {
|
||||
@@ -380,17 +382,30 @@ export class CredentialGeneratorService {
|
||||
configuration: Configuration<Settings, Policy>,
|
||||
dependencies: Policy$Dependencies,
|
||||
): Observable<GeneratorConstraints<Settings>> {
|
||||
const completion$ = dependencies.userId$.pipe(ignoreElements(), endWith(true));
|
||||
const email$ = dependencies.userId$.pipe(
|
||||
distinctUntilChanged(),
|
||||
withLatestFrom(this.accountService.accounts$),
|
||||
filter((accounts) => !!accounts),
|
||||
map(([userId, accounts]) => {
|
||||
if (userId in accounts) {
|
||||
return { userId, email: accounts[userId].email };
|
||||
}
|
||||
|
||||
const constraints$ = dependencies.userId$.pipe(
|
||||
switchMap((userId) => {
|
||||
// complete policy emissions otherwise `mergeMap` holds `policies$` open indefinitely
|
||||
return { userId, email: null };
|
||||
}),
|
||||
);
|
||||
|
||||
const constraints$ = email$.pipe(
|
||||
switchMap(({ userId, email }) => {
|
||||
// complete policy emissions otherwise `switchMap` holds `policies$` open indefinitely
|
||||
const policies$ = this.policyService
|
||||
.getAll$(configuration.policy.type, userId)
|
||||
.pipe(takeUntil(completion$));
|
||||
.pipe(
|
||||
mapPolicyToConstraints(configuration.policy, email),
|
||||
takeUntil(anyComplete(email$)),
|
||||
);
|
||||
return policies$;
|
||||
}),
|
||||
mapPolicyToConstraints(configuration.policy),
|
||||
);
|
||||
|
||||
return constraints$;
|
||||
|
||||
@@ -24,9 +24,13 @@ export type PolicyConfiguration<Policy, Settings> = {
|
||||
createEvaluator: (policy: Policy) => PolicyEvaluator<Policy, Settings>;
|
||||
|
||||
/** Converts policy service data into actionable policy constraints.
|
||||
*
|
||||
* @param policy - the policy to map into policy constraints.
|
||||
* @param email - the default email to extend.
|
||||
*
|
||||
* @remarks this version includes constraints needed for the reactive forms;
|
||||
* it was introduced so that the constraints can be incrementally introduced
|
||||
* as the new UI is built.
|
||||
*/
|
||||
toConstraints: (policy: Policy) => GeneratorConstraints<Settings>;
|
||||
toConstraints: (policy: Policy, email: string) => GeneratorConstraints<Settings>;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
@@ -43,6 +44,7 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
||||
I18nService,
|
||||
EncryptService,
|
||||
KeyService,
|
||||
AccountService,
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
[value]="totpCodeCopyObj?.totpCodeFormatted || '*** ***'"
|
||||
aria-readonly="true"
|
||||
data-testid="login-totp"
|
||||
class="tw-font-mono"
|
||||
/>
|
||||
<div
|
||||
*ngIf="isPremium$ | async"
|
||||
|
||||
Reference in New Issue
Block a user