mirror of
https://github.com/bitwarden/browser
synced 2026-02-15 07:54:55 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -107,6 +107,9 @@
|
||||
"copyPassword": {
|
||||
"message": "Copy password"
|
||||
},
|
||||
"copyPassphrase": {
|
||||
"message": "Copy passphrase"
|
||||
},
|
||||
"copyNote": {
|
||||
"message": "Copy note"
|
||||
},
|
||||
@@ -407,6 +410,9 @@
|
||||
"generatePassword": {
|
||||
"message": "Generate password"
|
||||
},
|
||||
"generatePassphrase": {
|
||||
"message": "Generate passphrase"
|
||||
},
|
||||
"regeneratePassword": {
|
||||
"message": "Regenerate password"
|
||||
},
|
||||
@@ -1389,6 +1395,10 @@
|
||||
"baseUrl": {
|
||||
"message": "Server URL"
|
||||
},
|
||||
"selfHostBaseUrl": {
|
||||
"message": "Self-host server URL",
|
||||
"description": "Label for field requesting a self-hosted integration service URL"
|
||||
},
|
||||
"apiUrl": {
|
||||
"message": "API server URL"
|
||||
},
|
||||
@@ -2827,6 +2837,9 @@
|
||||
"generateUsername": {
|
||||
"message": "Generate username"
|
||||
},
|
||||
"generateEmail": {
|
||||
"message": "Generate email"
|
||||
},
|
||||
"usernameType": {
|
||||
"message": "Username type"
|
||||
},
|
||||
@@ -2867,6 +2880,14 @@
|
||||
"forwardedEmailDesc": {
|
||||
"message": "Generate an email alias with an external forwarding service."
|
||||
},
|
||||
"forwarderDomainName": {
|
||||
"message": "Email domain",
|
||||
"description": "Labels the domain name email forwarder service option"
|
||||
},
|
||||
"forwarderDomainNameHint": {
|
||||
"message": "Choose a domain that is supported by the selected service",
|
||||
"description": "Guidance provided for email forwarding services that support multiple email domains."
|
||||
},
|
||||
"forwarderError": {
|
||||
"message": "$SERVICENAME$ error: $ERRORMESSAGE$",
|
||||
"description": "Reports an error returned by a forwarding service to the user.",
|
||||
|
||||
@@ -75,6 +75,8 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { ProcessReloadService } from "@bitwarden/common/key-management/services/process-reload.service";
|
||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -270,6 +272,7 @@ import CommandsBackground from "./commands.background";
|
||||
import IdleBackground from "./idle.background";
|
||||
import { NativeMessagingBackground } from "./nativeMessaging.background";
|
||||
import RuntimeBackground from "./runtime.background";
|
||||
|
||||
export default class MainBackground {
|
||||
messagingService: MessageSender;
|
||||
storageService: BrowserLocalStorageService;
|
||||
@@ -314,6 +317,7 @@ export default class MainBackground {
|
||||
badgeSettingsService: BadgeSettingsServiceAbstraction;
|
||||
domainSettingsService: DomainSettingsService;
|
||||
systemService: SystemServiceAbstraction;
|
||||
processReloadService: ProcessReloadServiceAbstraction;
|
||||
eventCollectionService: EventCollectionServiceAbstraction;
|
||||
eventUploadService: EventUploadServiceAbstraction;
|
||||
policyService: InternalPolicyServiceAbstraction;
|
||||
@@ -408,7 +412,7 @@ export default class MainBackground {
|
||||
await this.refreshMenu(true);
|
||||
if (this.systemService != null) {
|
||||
await this.systemService.clearPendingClipboard();
|
||||
await this.systemService.startProcessReload(this.authService);
|
||||
await this.processReloadService.startProcessReload(this.authService);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1088,15 +1092,18 @@ export default class MainBackground {
|
||||
};
|
||||
|
||||
this.systemService = new SystemService(
|
||||
this.platformUtilsService,
|
||||
this.autofillSettingsService,
|
||||
this.taskSchedulerService,
|
||||
);
|
||||
|
||||
this.processReloadService = new ProcessReloadService(
|
||||
this.pinService,
|
||||
this.messagingService,
|
||||
this.platformUtilsService,
|
||||
systemUtilsServiceReloadCallback,
|
||||
this.autofillSettingsService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.biometricStateService,
|
||||
this.accountService,
|
||||
this.taskSchedulerService,
|
||||
);
|
||||
|
||||
// Other fields
|
||||
@@ -1122,7 +1129,7 @@ export default class MainBackground {
|
||||
this.platformUtilsService as BrowserPlatformUtilsService,
|
||||
this.notificationsService,
|
||||
this.autofillSettingsService,
|
||||
this.systemService,
|
||||
this.processReloadService,
|
||||
this.environmentService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
@@ -1551,7 +1558,7 @@ export default class MainBackground {
|
||||
await this.mainContextMenuHandler?.noAccess();
|
||||
await this.notificationsService.updateConnection(false);
|
||||
await this.systemService.clearPendingClipboard();
|
||||
await this.systemService.startProcessReload(this.authService);
|
||||
await this.processReloadService.startProcessReload(this.authService);
|
||||
}
|
||||
|
||||
private async needsStorageReseed(userId: UserId): Promise<boolean> {
|
||||
|
||||
@@ -6,10 +6,10 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { AutofillOverlayVisibility, ExtensionCommand } from "@bitwarden/common/autofill/constants";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -40,7 +40,7 @@ export default class RuntimeBackground {
|
||||
private platformUtilsService: BrowserPlatformUtilsService,
|
||||
private notificationsService: NotificationsService,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
private systemService: SystemService,
|
||||
private processReloadSerivce: ProcessReloadServiceAbstraction,
|
||||
private environmentService: BrowserEnvironmentService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
@@ -216,7 +216,7 @@ export default class RuntimeBackground {
|
||||
}
|
||||
|
||||
await this.notificationsService.updateConnection(msg.command === "loggedIn");
|
||||
this.systemService.cancelProcessReload();
|
||||
this.processReloadSerivce.cancelProcessReload();
|
||||
|
||||
if (item) {
|
||||
await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId);
|
||||
|
||||
@@ -26,8 +26,8 @@ export class SendFilePopoutDialogContainerComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
if (
|
||||
this.config.sendType === SendType.File &&
|
||||
this.config.mode === "add" &&
|
||||
this.config?.sendType === SendType.File &&
|
||||
this.config?.mode === "add" &&
|
||||
this.filePopoutUtilsService.showFilePopoutMessage(window)
|
||||
) {
|
||||
this.dialogService.open(SendFilePopoutDialogComponent);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<div role="toolbar" [ariaLabel]="'filters' | i18n">
|
||||
<form [formGroup]="filterForm" class="tw-flex tw-flex-wrap tw-gap-2 tw-mt-2">
|
||||
<form
|
||||
[formGroup]="filterForm"
|
||||
class="tw-gap-2 tw-mt-2 tw-grid tw-grid-cols-2 sm:tw-grid-cols-3 lg:tw-grid-cols-4"
|
||||
>
|
||||
<ng-container *ngIf="organizations$ | async as organizations">
|
||||
<bit-chip-select
|
||||
*ngIf="organizations.length"
|
||||
fullWidth
|
||||
formControlName="organization"
|
||||
placeholderIcon="bwi-vault"
|
||||
[placeholderText]="'vault' | i18n"
|
||||
@@ -13,6 +17,7 @@
|
||||
<ng-container *ngIf="collections$ | async as collections">
|
||||
<bit-chip-select
|
||||
*ngIf="collections.length"
|
||||
fullWidth
|
||||
formControlName="collection"
|
||||
placeholderIcon="bwi-collection"
|
||||
[placeholderText]="'collection' | i18n"
|
||||
@@ -23,6 +28,7 @@
|
||||
<ng-container *ngIf="folders$ | async as folders">
|
||||
<bit-chip-select
|
||||
*ngIf="folders.length"
|
||||
fullWidth
|
||||
placeholderIcon="bwi-folder"
|
||||
formControlName="folder"
|
||||
[placeholderText]="'folder' | i18n"
|
||||
@@ -32,6 +38,7 @@
|
||||
</ng-container>
|
||||
<bit-chip-select
|
||||
formControlName="cipherType"
|
||||
fullWidth
|
||||
placeholderIcon="bwi-list"
|
||||
[placeholderText]="'type' | i18n"
|
||||
[options]="cipherTypes"
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<bit-section *ngIf="ciphers?.length > 0 || description" [disableMargin]="disableSectionMargin">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<button
|
||||
*ngIf="showRefresh"
|
||||
bitIconButton="bwi-refresh"
|
||||
type="button"
|
||||
size="small"
|
||||
(click)="onRefresh.emit()"
|
||||
[appA11yTitle]="'refresh' | i18n"
|
||||
></button>
|
||||
<span bitTypography="body2" slot="end">{{ ciphers.length }}</span>
|
||||
</bit-section-header>
|
||||
<div class="tw-ml-1">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<button
|
||||
*ngIf="showRefresh"
|
||||
bitIconButton="bwi-refresh"
|
||||
type="button"
|
||||
size="small"
|
||||
(click)="onRefresh.emit()"
|
||||
[appA11yTitle]="'refresh' | i18n"
|
||||
></button>
|
||||
<span bitTypography="body2" slot="end">{{ ciphers.length }}</span>
|
||||
</bit-section-header>
|
||||
</div>
|
||||
<div *ngIf="description" class="tw-text-muted tw-px-1 tw-mb-2" bitTypography="body2">
|
||||
{{ description }}
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
@@ -142,6 +143,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private notificationsService: NotificationsService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private systemService: SystemService,
|
||||
private processReloadService: ProcessReloadServiceAbstraction,
|
||||
private stateService: StateService,
|
||||
private eventUploadService: EventUploadService,
|
||||
private policyService: InternalPolicyService,
|
||||
@@ -213,7 +215,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.updateAppMenu();
|
||||
this.systemService.cancelProcessReload();
|
||||
this.processReloadService.cancelProcessReload();
|
||||
break;
|
||||
case "loggedOut":
|
||||
this.modalService.closeAll();
|
||||
@@ -224,7 +226,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.updateAppMenu();
|
||||
await this.systemService.clearPendingClipboard();
|
||||
await this.systemService.startProcessReload(this.authService);
|
||||
await this.processReloadService.startProcessReload(this.authService);
|
||||
break;
|
||||
case "authBlocked":
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
@@ -268,15 +270,15 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.notificationsService.updateConnection();
|
||||
await this.updateAppMenu();
|
||||
await this.systemService.clearPendingClipboard();
|
||||
await this.systemService.startProcessReload(this.authService);
|
||||
await this.processReloadService.startProcessReload(this.authService);
|
||||
break;
|
||||
case "startProcessReload":
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.systemService.startProcessReload(this.authService);
|
||||
this.processReloadService.startProcessReload(this.authService);
|
||||
break;
|
||||
case "cancelProcessReload":
|
||||
this.systemService.cancelProcessReload();
|
||||
this.processReloadService.cancelProcessReload();
|
||||
break;
|
||||
case "reloadProcess":
|
||||
ipc.platform.reloadProcess();
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { ProcessReloadService } from "@bitwarden/common/key-management/services/process-reload.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import {
|
||||
CryptoService,
|
||||
@@ -196,16 +198,22 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: SystemServiceAbstraction,
|
||||
useClass: SystemService,
|
||||
deps: [
|
||||
PlatformUtilsServiceAbstraction,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
TaskSchedulerService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ProcessReloadServiceAbstraction,
|
||||
useClass: ProcessReloadService,
|
||||
deps: [
|
||||
PinServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
RELOAD_CALLBACK,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
VaultTimeoutSettingsService,
|
||||
BiometricStateService,
|
||||
AccountServiceAbstraction,
|
||||
TaskSchedulerService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -259,6 +259,9 @@
|
||||
"generatePassword": {
|
||||
"message": "Generate password"
|
||||
},
|
||||
"generatePassphrase": {
|
||||
"message": "Generate passphrase"
|
||||
},
|
||||
"type": {
|
||||
"message": "Type"
|
||||
},
|
||||
@@ -394,6 +397,10 @@
|
||||
"copyPassword": {
|
||||
"message": "Copy password"
|
||||
},
|
||||
"copyPassphrase": {
|
||||
"message": "Copy passphrase",
|
||||
"description": "Copy passphrase to clipboard"
|
||||
},
|
||||
"copyUri": {
|
||||
"message": "Copy URI"
|
||||
},
|
||||
@@ -828,6 +835,10 @@
|
||||
"baseUrl": {
|
||||
"message": "Server URL"
|
||||
},
|
||||
"selfHostBaseUrl": {
|
||||
"message": "Self-host server URL",
|
||||
"description": "Label for field requesting a self-hosted integration service URL"
|
||||
},
|
||||
"apiUrl": {
|
||||
"message": "API server URL"
|
||||
},
|
||||
@@ -1218,6 +1229,9 @@
|
||||
"message": "Copy number",
|
||||
"description": "Copy credit card number"
|
||||
},
|
||||
"copyEmail": {
|
||||
"message": "Copy email"
|
||||
},
|
||||
"copySecurityCode": {
|
||||
"message": "Copy security code",
|
||||
"description": "Copy credit card security code (CVV)"
|
||||
@@ -2352,6 +2366,9 @@
|
||||
"generateUsername": {
|
||||
"message": "Generate username"
|
||||
},
|
||||
"generateEmail": {
|
||||
"message": "Generate email"
|
||||
},
|
||||
"usernameType": {
|
||||
"message": "Username type"
|
||||
},
|
||||
@@ -2395,6 +2412,14 @@
|
||||
"forwardedEmailDesc": {
|
||||
"message": "Generate an email alias with an external forwarding service."
|
||||
},
|
||||
"forwarderDomainName": {
|
||||
"message": "Email domain",
|
||||
"description": "Labels the domain name email forwarder service option"
|
||||
},
|
||||
"forwarderDomainNameHint": {
|
||||
"message": "Choose a domain that is supported by the selected service",
|
||||
"description": "Guidance provided for email forwarding services that support multiple email domains."
|
||||
},
|
||||
"forwarderError": {
|
||||
"message": "$SERVICENAME$ error: $ERRORMESSAGE$",
|
||||
"description": "Reports an error returned by a forwarding service to the user.",
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
color: themed("headingButtonColor");
|
||||
}
|
||||
|
||||
margin-right: 0.25rem;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
@include themify($themes) {
|
||||
|
||||
@@ -2,17 +2,15 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { from, map, switchMap, tap } from "rxjs";
|
||||
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 { 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";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
BadgeModule,
|
||||
@@ -28,10 +26,6 @@ import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { cipherData } from "../reports/pages/reports-ciphers.mock";
|
||||
|
||||
import { userData } from "./password-health.mock";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -47,24 +41,18 @@ import { userData } from "./password-health.mock";
|
||||
HeaderModule,
|
||||
TableModule,
|
||||
],
|
||||
providers: [PasswordHealthService],
|
||||
})
|
||||
export class PasswordHealthMembersComponent implements OnInit {
|
||||
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
|
||||
|
||||
weakPasswordCiphers: CipherView[] = [];
|
||||
|
||||
passwordUseMap = new Map<string, number>();
|
||||
|
||||
exposedPasswordMap = new Map<string, number>();
|
||||
|
||||
dataSource = new TableDataSource<CipherView>();
|
||||
|
||||
totalMembersMap = new Map<string, number>();
|
||||
|
||||
reportCiphers: CipherView[] = [];
|
||||
reportCipherIds: string[] = [];
|
||||
|
||||
organization: Organization;
|
||||
dataSource = new TableDataSource<CipherView>();
|
||||
|
||||
loading = true;
|
||||
|
||||
@@ -73,7 +61,6 @@ export class PasswordHealthMembersComponent implements OnInit {
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected organizationService: OrganizationService,
|
||||
protected auditService: AuditService,
|
||||
protected i18nService: I18nService,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
@@ -83,151 +70,29 @@ export class PasswordHealthMembersComponent implements OnInit {
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map((params) => params.get("organizationId")),
|
||||
switchMap((organizationId) => {
|
||||
return from(this.organizationService.get(organizationId));
|
||||
map(async (params) => {
|
||||
const organizationId = params.get("organizationId");
|
||||
await this.setCiphers(organizationId);
|
||||
}),
|
||||
tap((organization) => {
|
||||
this.organization = organization;
|
||||
}),
|
||||
switchMap(() => from(this.setCiphers())),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// mock data - will be replaced with actual data
|
||||
userData.forEach((user) => {
|
||||
user.cipherIds.forEach((cipherId: string) => {
|
||||
if (this.totalMembersMap.has(cipherId)) {
|
||||
this.totalMembersMap.set(cipherId, (this.totalMembersMap.get(cipherId) || 0) + 1);
|
||||
} else {
|
||||
this.totalMembersMap.set(cipherId, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
// const allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
const allCiphers = cipherData;
|
||||
allCiphers.forEach(async (cipher) => {
|
||||
this.findWeakPassword(cipher);
|
||||
this.findReusedPassword(cipher);
|
||||
await this.findExposedPassword(cipher);
|
||||
});
|
||||
this.dataSource.data = this.reportCiphers;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
protected checkForExistingCipher(ciph: CipherView) {
|
||||
if (!this.reportCipherIds.includes(ciph.id)) {
|
||||
this.reportCipherIds.push(ciph.id);
|
||||
this.reportCiphers.push(ciph);
|
||||
}
|
||||
}
|
||||
|
||||
protected async findExposedPassword(cipher: CipherView) {
|
||||
const { type, login, isDeleted, edit, viewPassword, id } = cipher;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
login.password === "" ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exposedCount = await this.auditService.passwordLeaked(login.password);
|
||||
if (exposedCount > 0) {
|
||||
this.exposedPasswordMap.set(id, exposedCount);
|
||||
this.checkForExistingCipher(cipher);
|
||||
}
|
||||
}
|
||||
|
||||
protected findReusedPassword(cipher: CipherView) {
|
||||
const { type, login, isDeleted, edit, viewPassword } = cipher;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
login.password === "" ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.passwordUseMap.has(login.password)) {
|
||||
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) || 0 + 1);
|
||||
} else {
|
||||
this.passwordUseMap.set(login.password, 1);
|
||||
}
|
||||
|
||||
this.checkForExistingCipher(cipher);
|
||||
}
|
||||
|
||||
protected findWeakPassword(cipher: CipherView): void {
|
||||
const { type, login, isDeleted, edit, viewPassword } = cipher;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
login.password === "" ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasUserName = this.isUserNameNotEmpty(cipher);
|
||||
let userInput: string[] = [];
|
||||
if (hasUserName) {
|
||||
const atPosition = login.username.indexOf("@");
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput
|
||||
.concat(
|
||||
login.username
|
||||
.substring(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/),
|
||||
)
|
||||
.filter((i) => i.length >= 3);
|
||||
} else {
|
||||
userInput = login.username
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/)
|
||||
.filter((i) => i.length >= 3);
|
||||
}
|
||||
}
|
||||
const { score } = this.passwordStrengthService.getPasswordStrength(
|
||||
login.password,
|
||||
null,
|
||||
userInput.length > 0 ? userInput : null,
|
||||
async setCiphers(organizationId: string) {
|
||||
const passwordHealthService = new PasswordHealthService(
|
||||
this.passwordStrengthService,
|
||||
this.auditService,
|
||||
this.cipherService,
|
||||
organizationId,
|
||||
);
|
||||
|
||||
if (score != null && score <= 2) {
|
||||
this.passwordStrengthMap.set(cipher.id, this.scoreKey(score));
|
||||
this.checkForExistingCipher(cipher);
|
||||
}
|
||||
}
|
||||
await passwordHealthService.generateReport();
|
||||
|
||||
private isUserNameNotEmpty(c: CipherView): boolean {
|
||||
return !Utils.isNullOrWhitespace(c.login.username);
|
||||
}
|
||||
|
||||
private scoreKey(score: number): [string, BadgeVariant] {
|
||||
switch (score) {
|
||||
case 4:
|
||||
return ["strong", "success"];
|
||||
case 3:
|
||||
return ["good", "primary"];
|
||||
case 2:
|
||||
return ["weak", "warning"];
|
||||
default:
|
||||
return ["veryWeak", "danger"];
|
||||
}
|
||||
this.dataSource.data = passwordHealthService.reportCiphers;
|
||||
this.exposedPasswordMap = passwordHealthService.exposedPasswordMap;
|
||||
this.passwordStrengthMap = passwordHealthService.passwordStrengthMap;
|
||||
this.passwordUseMap = passwordHealthService.passwordUseMap;
|
||||
this.totalMembersMap = passwordHealthService.totalMembersMap;
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, convertToParamMap } from "@angular/router";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
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 { 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";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -14,39 +14,30 @@ import { TableBodyDirective } from "@bitwarden/components/src/table/table.compon
|
||||
|
||||
import { LooseComponentsModule } from "../../shared";
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { cipherData } from "../reports/pages/reports-ciphers.mock";
|
||||
|
||||
import { PasswordHealthComponent } from "./password-health.component";
|
||||
|
||||
describe("PasswordHealthComponent", () => {
|
||||
let component: PasswordHealthComponent;
|
||||
let fixture: ComponentFixture<PasswordHealthComponent>;
|
||||
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
let cipherServiceMock: MockProxy<CipherService>;
|
||||
let auditServiceMock: MockProxy<AuditService>;
|
||||
const activeRouteParams = convertToParamMap({ organizationId: "orgId" });
|
||||
|
||||
beforeEach(async () => {
|
||||
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
|
||||
auditServiceMock = mock<AuditService>();
|
||||
organizationService = mock<OrganizationService>({
|
||||
get: jest.fn().mockResolvedValue({ id: "orgId" } as Organization),
|
||||
});
|
||||
cipherServiceMock = mock<CipherService>({
|
||||
getAllFromApiForOrganization: jest.fn().mockResolvedValue(cipherData),
|
||||
});
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule],
|
||||
declarations: [TableBodyDirective],
|
||||
providers: [
|
||||
{ provide: CipherService, useValue: cipherServiceMock },
|
||||
{ provide: PasswordStrengthServiceAbstraction, useValue: passwordStrengthService },
|
||||
{ provide: OrganizationService, useValue: organizationService },
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||
{ provide: AuditService, useValue: auditServiceMock },
|
||||
{ provide: AuditService, useValue: mock<AuditService>() },
|
||||
{
|
||||
provide: PasswordStrengthServiceAbstraction,
|
||||
useValue: mock<PasswordStrengthServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: PasswordHealthService,
|
||||
useValue: mock<PasswordHealthService>(),
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
@@ -69,46 +60,5 @@ describe("PasswordHealthComponent", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should populate reportCiphers with ciphers that have password issues", async () => {
|
||||
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 1 } as any);
|
||||
|
||||
auditServiceMock.passwordLeaked.mockResolvedValue(5);
|
||||
|
||||
await component.setCiphers();
|
||||
|
||||
const cipherIds = component.reportCiphers.map((c) => c.id);
|
||||
|
||||
expect(cipherIds).toEqual([
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228ab1",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228ab2",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
|
||||
]);
|
||||
expect(component.reportCiphers.length).toEqual(3);
|
||||
});
|
||||
|
||||
it("should correctly populate passwordStrengthMap", async () => {
|
||||
passwordStrengthService.getPasswordStrength.mockImplementation((password) => {
|
||||
let score = 0;
|
||||
if (password === "123") {
|
||||
score = 1;
|
||||
} else {
|
||||
score = 4;
|
||||
}
|
||||
return { score } as any;
|
||||
});
|
||||
|
||||
auditServiceMock.passwordLeaked.mockResolvedValue(0);
|
||||
|
||||
await component.setCiphers();
|
||||
|
||||
expect(component.passwordStrengthMap.size).toBeGreaterThan(0);
|
||||
expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toEqual([
|
||||
"veryWeak",
|
||||
"danger",
|
||||
]);
|
||||
expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toEqual([
|
||||
"veryWeak",
|
||||
"danger",
|
||||
]);
|
||||
});
|
||||
it("should call generateReport on init", () => {});
|
||||
});
|
||||
|
||||
@@ -2,17 +2,15 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { from, map, switchMap, tap } from "rxjs";
|
||||
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 { 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";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
BadgeModule,
|
||||
@@ -43,23 +41,17 @@ import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
HeaderModule,
|
||||
TableModule,
|
||||
],
|
||||
providers: [PasswordHealthService],
|
||||
})
|
||||
export class PasswordHealthComponent implements OnInit {
|
||||
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
|
||||
|
||||
weakPasswordCiphers: CipherView[] = [];
|
||||
|
||||
passwordUseMap = new Map<string, number>();
|
||||
|
||||
exposedPasswordMap = new Map<string, number>();
|
||||
|
||||
dataSource = new TableDataSource<CipherView>();
|
||||
|
||||
reportCiphers: CipherView[] = [];
|
||||
reportCipherIds: string[] = [];
|
||||
|
||||
organization: Organization;
|
||||
|
||||
loading = true;
|
||||
|
||||
private destroyRef = inject(DestroyRef);
|
||||
@@ -67,7 +59,6 @@ export class PasswordHealthComponent implements OnInit {
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected organizationService: OrganizationService,
|
||||
protected auditService: AuditService,
|
||||
protected i18nService: I18nService,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
@@ -77,153 +68,28 @@ export class PasswordHealthComponent implements OnInit {
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map((params) => params.get("organizationId")),
|
||||
switchMap((organizationId) => {
|
||||
return from(this.organizationService.get(organizationId));
|
||||
map(async (params) => {
|
||||
const organizationId = params.get("organizationId");
|
||||
await this.setCiphers(organizationId);
|
||||
}),
|
||||
tap((organization) => {
|
||||
this.organization = organization;
|
||||
}),
|
||||
switchMap(() => from(this.setCiphers())),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
allCiphers.forEach(async (cipher) => {
|
||||
this.findWeakPassword(cipher);
|
||||
this.findReusedPassword(cipher);
|
||||
await this.findExposedPassword(cipher);
|
||||
});
|
||||
this.dataSource.data = this.reportCiphers;
|
||||
this.loading = false;
|
||||
|
||||
// const reportIssues = allCiphers.map((c) => {
|
||||
// if (this.passwordStrengthMap.has(c.id)) {
|
||||
// return c;
|
||||
// }
|
||||
|
||||
// if (this.passwordUseMap.has(c.id)) {
|
||||
// return c;
|
||||
// }
|
||||
|
||||
// if (this.exposedPasswordMap.has(c.id)) {
|
||||
// return c;
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
protected checkForExistingCipher(ciph: CipherView) {
|
||||
if (!this.reportCipherIds.includes(ciph.id)) {
|
||||
this.reportCipherIds.push(ciph.id);
|
||||
this.reportCiphers.push(ciph);
|
||||
}
|
||||
}
|
||||
|
||||
protected async findExposedPassword(cipher: CipherView) {
|
||||
const { type, login, isDeleted, edit, viewPassword, id } = cipher;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
login.password === "" ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exposedCount = await this.auditService.passwordLeaked(login.password);
|
||||
if (exposedCount > 0) {
|
||||
this.exposedPasswordMap.set(id, exposedCount);
|
||||
this.checkForExistingCipher(cipher);
|
||||
}
|
||||
}
|
||||
|
||||
protected findReusedPassword(cipher: CipherView) {
|
||||
const { type, login, isDeleted, edit, viewPassword } = cipher;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
login.password === "" ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.passwordUseMap.has(login.password)) {
|
||||
this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) || 0 + 1);
|
||||
} else {
|
||||
this.passwordUseMap.set(login.password, 1);
|
||||
}
|
||||
|
||||
this.checkForExistingCipher(cipher);
|
||||
}
|
||||
|
||||
protected findWeakPassword(cipher: CipherView): void {
|
||||
const { type, login, isDeleted, edit, viewPassword } = cipher;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
login.password === "" ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasUserName = this.isUserNameNotEmpty(cipher);
|
||||
let userInput: string[] = [];
|
||||
if (hasUserName) {
|
||||
const atPosition = login.username.indexOf("@");
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput
|
||||
.concat(
|
||||
login.username
|
||||
.substring(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/),
|
||||
)
|
||||
.filter((i) => i.length >= 3);
|
||||
} else {
|
||||
userInput = login.username
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/)
|
||||
.filter((i) => i.length >= 3);
|
||||
}
|
||||
}
|
||||
const { score } = this.passwordStrengthService.getPasswordStrength(
|
||||
login.password,
|
||||
null,
|
||||
userInput.length > 0 ? userInput : null,
|
||||
async setCiphers(organizationId: string) {
|
||||
const passwordHealthService = new PasswordHealthService(
|
||||
this.passwordStrengthService,
|
||||
this.auditService,
|
||||
this.cipherService,
|
||||
organizationId,
|
||||
);
|
||||
|
||||
if (score != null && score <= 2) {
|
||||
this.passwordStrengthMap.set(cipher.id, this.scoreKey(score));
|
||||
this.checkForExistingCipher(cipher);
|
||||
}
|
||||
}
|
||||
await passwordHealthService.generateReport();
|
||||
|
||||
private isUserNameNotEmpty(c: CipherView): boolean {
|
||||
return !Utils.isNullOrWhitespace(c.login.username);
|
||||
}
|
||||
|
||||
private scoreKey(score: number): [string, BadgeVariant] {
|
||||
switch (score) {
|
||||
case 4:
|
||||
return ["strong", "success"];
|
||||
case 3:
|
||||
return ["good", "primary"];
|
||||
case 2:
|
||||
return ["weak", "warning"];
|
||||
default:
|
||||
return ["veryWeak", "danger"];
|
||||
}
|
||||
this.dataSource.data = passwordHealthService.reportCiphers;
|
||||
this.exposedPasswordMap = passwordHealthService.exposedPasswordMap;
|
||||
this.passwordStrengthMap = passwordHealthService.passwordStrengthMap;
|
||||
this.passwordUseMap = passwordHealthService.passwordUseMap;
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
export const userData: any[] = [
|
||||
{
|
||||
userName: "David Brent",
|
||||
email: "david.brent@wernhamhogg.uk",
|
||||
usesKeyConnector: true,
|
||||
cipherIds: [
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228ab1",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228ab2",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001227nm5",
|
||||
],
|
||||
},
|
||||
{
|
||||
userName: "Tim Canterbury",
|
||||
email: "tim.canterbury@wernhamhogg.uk",
|
||||
usesKeyConnector: false,
|
||||
cipherIds: [
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228ab2",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001227nm5",
|
||||
],
|
||||
},
|
||||
{
|
||||
userName: "Gareth Keenan",
|
||||
email: "gareth.keenan@wernhamhogg.uk",
|
||||
usesKeyConnector: true,
|
||||
cipherIds: [
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001227nm5",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001227nm7",
|
||||
],
|
||||
},
|
||||
{
|
||||
userName: "Dawn Tinsley",
|
||||
email: "dawn.tinsley@wernhamhogg.uk",
|
||||
usesKeyConnector: true,
|
||||
cipherIds: [
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228ab2",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
|
||||
],
|
||||
},
|
||||
{
|
||||
userName: "Keith Bishop",
|
||||
email: "keith.bishop@wernhamhogg.uk",
|
||||
usesKeyConnector: false,
|
||||
cipherIds: [
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228ab1",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001227nm5",
|
||||
],
|
||||
},
|
||||
{
|
||||
userName: "Chris Finch",
|
||||
email: "chris.finch@wernhamhogg.uk",
|
||||
usesKeyConnector: true,
|
||||
cipherIds: [
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228ab2",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228cd3",
|
||||
"cbea34a8-bde4-46ad-9d19-b05001228xy4",
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -405,6 +405,9 @@
|
||||
"generatePassword": {
|
||||
"message": "Generate password"
|
||||
},
|
||||
"generatePassphrase": {
|
||||
"message": "Generate passphrase"
|
||||
},
|
||||
"checkPassword": {
|
||||
"message": "Check if password has been exposed."
|
||||
},
|
||||
@@ -663,6 +666,10 @@
|
||||
"message": "Copy password",
|
||||
"description": "Copy password to clipboard"
|
||||
},
|
||||
"copyPassphrase": {
|
||||
"message": "Copy passphrase",
|
||||
"description": "Copy passphrase to clipboard"
|
||||
},
|
||||
"passwordCopied": {
|
||||
"message": "Password copied"
|
||||
},
|
||||
@@ -6354,6 +6361,9 @@
|
||||
"generateUsername": {
|
||||
"message": "Generate username"
|
||||
},
|
||||
"generateEmail": {
|
||||
"message": "Generate email"
|
||||
},
|
||||
"usernameType": {
|
||||
"message": "Username type"
|
||||
},
|
||||
@@ -6459,6 +6469,14 @@
|
||||
"forwardedEmailDesc": {
|
||||
"message": "Generate an email alias with an external forwarding service."
|
||||
},
|
||||
"forwarderDomainName": {
|
||||
"message": "Email domain",
|
||||
"description": "Labels the domain name email forwarder service option"
|
||||
},
|
||||
"forwarderDomainNameHint": {
|
||||
"message": "Choose a domain that is supported by the selected service",
|
||||
"description": "Guidance provided for email forwarding services that support multiple email domains."
|
||||
},
|
||||
"forwarderError": {
|
||||
"message": "$SERVICENAME$ error: $ERRORMESSAGE$",
|
||||
"description": "Reports an error returned by a forwarding service to the user.",
|
||||
@@ -8258,6 +8276,10 @@
|
||||
"baseUrl": {
|
||||
"message": "Server URL"
|
||||
},
|
||||
"selfHostBaseUrl": {
|
||||
"message": "Self-host server URL",
|
||||
"description": "Label for field requesting a self-hosted integration service URL"
|
||||
},
|
||||
"aliasDomain": {
|
||||
"message": "Alias domain"
|
||||
},
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
|
||||
button.toggle-button,
|
||||
button.add-button {
|
||||
margin-right: 0.25rem;
|
||||
&:hover,
|
||||
&:focus {
|
||||
@include themify($themes) {
|
||||
@@ -98,6 +99,7 @@
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
margin-right: 0.25rem;
|
||||
&:hover,
|
||||
&:focus {
|
||||
@include themify($themes) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"@bitwarden/auth/common": ["../../libs/auth/src/common"],
|
||||
"@bitwarden/auth/angular": ["../../libs/auth/src/angular"],
|
||||
"@bitwarden/billing": ["../../libs/billing/src"],
|
||||
"@bitwarden/bit-common/*": ["../../bitwarden_license/bit-common/src/*"],
|
||||
"@bitwarden/common/*": ["../../libs/common/src/*"],
|
||||
"@bitwarden/components": ["../../libs/components/src"],
|
||||
"@bitwarden/generator-components": ["../../libs/tools/generator/components/src"],
|
||||
|
||||
Reference in New Issue
Block a user