1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 18:23:31 +00:00

merge main, fix conflicts

This commit is contained in:
rr-bw
2024-09-05 16:53:32 -07:00
300 changed files with 5977 additions and 2162 deletions

View File

@@ -0,0 +1 @@
export * from "./organization-user";

View File

@@ -0,0 +1 @@
export * from "./organization-user-api.service";

View File

@@ -1,4 +1,4 @@
import { ListResponse } from "../../../models/response/list.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import {
OrganizationUserAcceptInitRequest,
@@ -9,19 +9,19 @@ import {
OrganizationUserResetPasswordEnrollmentRequest,
OrganizationUserResetPasswordRequest,
OrganizationUserUpdateRequest,
} from "./requests";
} from "../models/requests";
import {
OrganizationUserBulkPublicKeyResponse,
OrganizationUserBulkResponse,
OrganizationUserDetailsResponse,
OrganizationUserResetPasswordDetailsResponse,
OrganizationUserUserDetailsResponse,
} from "./responses";
} from "../models/responses";
/**
* Service for interacting with Organization Users via the API
*/
export abstract class OrganizationUserService {
export abstract class OrganizationUserApiService {
/**
* Retrieve a single organization user by Id
* @param organizationId - Identifier for the user's organization

View File

@@ -0,0 +1,3 @@
export * from "./abstractions";
export * from "./services";
export * from "./models";

View File

@@ -0,0 +1,2 @@
export * from "./requests";
export * from "./responses";

View File

@@ -6,3 +6,4 @@ export * from "./organization-user-invite.request";
export * from "./organization-user-reset-password.request";
export * from "./organization-user-reset-password-enrollment.request";
export * from "./organization-user-update.request";
export * from "./organization-user-bulk.request";

View File

@@ -1,4 +1,4 @@
import { OrganizationKeysRequest } from "../../../models/request/organization-keys.request";
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
export class OrganizationUserAcceptInitRequest {
token: string;

View File

@@ -0,0 +1,12 @@
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
export class OrganizationUserInviteRequest {
emails: string[] = [];
type: OrganizationUserType;
accessSecretsManager: boolean;
collections: SelectionReadOnlyRequest[] = [];
groups: string[];
permissions: PermissionsApi;
}

View File

@@ -1,4 +1,4 @@
import { SecretVerificationRequest } from "../../../../auth/models/request/secret-verification.request";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
export class OrganizationUserResetPasswordEnrollmentRequest extends SecretVerificationRequest {
resetPasswordKey: string;

View File

@@ -0,0 +1,11 @@
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
export class OrganizationUserUpdateRequest {
type: OrganizationUserType;
accessSecretsManager: boolean;
collections: SelectionReadOnlyRequest[] = [];
groups: string[] = [];
permissions: PermissionsApi;
}

View File

@@ -1,4 +1,4 @@
import { BaseResponse } from "../../../../models/response/base.response";
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class OrganizationUserBulkPublicKeyResponse extends BaseResponse {
id: string;

View File

@@ -1,4 +1,4 @@
import { BaseResponse } from "../../../../models/response/base.response";
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class OrganizationUserBulkResponse extends BaseResponse {
id: string;

View File

@@ -1,8 +1,11 @@
import { BaseResponse } from "../../../../models/response/base.response";
import { KdfType } from "../../../../platform/enums";
import { OrganizationUserStatusType, OrganizationUserType } from "../../../enums";
import { PermissionsApi } from "../../../models/api/permissions.api";
import { SelectionReadOnlyResponse } from "../../../models/response/selection-read-only.response";
import {
OrganizationUserStatusType,
OrganizationUserType,
} from "@bitwarden/common/admin-console/enums";
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
import { SelectionReadOnlyResponse } from "@bitwarden/common/admin-console/models/response/selection-read-only.response";
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { KdfType } from "@bitwarden/common/platform/enums";
export class OrganizationUserResponse extends BaseResponse {
id: string;

View File

@@ -1,6 +1,7 @@
import { ApiService } from "../../../abstractions/api.service";
import { ListResponse } from "../../../models/response/list.response";
import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { OrganizationUserApiService } from "../abstractions";
import {
OrganizationUserAcceptInitRequest,
OrganizationUserAcceptRequest,
@@ -10,18 +11,17 @@ import {
OrganizationUserResetPasswordEnrollmentRequest,
OrganizationUserResetPasswordRequest,
OrganizationUserUpdateRequest,
} from "../../abstractions/organization-user/requests";
OrganizationUserBulkRequest,
} from "../models/requests";
import {
OrganizationUserBulkPublicKeyResponse,
OrganizationUserBulkResponse,
OrganizationUserDetailsResponse,
OrganizationUserResetPasswordDetailsResponse,
OrganizationUserUserDetailsResponse,
} from "../../abstractions/organization-user/responses";
} from "../models/responses";
import { OrganizationUserBulkRequest } from "./requests";
export class OrganizationUserServiceImplementation implements OrganizationUserService {
export class DefaultOrganizationUserApiService implements OrganizationUserApiService {
constructor(private apiService: ApiService) {}
async getOrganizationUser(

View File

@@ -0,0 +1 @@
export * from "./default-organization-user-api.service";

View File

@@ -17,6 +17,7 @@ import {
take,
} from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import {
LoginEmailServiceAbstraction,
UserDecryptionOptions,
@@ -24,7 +25,6 @@ import {
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
@@ -95,7 +95,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
protected loginEmailService: LoginEmailServiceAbstraction,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService,
protected organizationUserApiService: OrganizationUserApiService,
protected apiService: ApiService,
protected i18nService: I18nService,
protected validationService: ValidationService,
@@ -251,12 +251,12 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
return;
}
this.loginEmailService.setEmail(this.data.userEmail);
this.loginEmailService.setLoginEmail(this.data.userEmail);
await this.router.navigate(["/login-with-device"]);
}
async requestAdminApproval() {
this.loginEmailService.setEmail(this.data.userEmail);
this.loginEmailService.setLoginEmail(this.data.userEmail);
await this.router.navigate(["/admin-approval-requested"]);
}

View File

@@ -1,5 +1,6 @@
import { Directive, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -27,8 +28,8 @@ export class HintComponent implements OnInit {
protected toastService: ToastService,
) {}
ngOnInit(): void {
this.email = this.loginEmailService.getEmail() ?? "";
async ngOnInit(): Promise<void> {
this.email = (await firstValueFrom(this.loginEmailService.loginEmail$)) ?? "";
}
async submit() {

View File

@@ -93,13 +93,6 @@ export class LoginViaAuthRequestComponent
) {
super(environmentService, i18nService, platformUtilsService, toastService);
// TODO: I don't know why this is necessary.
// Why would the existence of the email depend on the navigation?
const navigation = this.router.getCurrentNavigation();
if (navigation) {
this.email = this.loginEmailService.getEmail();
}
// Gets signalR push notification
// Only fires on approval to prevent enumeration
this.authRequestService.authRequestPushNotification$
@@ -118,6 +111,7 @@ export class LoginViaAuthRequestComponent
}
async ngOnInit() {
this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
this.userAuthNStatus = await this.authService.getAuthStatus();
const matchOptions: IsActiveMatchOptions = {
@@ -165,7 +159,7 @@ export class LoginViaAuthRequestComponent
} else {
// Standard auth request
// TODO: evaluate if we can remove the setting of this.email in the constructor
this.email = this.loginEmailService.getEmail();
this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
if (!this.email) {
this.toastService.showToast({
@@ -216,9 +210,10 @@ export class LoginViaAuthRequestComponent
const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey(
adminAuthReqStorable.privateKey,
);
this.fingerprintPhrase = (
await this.cryptoService.getFingerprint(this.email, derivedPublicKeyArrayBuffer)
).join("-");
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
this.email,
derivedPublicKeyArrayBuffer,
);
// Request denied
if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) {
@@ -265,9 +260,10 @@ export class LoginViaAuthRequestComponent
length: 25,
});
this.fingerprintPhrase = (
await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair.publicKey)
).join("-");
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
this.email,
this.authRequestKeyPair.publicKey,
);
this.authRequest = new CreateAuthRequest(
this.email,

View File

@@ -1,8 +1,8 @@
import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, firstValueFrom } from "rxjs";
import { take, takeUntil } from "rxjs/operators";
import { Subject, firstValueFrom, of } from "rxjs";
import { switchMap, take, takeUntil } from "rxjs/operators";
import {
LoginStrategyServiceAbstraction,
@@ -101,22 +101,31 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
}
async ngOnInit() {
this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
if (!params) {
return;
}
this.route?.queryParams
.pipe(
switchMap((params) => {
if (!params) {
// If no params,loadEmailSettings from state
return this.loadEmailSettings();
}
const queryParamsEmail = params.email;
const queryParamsEmail = params.email;
// If there is an email in the query params, set that email as the form field value
if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) {
this.formGroup.controls.email.setValue(queryParamsEmail);
this.paramEmailSet = true;
}
});
if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) {
this.formGroup.controls.email.setValue(queryParamsEmail);
this.paramEmailSet = true;
}
// if there is no email in the query params, attempt to load email settings from loginEmailService
if (!this.paramEmailSet) {
// If paramEmailSet is false, loadEmailSettings from state
return this.paramEmailSet ? of(null) : this.loadEmailSettings();
}),
takeUntil(this.destroy$),
)
.subscribe();
// Backup check to handle unknown case where activatedRoute is not available
// This shouldn't happen under normal circumstances
if (!this.route) {
await this.loadEmailSettings();
}
}
@@ -310,7 +319,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
private async loadEmailSettings() {
// Try to load from memory first
const email = this.loginEmailService.getEmail();
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
const rememberEmail = this.loginEmailService.getRememberEmail();
if (email) {
@@ -328,7 +337,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit,
}
protected async saveEmailSettings() {
this.loginEmailService.setEmail(this.formGroup.value.email);
this.loginEmailService.setLoginEmail(this.formGroup.value.email);
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
await this.loginEmailService.saveEmailSettings();
}

View File

@@ -3,11 +3,13 @@ import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, of } from "rxjs";
import { filter, first, switchMap, tap } from "rxjs/operators";
import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
@@ -68,7 +70,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
private route: ActivatedRoute,
stateService: StateService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
@@ -219,7 +221,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
resetRequest.masterPasswordHash = masterPasswordHash;
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
return this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
this.orgId,
this.userId,
resetRequest,

View File

@@ -1,6 +1,10 @@
import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core";
import { Subject } from "rxjs";
import {
OrganizationUserApiService,
DefaultOrganizationUserApiService,
} from "@bitwarden/admin-console/common";
import {
SetPasswordJitService,
DefaultSetPasswordJitService,
@@ -45,7 +49,6 @@ import {
OrgDomainServiceAbstraction,
} from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain.service.abstraction";
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import {
InternalPolicyService,
@@ -58,7 +61,6 @@ import { OrganizationService } from "@bitwarden/common/admin-console/services/or
import { OrgDomainApiService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain-api.service";
import { OrgDomainService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain.service";
import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/services/organization-management-preferences/default-organization-management-preferences.service";
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service";
@@ -370,7 +372,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: AppIdServiceAbstraction,
useClass: AppIdService,
deps: [GlobalStateProvider],
deps: [OBSERVABLE_DISK_STORAGE, LogService],
}),
safeProvider({
provide: AuditServiceAbstraction,
@@ -936,8 +938,8 @@ const safeProviders: SafeProvider[] = [
useExisting: InternalOrganizationServiceAbstraction,
}),
safeProvider({
provide: OrganizationUserService,
useClass: OrganizationUserServiceImplementation,
provide: OrganizationUserApiService,
useClass: DefaultOrganizationUserApiService,
deps: [ApiServiceAbstraction],
}),
safeProvider({
@@ -947,7 +949,7 @@ const safeProviders: SafeProvider[] = [
OrganizationApiServiceAbstraction,
AccountServiceAbstraction,
CryptoServiceAbstraction,
OrganizationUserService,
OrganizationUserApiService,
I18nServiceAbstraction,
],
}),
@@ -1275,7 +1277,7 @@ const safeProviders: SafeProvider[] = [
KdfConfigServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
OrganizationApiServiceAbstraction,
OrganizationUserService,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
],
}),

View File

@@ -37,6 +37,7 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -330,6 +331,11 @@ export class AddEditComponent implements OnInit, OnDestroy {
return this.restore();
}
// normalize card expiry year on save
if (this.cipher.type === this.cipherType.Card) {
this.cipher.card.expYear = normalizeExpiryYearFormat(this.cipher.card.expYear);
}
if (this.cipher.name == null || this.cipher.name === "") {
this.platformUtilsService.showToast(
"error",

View File

@@ -15,8 +15,6 @@ import {
Environment,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { ButtonModule } from "@bitwarden/components";
// FIXME: remove `/apps` import from `/libs`
@@ -40,7 +38,6 @@ const decorators = (options: {
applicationVersion?: string;
clientType?: ClientType;
hostName?: string;
themeType?: ThemeType;
}) => {
return [
componentWrapperDecorator(
@@ -84,12 +81,6 @@ const decorators = (options: {
getClientType: () => options.clientType || ClientType.Web,
} as Partial<PlatformUtilsService>,
},
{
provide: ThemeStateService,
useValue: {
selectedTheme$: of(options.themeType || ThemeType.Light),
} as Partial<ThemeStateService>,
},
],
}),
applicationConfig({

View File

@@ -1,6 +1,10 @@
<main
class="tw-flex tw-min-h-screen tw-w-full tw-mx-auto tw-flex-col tw-gap-7 tw-bg-background-alt tw-px-8 tw-pb-4 tw-text-main"
[ngClass]="{ 'tw-pt-0': decreaseTopPadding, 'tw-pt-8': !decreaseTopPadding }"
[ngClass]="{
'tw-pt-0': decreaseTopPadding,
'tw-pt-8': !decreaseTopPadding,
'tw-relative tw-top-0': clientType === 'browser',
}"
>
<bit-icon *ngIf="!hideLogo" [icon]="logo" class="tw-w-[128px] [&>*]:tw-align-top"></bit-icon>

View File

@@ -5,13 +5,11 @@ import { firstValueFrom } from "rxjs";
import { ClientType } from "@bitwarden/common/enums";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { IconModule, Icon } from "../../../../components/src/icon";
import { SharedModule } from "../../../../components/src/shared";
import { TypographyModule } from "../../../../components/src/typography";
import { BitwardenLogoPrimary, BitwardenLogoWhite } from "../icons";
import { BitwardenShieldPrimary, BitwardenShieldWhite } from "../icons/bitwarden-shield.icon";
import { BitwardenLogo, BitwardenShield } from "../icons";
@Component({
standalone: true,
@@ -34,20 +32,17 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
*/
@Input() maxWidth: "md" | "3xl" = "md";
protected logo: Icon;
protected logo = BitwardenLogo;
protected year = "2024";
protected clientType: ClientType;
protected hostname: string;
protected version: string;
protected theme: string;
protected hideYearAndVersion = false;
constructor(
private environmentService: EnvironmentService,
private platformUtilsService: PlatformUtilsService,
private themeStateService: ThemeStateService,
) {
this.year = new Date().getFullYear().toString();
this.clientType = this.platformUtilsService.getClientType();
@@ -56,41 +51,18 @@ export class AnonLayoutComponent implements OnInit, OnChanges {
async ngOnInit() {
this.maxWidth = this.maxWidth ?? "md";
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
if (this.theme === "dark") {
this.logo = BitwardenLogoWhite;
} else {
this.logo = BitwardenLogoPrimary;
}
await this.updateIcon(this.theme);
this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname();
this.version = await this.platformUtilsService.getApplicationVersion();
// If there is no icon input, then use the default icon
if (this.icon == null) {
this.icon = BitwardenShield;
}
}
async ngOnChanges(changes: SimpleChanges) {
if (changes.icon) {
const theme = await firstValueFrom(this.themeStateService.selectedTheme$);
await this.updateIcon(theme);
}
if (changes.maxWidth) {
this.maxWidth = changes.maxWidth.currentValue ?? "md";
}
}
private async updateIcon(theme: string) {
if (this.icon == null) {
if (theme === "dark") {
this.icon = BitwardenShieldWhite;
}
if (theme !== "dark") {
this.icon = BitwardenShieldPrimary;
}
}
}
}

View File

@@ -1,11 +1,10 @@
import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
import { BehaviorSubject, of } from "rxjs";
import { BehaviorSubject } from "rxjs";
import { ClientType } from "@bitwarden/common/enums";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { ButtonModule } from "../../../../components/src/button";
import { I18nMockService } from "../../../../components/src/utils/i18n-mock.service";
@@ -47,12 +46,6 @@ export default {
}).asObservable(),
},
},
{
provide: ThemeStateService,
useValue: {
selectedTheme$: of("light"),
},
},
],
}),
],

View File

@@ -1,17 +1,9 @@
import { svgIcon } from "@bitwarden/components";
export const BitwardenLogoPrimary = svgIcon`
<svg viewBox="0 0 290 45" fill="#175DDC" xmlns="http://www.w3.org/2000/svg">
export const BitwardenLogo = svgIcon`
<svg viewBox="0 0 290 45" xmlns="http://www.w3.org/2000/svg">
<title>Bitwarden</title>
<path fill-rule="evenodd" clip-rule="evenodd" d="M69.799 10.713c3.325 0 5.911 1.248 7.811 3.848 1.9 2.549 2.85 6.033 2.85 10.453 0 4.576-.95 8.113-2.902 10.61-1.953 2.547-4.592 3.743-7.918 3.743-3.325 0-5.858-1.144-7.758-3.536h-.528l-1.003 2.444a.976.976 0 0 1-.897.572H55.23a.94.94 0 0 1-.95-.936V1.352a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v8.009c0 1.144-.105 2.964-.316 5.46h.317c1.741-2.704 4.433-4.108 7.917-4.108Zm-2.428 6.084c-1.847 0-3.273.572-4.17 1.717-.844 1.144-1.32 3.068-1.32 5.668v.832c0 2.964.423 5.097 1.32 6.345.897 1.248 2.322 1.924 4.275 1.924 1.531 0 2.85-.728 3.748-2.184.897-1.404 1.372-3.537 1.372-6.189 0-2.704-.475-4.732-1.372-6.084-.95-1.352-2.27-2.029-3.853-2.029ZM93.022 38.9h-5.7a.94.94 0 0 1-.95-.936V12.221a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v25.69c.053.468-.422.988-.95.988Zm20.849-5.564c1.108 0 2.428-.208 4.011-.624a.632.632 0 0 1 .792.624v4.316a.64.64 0 0 1-.37.572c-1.794.728-4.064 1.092-6.597 1.092-3.062 0-5.278-.728-6.651-2.288-1.372-1.508-2.111-3.796-2.111-6.812V16.953h-3.008c-.37 0-.634-.26-.634-.624v-2.444c0-.052.053-.104.053-.156l4.17-2.444 2.058-5.408c.106-.26.317-.417.581-.417h3.8c.369 0 .633.26.633.625v5.252h7.548c.158 0 .317.156.317.312v4.68c0 .364-.264.624-.634.624h-7.178v13.21c0 1.04.317 1.872.897 2.34.528.572 1.373.832 2.323.832Zm35.521 5.564c-.739 0-1.319-.468-1.636-1.144l-5.595-16.797c-.369-1.196-.844-3.016-1.478-5.357h-.158l-.528 1.873-1.108 3.536-5.753 16.797c-.211.676-.845 1.092-1.584 1.092a1.628 1.628 0 0 1-1.583-1.196l-7.02-24.182c-.211-.728.369-1.508 1.214-1.508h.158c.528 0 1.003.364 1.161.884l4.117 14.717c1.003 3.849 1.689 6.657 2.006 8.53h.158c.95-3.85 1.689-6.397 2.164-7.698l5.331-15.393c.211-.624.792-1.04 1.531-1.04.686 0 1.267.416 1.478 1.04l4.961 15.29c1.214 3.9 1.953 6.396 2.217 7.696h.158c.159-1.04.792-3.952 2.006-8.633l3.958-14.509c.159-.52.634-.884 1.162-.884.791 0 1.372.728 1.161 1.508l-6.651 24.182c-.211.728-.844 1.196-1.636 1.196h-.211Zm31.352 0a.962.962 0 0 1-.95-.832l-.475-3.432h-.264c-1.372 1.716-2.745 2.964-4.223 3.692-1.425.728-3.166 1.04-5.119 1.04-2.692 0-4.751-.676-6.228-2.028-1.32-1.196-2.059-2.808-2.164-4.836-.212-2.704.95-5.305 3.166-6.813 2.27-1.456 5.437-2.34 9.712-2.34l5.173-.156v-1.768c0-2.6-.528-4.473-1.637-5.773-1.108-1.3-2.744-1.924-5.067-1.924-2.216 0-4.433.52-6.756 1.612-.58.26-1.266 0-1.53-.572s0-1.248.58-1.456c2.639-1.04 5.226-1.612 7.865-1.612 3.008 0 5.225.78 6.756 2.34 1.478 1.508 2.216 3.953 2.216 7.125v16.901c-.052.312-.527.832-1.055.832Zm-10.926-1.768c2.956 0 5.226-.832 6.862-2.444 1.689-1.612 2.533-3.952 2.533-6.813v-2.6l-4.75.208c-3.853.156-6.545.78-8.234 1.768-1.636.988-2.481 2.6-2.481 4.68 0 1.665.528 3.017 1.531 3.953 1.161.78 2.639 1.248 4.539 1.248Zm31.246-25.638c.792 0 1.584.052 2.481.156a1.176 1.176 0 0 1 1.003 1.352c-.106.624-.739.988-1.372.884-.792-.104-1.584-.208-2.375-.208-2.323 0-4.223.988-5.701 2.912-1.478 1.925-2.217 4.42-2.217 7.333v13.625c0 .676-.527 1.196-1.214 1.196-.686 0-1.213-.52-1.213-1.196V13.105c0-.572.475-1.04 1.055-1.04.581 0 1.056.416 1.056.988l.211 3.848h.158c1.109-1.976 2.323-3.38 3.589-4.16 1.214-.832 2.745-1.248 4.539-1.248Zm18.579 0c1.953 0 3.695.364 5.12 1.04 1.478.676 2.745 1.924 3.853 3.64h.158a122.343 122.343 0 0 1-.158-6.084V1.612c0-.676.528-1.196 1.214-1.196.686 0 1.214.52 1.214 1.196v36.351c0 .468-.37.832-.845.832a.852.852 0 0 1-.844-.78l-.528-3.38h-.211c-2.058 3.068-5.067 4.576-8.92 4.576-3.8 0-6.598-1.144-8.656-3.484-1.953-2.34-3.008-5.668-3.008-10.089 0-4.628.95-8.165 2.955-10.66 2.006-2.237 4.856-3.485 8.656-3.485Zm0 2.236c-3.008 0-5.225 1.04-6.756 3.12-1.478 2.029-2.216 4.993-2.216 8.945 0 7.593 3.008 11.39 9.025 11.39 3.114 0 5.331-.885 6.756-2.653 1.478-1.768 2.164-4.68 2.164-8.737v-.416c0-4.16-.686-7.124-2.164-8.893-1.372-1.872-3.642-2.756-6.809-2.756Zm31.616 25.638c-3.959 0-7.02-1.196-9.289-3.64-2.217-2.392-3.326-5.772-3.326-10.089 0-4.316 1.056-7.748 3.22-10.297 2.164-2.6 5.014-3.9 8.656-3.9 3.167 0 5.753 1.092 7.548 3.276 1.9 2.184 2.797 5.2 2.797 8.997v1.976h-19.634c.052 3.692.897 6.5 2.639 8.477 1.741 1.976 4.169 2.86 7.389 2.86 1.531 0 2.956-.104 4.117-.312.844-.156 1.847-.416 3.061-.832.686-.26 1.425.26 1.425.988 0 .416-.264.832-.686.988-1.267.52-2.481.832-3.589 1.04-1.32.364-2.745.468-4.328.468Zm-.739-25.69c-2.639 0-4.75.832-6.334 2.548-1.583 1.665-2.48 4.16-2.797 7.333h16.89c0-3.068-.686-5.564-2.059-7.28-1.372-1.717-3.272-2.6-5.7-2.6ZM288.733 38.9c-.686 0-1.214-.52-1.214-1.196V21.426c0-2.704-.58-4.68-1.689-5.877-1.214-1.196-2.955-1.872-5.383-1.872-3.273 0-5.648.78-7.126 2.444-1.478 1.613-2.322 4.265-2.322 7.853V37.6c0 .676-.528 1.196-1.214 1.196-.686 0-1.214-.52-1.214-1.196V13.105c0-.624.475-1.092 1.108-1.092.581 0 1.003.416 1.109.936l.316 2.704h.159c1.794-2.808 4.908-4.212 9.448-4.212 6.175 0 9.289 3.276 9.289 9.829V37.6c-.053.727-.633 1.3-1.267 1.3ZM90.225 0c-2.48 0-4.486 1.872-4.486 4.212v.416c0 2.289 2.058 4.213 4.486 4.213s4.486-1.924 4.486-4.213v-.364C94.711 1.872 92.653 0 90.225 0Z" />
<path d="M32.041 24.546V5.95H18.848v33.035c2.336-1.22 4.427-2.547 6.272-3.98 4.614-3.565 6.921-7.051 6.921-10.46Zm5.654-22.314v22.314c0 1.665-.329 3.317-.986 4.953-.658 1.637-1.473 3.09-2.445 4.359-.971 1.268-2.13 2.503-3.475 3.704-1.345 1.2-2.586 2.199-3.725 2.993a46.963 46.963 0 0 1-3.563 2.251c-1.237.707-2.116 1.187-2.636 1.439-.52.251-.938.445-1.252.58-.235.117-.49.175-.765.175s-.53-.058-.766-.174c-.314-.136-.731-.33-1.252-.581-.52-.252-1.398-.732-2.635-1.439a47.003 47.003 0 0 1-3.564-2.251c-1.138-.794-2.38-1.792-3.725-2.993-1.345-1.2-2.503-2.436-3.475-3.704-.972-1.27-1.787-2.722-2.444-4.359C.329 27.863 0 26.211 0 24.546V2.232c0-.504.187-.94.56-1.308A1.823 1.823 0 0 1 1.885.372H35.81c.511 0 .953.184 1.326.552.373.368.56.804.56 1.308Z" />
</svg>
`;
export const BitwardenLogoWhite = svgIcon`
<svg viewBox="0 0 290 45" fill="#FFF" xmlns="http://www.w3.org/2000/svg">
<title>Bitwarden</title>
<path fill-rule="evenodd" clip-rule="evenodd" d="M69.799 10.713c3.325 0 5.911 1.248 7.811 3.848 1.9 2.549 2.85 6.033 2.85 10.453 0 4.576-.95 8.113-2.902 10.61-1.953 2.547-4.592 3.743-7.918 3.743-3.325 0-5.858-1.144-7.758-3.536h-.528l-1.003 2.444a.976.976 0 0 1-.897.572H55.23a.94.94 0 0 1-.95-.936V1.352a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v8.009c0 1.144-.105 2.964-.316 5.46h.317c1.741-2.704 4.433-4.108 7.917-4.108Zm-2.428 6.084c-1.847 0-3.273.572-4.17 1.717-.844 1.144-1.32 3.068-1.32 5.668v.832c0 2.964.423 5.097 1.32 6.345.897 1.248 2.322 1.924 4.275 1.924 1.531 0 2.85-.728 3.748-2.184.897-1.404 1.372-3.537 1.372-6.189 0-2.704-.475-4.732-1.372-6.084-.95-1.352-2.27-2.029-3.853-2.029ZM93.022 38.9h-5.7a.94.94 0 0 1-.95-.936V12.221a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v25.69c.053.468-.422.988-.95.988Zm20.849-5.564c1.108 0 2.428-.208 4.011-.624a.632.632 0 0 1 .792.624v4.316a.64.64 0 0 1-.37.572c-1.794.728-4.064 1.092-6.597 1.092-3.062 0-5.278-.728-6.651-2.288-1.372-1.508-2.111-3.796-2.111-6.812V16.953h-3.008c-.37 0-.634-.26-.634-.624v-2.444c0-.052.053-.104.053-.156l4.17-2.444 2.058-5.408c.106-.26.317-.417.581-.417h3.8c.369 0 .633.26.633.625v5.252h7.548c.158 0 .317.156.317.312v4.68c0 .364-.264.624-.634.624h-7.178v13.21c0 1.04.317 1.872.897 2.34.528.572 1.373.832 2.323.832Zm35.521 5.564c-.739 0-1.319-.468-1.636-1.144l-5.595-16.797c-.369-1.196-.844-3.016-1.478-5.357h-.158l-.528 1.873-1.108 3.536-5.753 16.797c-.211.676-.845 1.092-1.584 1.092a1.628 1.628 0 0 1-1.583-1.196l-7.02-24.182c-.211-.728.369-1.508 1.214-1.508h.158c.528 0 1.003.364 1.161.884l4.117 14.717c1.003 3.849 1.689 6.657 2.006 8.53h.158c.95-3.85 1.689-6.397 2.164-7.698l5.331-15.393c.211-.624.792-1.04 1.531-1.04.686 0 1.267.416 1.478 1.04l4.961 15.29c1.214 3.9 1.953 6.396 2.217 7.696h.158c.159-1.04.792-3.952 2.006-8.633l3.958-14.509c.159-.52.634-.884 1.162-.884.791 0 1.372.728 1.161 1.508l-6.651 24.182c-.211.728-.844 1.196-1.636 1.196h-.211Zm31.352 0a.962.962 0 0 1-.95-.832l-.475-3.432h-.264c-1.372 1.716-2.745 2.964-4.223 3.692-1.425.728-3.166 1.04-5.119 1.04-2.692 0-4.751-.676-6.228-2.028-1.32-1.196-2.059-2.808-2.164-4.836-.212-2.704.95-5.305 3.166-6.813 2.27-1.456 5.437-2.34 9.712-2.34l5.173-.156v-1.768c0-2.6-.528-4.473-1.637-5.773-1.108-1.3-2.744-1.924-5.067-1.924-2.216 0-4.433.52-6.756 1.612-.58.26-1.266 0-1.53-.572s0-1.248.58-1.456c2.639-1.04 5.226-1.612 7.865-1.612 3.008 0 5.225.78 6.756 2.34 1.478 1.508 2.216 3.953 2.216 7.125v16.901c-.052.312-.527.832-1.055.832Zm-10.926-1.768c2.956 0 5.226-.832 6.862-2.444 1.689-1.612 2.533-3.952 2.533-6.813v-2.6l-4.75.208c-3.853.156-6.545.78-8.234 1.768-1.636.988-2.481 2.6-2.481 4.68 0 1.665.528 3.017 1.531 3.953 1.161.78 2.639 1.248 4.539 1.248Zm31.246-25.638c.792 0 1.584.052 2.481.156a1.176 1.176 0 0 1 1.003 1.352c-.106.624-.739.988-1.372.884-.792-.104-1.584-.208-2.375-.208-2.323 0-4.223.988-5.701 2.912-1.478 1.925-2.217 4.42-2.217 7.333v13.625c0 .676-.527 1.196-1.214 1.196-.686 0-1.213-.52-1.213-1.196V13.105c0-.572.475-1.04 1.055-1.04.581 0 1.056.416 1.056.988l.211 3.848h.158c1.109-1.976 2.323-3.38 3.589-4.16 1.214-.832 2.745-1.248 4.539-1.248Zm18.579 0c1.953 0 3.695.364 5.12 1.04 1.478.676 2.745 1.924 3.853 3.64h.158a122.343 122.343 0 0 1-.158-6.084V1.612c0-.676.528-1.196 1.214-1.196.686 0 1.214.52 1.214 1.196v36.351c0 .468-.37.832-.845.832a.852.852 0 0 1-.844-.78l-.528-3.38h-.211c-2.058 3.068-5.067 4.576-8.92 4.576-3.8 0-6.598-1.144-8.656-3.484-1.953-2.34-3.008-5.668-3.008-10.089 0-4.628.95-8.165 2.955-10.66 2.006-2.237 4.856-3.485 8.656-3.485Zm0 2.236c-3.008 0-5.225 1.04-6.756 3.12-1.478 2.029-2.216 4.993-2.216 8.945 0 7.593 3.008 11.39 9.025 11.39 3.114 0 5.331-.885 6.756-2.653 1.478-1.768 2.164-4.68 2.164-8.737v-.416c0-4.16-.686-7.124-2.164-8.893-1.372-1.872-3.642-2.756-6.809-2.756Zm31.616 25.638c-3.959 0-7.02-1.196-9.289-3.64-2.217-2.392-3.326-5.772-3.326-10.089 0-4.316 1.056-7.748 3.22-10.297 2.164-2.6 5.014-3.9 8.656-3.9 3.167 0 5.753 1.092 7.548 3.276 1.9 2.184 2.797 5.2 2.797 8.997v1.976h-19.634c.052 3.692.897 6.5 2.639 8.477 1.741 1.976 4.169 2.86 7.389 2.86 1.531 0 2.956-.104 4.117-.312.844-.156 1.847-.416 3.061-.832.686-.26 1.425.26 1.425.988 0 .416-.264.832-.686.988-1.267.52-2.481.832-3.589 1.04-1.32.364-2.745.468-4.328.468Zm-.739-25.69c-2.639 0-4.75.832-6.334 2.548-1.583 1.665-2.48 4.16-2.797 7.333h16.89c0-3.068-.686-5.564-2.059-7.28-1.372-1.717-3.272-2.6-5.7-2.6ZM288.733 38.9c-.686 0-1.214-.52-1.214-1.196V21.426c0-2.704-.58-4.68-1.689-5.877-1.214-1.196-2.955-1.872-5.383-1.872-3.273 0-5.648.78-7.126 2.444-1.478 1.613-2.322 4.265-2.322 7.853V37.6c0 .676-.528 1.196-1.214 1.196-.686 0-1.214-.52-1.214-1.196V13.105c0-.624.475-1.092 1.108-1.092.581 0 1.003.416 1.109.936l.316 2.704h.159c1.794-2.808 4.908-4.212 9.448-4.212 6.175 0 9.289 3.276 9.289 9.829V37.6c-.053.727-.633 1.3-1.267 1.3ZM90.225 0c-2.48 0-4.486 1.872-4.486 4.212v.416c0 2.289 2.058 4.213 4.486 4.213s4.486-1.924 4.486-4.213v-.364C94.711 1.872 92.653 0 90.225 0Z" />
<path d="M32.041 24.546V5.95H18.848v33.035c2.336-1.22 4.427-2.547 6.272-3.98 4.614-3.565 6.921-7.051 6.921-10.46Zm5.654-22.314v22.314c0 1.665-.329 3.317-.986 4.953-.658 1.637-1.473 3.09-2.445 4.359-.971 1.268-2.13 2.503-3.475 3.704-1.345 1.2-2.586 2.199-3.725 2.993a46.963 46.963 0 0 1-3.563 2.251c-1.237.707-2.116 1.187-2.636 1.439-.52.251-.938.445-1.252.58-.235.117-.49.175-.765.175s-.53-.058-.766-.174c-.314-.136-.731-.33-1.252-.581-.52-.252-1.398-.732-2.635-1.439a47.003 47.003 0 0 1-3.564-2.251c-1.138-.794-2.38-1.792-3.725-2.993-1.345-1.2-2.503-2.436-3.475-3.704-.972-1.27-1.787-2.722-2.444-4.359C.329 27.863 0 26.211 0 24.546V2.232c0-.504.187-.94.56-1.308A1.823 1.823 0 0 1 1.885.372H35.81c.511 0 .953.184 1.326.552.373.368.56.804.56 1.308Z" />
<path class="tw-fill-marketing-logo" fill-rule="evenodd" clip-rule="evenodd" d="M69.799 10.713c3.325 0 5.911 1.248 7.811 3.848 1.9 2.549 2.85 6.033 2.85 10.453 0 4.576-.95 8.113-2.902 10.61-1.953 2.547-4.592 3.743-7.918 3.743-3.325 0-5.858-1.144-7.758-3.536h-.528l-1.003 2.444a.976.976 0 0 1-.897.572H55.23a.94.94 0 0 1-.95-.936V1.352a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v8.009c0 1.144-.105 2.964-.316 5.46h.317c1.741-2.704 4.433-4.108 7.917-4.108Zm-2.428 6.084c-1.847 0-3.273.572-4.17 1.717-.844 1.144-1.32 3.068-1.32 5.668v.832c0 2.964.423 5.097 1.32 6.345.897 1.248 2.322 1.924 4.275 1.924 1.531 0 2.85-.728 3.748-2.184.897-1.404 1.372-3.537 1.372-6.189 0-2.704-.475-4.732-1.372-6.084-.95-1.352-2.27-2.029-3.853-2.029ZM93.022 38.9h-5.7a.94.94 0 0 1-.95-.936V12.221a.94.94 0 0 1 .95-.936h5.7a.94.94 0 0 1 .95.936v25.69c.053.468-.422.988-.95.988Zm20.849-5.564c1.108 0 2.428-.208 4.011-.624a.632.632 0 0 1 .792.624v4.316a.64.64 0 0 1-.37.572c-1.794.728-4.064 1.092-6.597 1.092-3.062 0-5.278-.728-6.651-2.288-1.372-1.508-2.111-3.796-2.111-6.812V16.953h-3.008c-.37 0-.634-.26-.634-.624v-2.444c0-.052.053-.104.053-.156l4.17-2.444 2.058-5.408c.106-.26.317-.417.581-.417h3.8c.369 0 .633.26.633.625v5.252h7.548c.158 0 .317.156.317.312v4.68c0 .364-.264.624-.634.624h-7.178v13.21c0 1.04.317 1.872.897 2.34.528.572 1.373.832 2.323.832Zm35.521 5.564c-.739 0-1.319-.468-1.636-1.144l-5.595-16.797c-.369-1.196-.844-3.016-1.478-5.357h-.158l-.528 1.873-1.108 3.536-5.753 16.797c-.211.676-.845 1.092-1.584 1.092a1.628 1.628 0 0 1-1.583-1.196l-7.02-24.182c-.211-.728.369-1.508 1.214-1.508h.158c.528 0 1.003.364 1.161.884l4.117 14.717c1.003 3.849 1.689 6.657 2.006 8.53h.158c.95-3.85 1.689-6.397 2.164-7.698l5.331-15.393c.211-.624.792-1.04 1.531-1.04.686 0 1.267.416 1.478 1.04l4.961 15.29c1.214 3.9 1.953 6.396 2.217 7.696h.158c.159-1.04.792-3.952 2.006-8.633l3.958-14.509c.159-.52.634-.884 1.162-.884.791 0 1.372.728 1.161 1.508l-6.651 24.182c-.211.728-.844 1.196-1.636 1.196h-.211Zm31.352 0a.962.962 0 0 1-.95-.832l-.475-3.432h-.264c-1.372 1.716-2.745 2.964-4.223 3.692-1.425.728-3.166 1.04-5.119 1.04-2.692 0-4.751-.676-6.228-2.028-1.32-1.196-2.059-2.808-2.164-4.836-.212-2.704.95-5.305 3.166-6.813 2.27-1.456 5.437-2.34 9.712-2.34l5.173-.156v-1.768c0-2.6-.528-4.473-1.637-5.773-1.108-1.3-2.744-1.924-5.067-1.924-2.216 0-4.433.52-6.756 1.612-.58.26-1.266 0-1.53-.572s0-1.248.58-1.456c2.639-1.04 5.226-1.612 7.865-1.612 3.008 0 5.225.78 6.756 2.34 1.478 1.508 2.216 3.953 2.216 7.125v16.901c-.052.312-.527.832-1.055.832Zm-10.926-1.768c2.956 0 5.226-.832 6.862-2.444 1.689-1.612 2.533-3.952 2.533-6.813v-2.6l-4.75.208c-3.853.156-6.545.78-8.234 1.768-1.636.988-2.481 2.6-2.481 4.68 0 1.665.528 3.017 1.531 3.953 1.161.78 2.639 1.248 4.539 1.248Zm31.246-25.638c.792 0 1.584.052 2.481.156a1.176 1.176 0 0 1 1.003 1.352c-.106.624-.739.988-1.372.884-.792-.104-1.584-.208-2.375-.208-2.323 0-4.223.988-5.701 2.912-1.478 1.925-2.217 4.42-2.217 7.333v13.625c0 .676-.527 1.196-1.214 1.196-.686 0-1.213-.52-1.213-1.196V13.105c0-.572.475-1.04 1.055-1.04.581 0 1.056.416 1.056.988l.211 3.848h.158c1.109-1.976 2.323-3.38 3.589-4.16 1.214-.832 2.745-1.248 4.539-1.248Zm18.579 0c1.953 0 3.695.364 5.12 1.04 1.478.676 2.745 1.924 3.853 3.64h.158a122.343 122.343 0 0 1-.158-6.084V1.612c0-.676.528-1.196 1.214-1.196.686 0 1.214.52 1.214 1.196v36.351c0 .468-.37.832-.845.832a.852.852 0 0 1-.844-.78l-.528-3.38h-.211c-2.058 3.068-5.067 4.576-8.92 4.576-3.8 0-6.598-1.144-8.656-3.484-1.953-2.34-3.008-5.668-3.008-10.089 0-4.628.95-8.165 2.955-10.66 2.006-2.237 4.856-3.485 8.656-3.485Zm0 2.236c-3.008 0-5.225 1.04-6.756 3.12-1.478 2.029-2.216 4.993-2.216 8.945 0 7.593 3.008 11.39 9.025 11.39 3.114 0 5.331-.885 6.756-2.653 1.478-1.768 2.164-4.68 2.164-8.737v-.416c0-4.16-.686-7.124-2.164-8.893-1.372-1.872-3.642-2.756-6.809-2.756Zm31.616 25.638c-3.959 0-7.02-1.196-9.289-3.64-2.217-2.392-3.326-5.772-3.326-10.089 0-4.316 1.056-7.748 3.22-10.297 2.164-2.6 5.014-3.9 8.656-3.9 3.167 0 5.753 1.092 7.548 3.276 1.9 2.184 2.797 5.2 2.797 8.997v1.976h-19.634c.052 3.692.897 6.5 2.639 8.477 1.741 1.976 4.169 2.86 7.389 2.86 1.531 0 2.956-.104 4.117-.312.844-.156 1.847-.416 3.061-.832.686-.26 1.425.26 1.425.988 0 .416-.264.832-.686.988-1.267.52-2.481.832-3.589 1.04-1.32.364-2.745.468-4.328.468Zm-.739-25.69c-2.639 0-4.75.832-6.334 2.548-1.583 1.665-2.48 4.16-2.797 7.333h16.89c0-3.068-.686-5.564-2.059-7.28-1.372-1.717-3.272-2.6-5.7-2.6ZM288.733 38.9c-.686 0-1.214-.52-1.214-1.196V21.426c0-2.704-.58-4.68-1.689-5.877-1.214-1.196-2.955-1.872-5.383-1.872-3.273 0-5.648.78-7.126 2.444-1.478 1.613-2.322 4.265-2.322 7.853V37.6c0 .676-.528 1.196-1.214 1.196-.686 0-1.214-.52-1.214-1.196V13.105c0-.624.475-1.092 1.108-1.092.581 0 1.003.416 1.109.936l.316 2.704h.159c1.794-2.808 4.908-4.212 9.448-4.212 6.175 0 9.289 3.276 9.289 9.829V37.6c-.053.727-.633 1.3-1.267 1.3ZM90.225 0c-2.48 0-4.486 1.872-4.486 4.212v.416c0 2.289 2.058 4.213 4.486 4.213s4.486-1.924 4.486-4.213v-.364C94.711 1.872 92.653 0 90.225 0Z" />
<path class="tw-fill-marketing-logo" d="M32.041 24.546V5.95H18.848v33.035c2.336-1.22 4.427-2.547 6.272-3.98 4.614-3.565 6.921-7.051 6.921-10.46Zm5.654-22.314v22.314c0 1.665-.329 3.317-.986 4.953-.658 1.637-1.473 3.09-2.445 4.359-.971 1.268-2.13 2.503-3.475 3.704-1.345 1.2-2.586 2.199-3.725 2.993a46.963 46.963 0 0 1-3.563 2.251c-1.237.707-2.116 1.187-2.636 1.439-.52.251-.938.445-1.252.58-.235.117-.49.175-.765.175s-.53-.058-.766-.174c-.314-.136-.731-.33-1.252-.581-.52-.252-1.398-.732-2.635-1.439a47.003 47.003 0 0 1-3.564-2.251c-1.138-.794-2.38-1.792-3.725-2.993-1.345-1.2-2.503-2.436-3.475-3.704-.972-1.27-1.787-2.722-2.444-4.359C.329 27.863 0 26.211 0 24.546V2.232c0-.504.187-.94.56-1.308A1.823 1.823 0 0 1 1.885.372H35.81c.511 0 .953.184 1.326.552.373.368.56.804.56 1.308Z" />
</svg>
`;

View File

@@ -1,13 +1,7 @@
import { svgIcon } from "@bitwarden/components";
export const BitwardenShieldPrimary = svgIcon`
<svg viewBox="0 0 120 132" fill="#175DDC" xmlns="http://www.w3.org/2000/svg">
<path d="M82.2944 69.1899V37.2898H60V93.9624C63.948 91.869 67.4812 89.5927 70.5998 87.1338C78.3962 81.0196 82.2944 75.0383 82.2944 69.1899ZM91.8491 30.9097V69.1899C91.8491 72.0477 91.2934 74.8805 90.182 77.6883C89.0706 80.4962 87.6938 82.9884 86.0516 85.1649C84.4094 87.3415 82.452 89.4598 80.1794 91.5201C77.9068 93.5803 75.8084 95.2916 73.8842 96.654C71.96 98.0164 69.9528 99.304 67.8627 100.517C65.7726 101.73 64.288 102.552 63.4088 102.984C62.5297 103.416 61.8247 103.748 61.2939 103.981C60.8958 104.18 60.4645 104.28 60 104.28C59.5355 104.28 59.1042 104.18 58.7061 103.981C58.1753 103.748 57.4703 103.416 56.5911 102.984C55.712 102.552 54.2273 101.73 52.1372 100.517C50.0471 99.304 48.04 98.0164 46.1158 96.654C44.1916 95.2916 42.0932 93.5803 39.8206 91.5201C37.548 89.4598 35.5906 87.3415 33.9484 85.1649C32.3062 82.9884 30.9294 80.4962 29.818 77.6883C28.7066 74.8805 28.1509 72.0477 28.1509 69.1899V30.9097C28.1509 30.0458 28.4661 29.2981 29.0964 28.6668C29.7267 28.0354 30.4732 27.7197 31.3358 27.7197H88.6642C89.5268 27.7197 90.2732 28.0354 90.9036 28.6668C91.5339 29.2981 91.8491 30.0458 91.8491 30.9097Z" fill="#175DDC"/>
</svg>
`;
export const BitwardenShieldWhite = svgIcon`
<svg viewBox="0 0 120 132" fill="#FFF" xmlns="http://www.w3.org/2000/svg">
<path d="M82.2944 69.1899V37.2898H60V93.9624C63.948 91.869 67.4812 89.5927 70.5998 87.1338C78.3962 81.0196 82.2944 75.0383 82.2944 69.1899ZM91.8491 30.9097V69.1899C91.8491 72.0477 91.2934 74.8805 90.182 77.6883C89.0706 80.4962 87.6938 82.9884 86.0516 85.1649C84.4094 87.3415 82.452 89.4598 80.1794 91.5201C77.9068 93.5803 75.8084 95.2916 73.8842 96.654C71.96 98.0164 69.9528 99.304 67.8627 100.517C65.7726 101.73 64.288 102.552 63.4088 102.984C62.5297 103.416 61.8247 103.748 61.2939 103.981C60.8958 104.18 60.4645 104.28 60 104.28C59.5355 104.28 59.1042 104.18 58.7061 103.981C58.1753 103.748 57.4703 103.416 56.5911 102.984C55.712 102.552 54.2273 101.73 52.1372 100.517C50.0471 99.304 48.04 98.0164 46.1158 96.654C44.1916 95.2916 42.0932 93.5803 39.8206 91.5201C37.548 89.4598 35.5906 87.3415 33.9484 85.1649C32.3062 82.9884 30.9294 80.4962 29.818 77.6883C28.7066 74.8805 28.1509 72.0477 28.1509 69.1899V30.9097C28.1509 30.0458 28.4661 29.2981 29.0964 28.6668C29.7267 28.0354 30.4732 27.7197 31.3358 27.7197H88.6642C89.5268 27.7197 90.2732 28.0354 90.9036 28.6668C91.5339 29.2981 91.8491 30.0458 91.8491 30.9097Z" fill="#175DDC"/>
export const BitwardenShield = svgIcon`
<svg viewBox="0 0 120 132" xmlns="http://www.w3.org/2000/svg">
<path class="tw-fill-marketing-logo" d="M82.2944 69.1899V37.2898H60V93.9624C63.948 91.869 67.4812 89.5927 70.5998 87.1338C78.3962 81.0196 82.2944 75.0383 82.2944 69.1899ZM91.8491 30.9097V69.1899C91.8491 72.0477 91.2934 74.8805 90.182 77.6883C89.0706 80.4962 87.6938 82.9884 86.0516 85.1649C84.4094 87.3415 82.452 89.4598 80.1794 91.5201C77.9068 93.5803 75.8084 95.2916 73.8842 96.654C71.96 98.0164 69.9528 99.304 67.8627 100.517C65.7726 101.73 64.288 102.552 63.4088 102.984C62.5297 103.416 61.8247 103.748 61.2939 103.981C60.8958 104.18 60.4645 104.28 60 104.28C59.5355 104.28 59.1042 104.18 58.7061 103.981C58.1753 103.748 57.4703 103.416 56.5911 102.984C55.712 102.552 54.2273 101.73 52.1372 100.517C50.0471 99.304 48.04 98.0164 46.1158 96.654C44.1916 95.2916 42.0932 93.5803 39.8206 91.5201C37.548 89.4598 35.5906 87.3415 33.9484 85.1649C32.3062 82.9884 30.9294 80.4962 29.818 77.6883C28.7066 74.8805 28.1509 72.0477 28.1509 69.1899V30.9097C28.1509 30.0458 28.4661 29.2981 29.0964 28.6668C29.7267 28.0354 30.4732 27.7197 31.3358 27.7197H88.6642C89.5268 27.7197 90.2732 28.0354 90.9036 28.6668C91.5339 29.2981 91.8491 30.0458 91.8491 30.9097Z" />
</svg>
`;

View File

@@ -1,13 +1,13 @@
import { MockProxy, mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import {
FakeUserDecryptionOptions as UserDecryptionOptions,
InternalUserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
@@ -37,7 +37,7 @@ describe("DefaultSetPasswordJitService", () => {
let kdfConfigService: MockProxy<KdfConfigService>;
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let organizationUserService: MockProxy<OrganizationUserService>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
beforeEach(() => {
@@ -47,7 +47,7 @@ describe("DefaultSetPasswordJitService", () => {
kdfConfigService = mock<KdfConfigService>();
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
organizationApiService = mock<OrganizationApiServiceAbstraction>();
organizationUserService = mock<OrganizationUserService>();
organizationUserApiService = mock<OrganizationUserApiService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
sut = new DefaultSetPasswordJitService(
@@ -57,7 +57,7 @@ describe("DefaultSetPasswordJitService", () => {
kdfConfigService,
masterPasswordService,
organizationApiService,
organizationUserService,
organizationUserApiService,
userDecryptionOptionsService,
);
});
@@ -170,7 +170,7 @@ describe("DefaultSetPasswordJitService", () => {
cryptoService.userKey$.mockReturnValue(of(userKey));
cryptoService.rsaEncrypt.mockResolvedValue(userKeyEncString);
organizationUserService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue(
organizationUserApiService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue(
undefined,
);
}
@@ -211,7 +211,9 @@ describe("DefaultSetPasswordJitService", () => {
expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId);
expect(cryptoService.rsaEncrypt).toHaveBeenCalledWith(userKey.key, orgPublicKey);
expect(organizationUserService.putOrganizationUserResetPasswordEnrollment).toHaveBeenCalled();
expect(
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).toHaveBeenCalled();
});
it("when handling reset password auto enroll, it should throw an error if organization keys are not found", async () => {
@@ -224,7 +226,7 @@ describe("DefaultSetPasswordJitService", () => {
// Act and Assert
await expect(sut.setPassword(credentials)).rejects.toThrow();
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment,
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).not.toHaveBeenCalled();
});
});

View File

@@ -1,10 +1,12 @@
import { firstValueFrom } from "rxjs";
import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@@ -31,7 +33,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
protected kdfConfigService: KdfConfigService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected organizationApiService: OrganizationApiServiceAbstraction,
protected organizationUserService: OrganizationUserService,
protected organizationUserApiService: OrganizationUserApiService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
) {}
@@ -161,7 +163,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
resetRequest.masterPasswordHash = masterKeyHash;
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
orgId,
userId,
resetRequest,

View File

@@ -96,4 +96,12 @@ export abstract class AuthRequestServiceAbstraction {
* @remark We should only be receiving approved push notifications to prevent enumeration.
*/
abstract sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => void;
/**
* Creates a dash-delimited fingerprint for use in confirming the `AuthRequest` between the requesting and approving device.
* @param email The email address of the user.
* @param publicKey The public key for the user.
* @returns The dash-delimited fingerprint phrase.
*/
abstract getFingerprintPhrase(email: string, publicKey: Uint8Array): Promise<string>;
}

View File

@@ -1,29 +1,28 @@
import { Observable } from "rxjs";
export abstract class LoginEmailServiceAbstraction {
/**
* An observable that monitors the loginEmail in memory.
* The loginEmail is the email that is being used in the current login process.
*/
loginEmail$: Observable<string | null>;
/**
* An observable that monitors the storedEmail on disk.
* This will return null if an account is being added.
*/
storedEmail$: Observable<string | null>;
/**
* Gets the current email being used in the login process from memory.
* @returns A string of the email.
* Sets the loginEmail in memory.
* The loginEmail is the email that is being used in the current login process.
*/
getEmail: () => string;
/**
* Sets the current email being used in the login process in memory.
* @param email The email to be set.
*/
setEmail: (email: string) => void;
setLoginEmail: (email: string) => Promise<void>;
/**
* Gets from memory whether or not the email should be stored on disk when `saveEmailSettings` is called.
* @returns A boolean stating whether or not the email should be stored on disk.
*/
getRememberEmail: () => boolean;
/**
* Sets in memory whether or not the email should be stored on disk when
* `saveEmailSettings` is called.
* Sets in memory whether or not the email should be stored on disk when `saveEmailSettings` is called.
*/
setRememberEmail: (value: boolean) => void;
/**

View File

@@ -27,6 +27,7 @@ describe("AuthRequestService", () => {
const apiService = mock<ApiService>();
let mockPrivateKey: Uint8Array;
let mockPublicKey: Uint8Array;
const mockUserId = Utils.newGuid() as UserId;
beforeEach(() => {
@@ -44,6 +45,7 @@ describe("AuthRequestService", () => {
);
mockPrivateKey = new Uint8Array(64);
mockPublicKey = new Uint8Array(64);
});
describe("authRequestPushNotification$", () => {
@@ -262,4 +264,14 @@ describe("AuthRequestService", () => {
expect(result.masterKeyHash).toEqual(mockDecryptedMasterKeyHash);
});
});
describe("getFingerprintPhrase", () => {
it("returns the same fingerprint regardless of email casing", () => {
const email = "test@email.com";
const emailUpperCase = email.toUpperCase();
const phrase = sut.getFingerprintPhrase(email, mockPublicKey);
const phraseUpperCase = sut.getFingerprintPhrase(emailUpperCase, mockPublicKey);
expect(phrase).toEqual(phraseUpperCase);
});
});
});

View File

@@ -198,4 +198,8 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
this.authRequestPushNotificationSubject.next(notification.id);
}
}
async getFingerprintPhrase(email: string, publicKey: Uint8Array): Promise<string> {
return (await this.cryptoService.getFingerprint(email.toLowerCase(), publicKey)).join("-");
}
}

View File

@@ -43,7 +43,7 @@ describe("LoginEmailService", () => {
describe("storedEmail$", () => {
it("returns the stored email when not adding an account", async () => {
sut.setEmail("userEmail@bitwarden.com");
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
@@ -53,7 +53,7 @@ describe("LoginEmailService", () => {
});
it("returns the stored email when not adding an account and the user has just logged in", async () => {
sut.setEmail("userEmail@bitwarden.com");
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
@@ -66,7 +66,7 @@ describe("LoginEmailService", () => {
});
it("returns null when adding an account", async () => {
sut.setEmail("userEmail@bitwarden.com");
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
@@ -83,7 +83,7 @@ describe("LoginEmailService", () => {
describe("saveEmailSettings", () => {
it("saves the email when not adding an account", async () => {
sut.setEmail("userEmail@bitwarden.com");
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
@@ -95,7 +95,7 @@ describe("LoginEmailService", () => {
it("clears the email when not adding an account and rememberEmail is false", async () => {
storedEmailState.stateSubject.next("initialEmail@bitwarden.com");
sut.setEmail("userEmail@bitwarden.com");
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(false);
await sut.saveEmailSettings();
@@ -110,7 +110,7 @@ describe("LoginEmailService", () => {
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
});
sut.setEmail("userEmail@bitwarden.com");
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
@@ -127,7 +127,7 @@ describe("LoginEmailService", () => {
["OtherUserId" as UserId]: AuthenticationStatus.Locked,
});
sut.setEmail("userEmail@bitwarden.com");
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(false);
await sut.saveEmailSettings();
@@ -140,11 +140,11 @@ describe("LoginEmailService", () => {
it("does not clear the email and rememberEmail after saving", async () => {
// Browser uses these values to maintain the email between login and 2fa components so
// we do not want to clear them too early.
sut.setEmail("userEmail@bitwarden.com");
await sut.setLoginEmail("userEmail@bitwarden.com");
sut.setRememberEmail(true);
await sut.saveEmailSettings();
const result = sut.getEmail();
const result = await firstValueFrom(sut.loginEmail$);
expect(result).toBe("userEmail@bitwarden.com");
});

View File

@@ -8,21 +8,28 @@ import {
GlobalState,
KeyDefinition,
LOGIN_EMAIL_DISK,
LOGIN_EMAIL_MEMORY,
StateProvider,
} from "../../../../../common/src/platform/state";
import { LoginEmailServiceAbstraction } from "../../abstractions/login-email.service";
export const LOGIN_EMAIL = new KeyDefinition<string>(LOGIN_EMAIL_MEMORY, "loginEmail", {
deserializer: (value: string) => value,
});
export const STORED_EMAIL = new KeyDefinition<string>(LOGIN_EMAIL_DISK, "storedEmail", {
deserializer: (value: string) => value,
});
export class LoginEmailService implements LoginEmailServiceAbstraction {
private email: string | null;
private rememberEmail: boolean;
// True if an account is currently being added through account switching
private readonly addingAccount$: Observable<boolean>;
private readonly loginEmailState: GlobalState<string>;
loginEmail$: Observable<string | null>;
private readonly storedEmailState: GlobalState<string>;
storedEmail$: Observable<string | null>;
@@ -31,6 +38,7 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
private authService: AuthService,
private stateProvider: StateProvider,
) {
this.loginEmailState = this.stateProvider.getGlobal(LOGIN_EMAIL);
this.storedEmailState = this.stateProvider.getGlobal(STORED_EMAIL);
// In order to determine if an account is being added, we check if any account is not logged out
@@ -46,6 +54,8 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
}),
);
this.loginEmail$ = this.loginEmailState.state$;
this.storedEmail$ = this.storedEmailState.state$.pipe(
switchMap(async (storedEmail) => {
// When adding an account, we don't show the stored email
@@ -57,12 +67,8 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
);
}
getEmail() {
return this.email;
}
setEmail(email: string) {
this.email = email;
async setLoginEmail(email: string) {
await this.loginEmailState.update((_) => email);
}
getRememberEmail() {
@@ -76,25 +82,27 @@ export class LoginEmailService implements LoginEmailServiceAbstraction {
// Note: only clear values on successful login or you are sure they are not needed.
// Browser uses these values to maintain the email between login and 2fa components so
// we do not want to clear them too early.
clearValues() {
this.email = null;
async clearValues() {
await this.setLoginEmail(null);
this.rememberEmail = false;
}
async saveEmailSettings() {
const addingAccount = await firstValueFrom(this.addingAccount$);
const email = await firstValueFrom(this.loginEmail$);
await this.storedEmailState.update((storedEmail) => {
// If we're adding an account, only overwrite the stored email when rememberEmail is true
if (addingAccount) {
if (this.rememberEmail) {
return this.email;
return email;
}
return storedEmail;
}
// Saving with rememberEmail set to false will clear the stored email
if (this.rememberEmail) {
return this.email;
return email;
}
return null;
});

View File

@@ -1,12 +0,0 @@
import { OrganizationUserType } from "../../../enums";
import { PermissionsApi } from "../../../models/api/permissions.api";
import { SelectionReadOnlyRequest } from "../../../models/request/selection-read-only.request";
export class OrganizationUserInviteRequest {
emails: string[] = [];
type: OrganizationUserType;
accessSecretsManager: boolean;
collections: SelectionReadOnlyRequest[] = [];
groups: string[];
permissions: PermissionsApi;
}

View File

@@ -1,11 +0,0 @@
import { OrganizationUserType } from "../../../enums";
import { PermissionsApi } from "../../../models/api/permissions.api";
import { SelectionReadOnlyRequest } from "../../../models/request/selection-read-only.request";
export class OrganizationUserUpdateRequest {
type: OrganizationUserType;
accessSecretsManager: boolean;
collections: SelectionReadOnlyRequest[] = [];
groups: string[] = [];
permissions: PermissionsApi;
}

View File

@@ -1,3 +1,3 @@
import { OrganizationUserBulkPublicKeyResponse } from "../../../abstractions/organization-user/responses";
import { OrganizationUserBulkPublicKeyResponse } from "@bitwarden/admin-console/common";
export class ProviderUserBulkPublicKeyResponse extends OrganizationUserBulkPublicKeyResponse {}

View File

@@ -1 +0,0 @@
export * from "./organization-user-bulk.request";

View File

@@ -1,4 +1,4 @@
import { OrganizationUserResetPasswordRequest } from "../../../admin-console/abstractions/organization-user/requests";
import { OrganizationUserResetPasswordRequest } from "@bitwarden/admin-console/common";
export class UpdateTdeOffboardingPasswordRequest extends OrganizationUserResetPasswordRequest {
masterPasswordHint: string;

View File

@@ -1,4 +1,4 @@
import { OrganizationUserResetPasswordRequest } from "../../../admin-console/abstractions/organization-user/requests";
import { OrganizationUserResetPasswordRequest } from "@bitwarden/admin-console/common";
export class UpdateTempPasswordRequest extends OrganizationUserResetPasswordRequest {
masterPasswordHint: string;

View File

@@ -1,9 +1,10 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { UserId } from "../../../../common/src/types/guid";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
@@ -17,7 +18,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let accountService: MockProxy<AccountService>;
let cryptoService: MockProxy<CryptoService>;
let organizationUserService: MockProxy<OrganizationUserService>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let i18nService: MockProxy<I18nService>;
let service: PasswordResetEnrollmentServiceImplementation;
@@ -26,13 +27,13 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
accountService = mock<AccountService>();
accountService.activeAccount$ = activeAccountSubject;
cryptoService = mock<CryptoService>();
organizationUserService = mock<OrganizationUserService>();
organizationUserApiService = mock<OrganizationUserApiService>();
i18nService = mock<I18nService>();
service = new PasswordResetEnrollmentServiceImplementation(
organizationApiService,
accountService,
cryptoService,
organizationUserService,
organizationUserApiService,
i18nService,
);
});
@@ -100,7 +101,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
await service.enroll("orgId");
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment,
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).toHaveBeenCalledWith(
"orgId",
"userId",
@@ -122,7 +123,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
await service.enroll("orgId", "userId", { key: "key" } as any);
expect(
organizationUserService.putOrganizationUserResetPasswordEnrollment,
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
).toHaveBeenCalledWith(
"orgId",
"userId",

View File

@@ -1,8 +1,11 @@
import { firstValueFrom, map } from "rxjs";
import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "../../admin-console/abstractions/organization-user/requests";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { Utils } from "../../platform/misc/utils";
@@ -17,7 +20,7 @@ export class PasswordResetEnrollmentServiceImplementation
protected organizationApiService: OrganizationApiServiceAbstraction,
protected accountService: AccountService,
protected cryptoService: CryptoService,
protected organizationUserService: OrganizationUserService,
protected organizationUserApiService: OrganizationUserApiService,
protected i18nService: I18nService,
) {}
@@ -49,7 +52,7 @@ export class PasswordResetEnrollmentServiceImplementation
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
organizationId,
userId,
resetRequest,

View File

@@ -1,8 +1,4 @@
import { Observable } from "rxjs";
export abstract class AppIdService {
abstract appId$: Observable<string>;
abstract anonymousAppId$: Observable<string>;
abstract getAppId(): Promise<string>;
abstract getAnonymousAppId(): Promise<string>;
}

View File

@@ -1,19 +1,18 @@
import { FakeGlobalState, FakeGlobalStateProvider, ObservableTracker } from "../../../spec";
import { mock } from "jest-mock-extended";
import { FakeStorageService } from "../../../spec";
import { LogService } from "../abstractions/log.service";
import { Utils } from "../misc/utils";
import { ANONYMOUS_APP_ID_KEY, APP_ID_KEY, AppIdService } from "./app-id.service";
describe("AppIdService", () => {
let globalStateProvider: FakeGlobalStateProvider;
let appIdState: FakeGlobalState<string>;
let anonymousAppIdState: FakeGlobalState<string>;
let fakeStorageService: FakeStorageService;
let sut: AppIdService;
beforeEach(() => {
globalStateProvider = new FakeGlobalStateProvider();
appIdState = globalStateProvider.getFake(APP_ID_KEY);
anonymousAppIdState = globalStateProvider.getFake(ANONYMOUS_APP_ID_KEY);
sut = new AppIdService(globalStateProvider);
fakeStorageService = new FakeStorageService();
sut = new AppIdService(fakeStorageService, mock<LogService>());
});
afterEach(() => {
@@ -22,7 +21,7 @@ describe("AppIdService", () => {
describe("getAppId", () => {
it("returns the existing appId when it exists", async () => {
appIdState.stateSubject.next("existingAppId");
fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: "existingAppId" });
const appId = await sut.getAppId();
@@ -30,7 +29,7 @@ describe("AppIdService", () => {
});
it("creates a new appId only once", async () => {
appIdState.stateSubject.next(null);
fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: null });
const appIds: string[] = [];
const promises = [async () => appIds.push(await sut.getAppId())];
@@ -41,7 +40,7 @@ describe("AppIdService", () => {
});
it.each([null, undefined])("returns a new appId when %s", async (value) => {
appIdState.stateSubject.next(value);
fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: value });
const appId = await sut.getAppId();
@@ -49,27 +48,17 @@ describe("AppIdService", () => {
});
it.each([null, undefined])("stores the new guid when %s", async (value) => {
appIdState.stateSubject.next(value);
fakeStorageService.internalUpdateStore({ [APP_ID_KEY]: value });
const appId = await sut.getAppId();
expect(appIdState.nextMock).toHaveBeenCalledWith(appId);
});
it("emits only once when creating a new appId", async () => {
appIdState.stateSubject.next(null);
const tracker = new ObservableTracker(sut.appId$);
const appId = await sut.getAppId();
expect(tracker.emissions).toEqual([appId]);
await expect(tracker.pauseUntilReceived(2, 50)).rejects.toThrow("Timeout exceeded");
expect(fakeStorageService.mock.save).toHaveBeenCalledWith(APP_ID_KEY, appId, undefined);
});
});
describe("getAnonymousAppId", () => {
it("returns the existing appId when it exists", async () => {
anonymousAppIdState.stateSubject.next("existingAppId");
fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: "existingAppId" });
const appId = await sut.getAnonymousAppId();
@@ -77,7 +66,7 @@ describe("AppIdService", () => {
});
it("creates a new anonymousAppId only once", async () => {
anonymousAppIdState.stateSubject.next(null);
fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: null });
const appIds: string[] = [];
const promises = [async () => appIds.push(await sut.getAnonymousAppId())];
@@ -88,7 +77,7 @@ describe("AppIdService", () => {
});
it.each([null, undefined])("returns a new appId when it does not exist", async (value) => {
anonymousAppIdState.stateSubject.next(value);
fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: value });
const appId = await sut.getAnonymousAppId();
@@ -98,22 +87,16 @@ describe("AppIdService", () => {
it.each([null, undefined])(
"stores the new guid when it an existing one is not found",
async (value) => {
anonymousAppIdState.stateSubject.next(value);
fakeStorageService.internalUpdateStore({ [ANONYMOUS_APP_ID_KEY]: value });
const appId = await sut.getAnonymousAppId();
expect(anonymousAppIdState.nextMock).toHaveBeenCalledWith(appId);
expect(fakeStorageService.mock.save).toHaveBeenCalledWith(
ANONYMOUS_APP_ID_KEY,
appId,
undefined,
);
},
);
it("emits only once when creating a new anonymousAppId", async () => {
anonymousAppIdState.stateSubject.next(null);
const tracker = new ObservableTracker(sut.anonymousAppId$);
const appId = await sut.getAnonymousAppId();
expect(tracker.emissions).toEqual([appId]);
await expect(tracker.pauseUntilReceived(2, 50)).rejects.toThrow("Timeout exceeded");
});
});
});

View File

@@ -1,59 +1,34 @@
import { Observable, concatMap, distinctUntilChanged, firstValueFrom, share } from "rxjs";
import { AppIdService as AppIdServiceAbstraction } from "../abstractions/app-id.service";
import { LogService } from "../abstractions/log.service";
import { AbstractStorageService } from "../abstractions/storage.service";
import { Utils } from "../misc/utils";
import { APPLICATION_ID_DISK, GlobalStateProvider, KeyDefinition } from "../state";
export const APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "appId", {
deserializer: (value: string) => value,
cleanupDelayMs: 0,
debug: {
enableRetrievalLogging: true,
enableUpdateLogging: true,
},
});
export const ANONYMOUS_APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "anonymousAppId", {
deserializer: (value: string) => value,
});
export const APP_ID_KEY = "global_applicationId_appId";
export const ANONYMOUS_APP_ID_KEY = "global_applicationId_appId";
export class AppIdService implements AppIdServiceAbstraction {
appId$: Observable<string>;
anonymousAppId$: Observable<string>;
constructor(globalStateProvider: GlobalStateProvider) {
const appIdState = globalStateProvider.get(APP_ID_KEY);
const anonymousAppIdState = globalStateProvider.get(ANONYMOUS_APP_ID_KEY);
this.appId$ = appIdState.state$.pipe(
concatMap(async (appId) => {
if (!appId) {
return await appIdState.update(() => Utils.newGuid(), {
shouldUpdate: (v) => v == null,
});
}
return appId;
}),
distinctUntilChanged(),
share(),
);
this.anonymousAppId$ = anonymousAppIdState.state$.pipe(
concatMap(async (appId) => {
if (!appId) {
return await anonymousAppIdState.update(() => Utils.newGuid(), {
shouldUpdate: (v) => v == null,
});
}
return appId;
}),
distinctUntilChanged(),
share(),
);
}
constructor(
private readonly storageService: AbstractStorageService,
private readonly logService: LogService,
) {}
async getAppId(): Promise<string> {
return await firstValueFrom(this.appId$);
this.logService.info("Retrieving application id");
return await this.getEnsuredValue(APP_ID_KEY);
}
async getAnonymousAppId(): Promise<string> {
return await firstValueFrom(this.anonymousAppId$);
return await this.getEnsuredValue(ANONYMOUS_APP_ID_KEY);
}
private async getEnsuredValue(key: string) {
let value = await this.storageService.get<string | null>(key);
if (value == null) {
value = Utils.newGuid();
await this.storageService.save(key, value);
}
return value;
}
}

View File

@@ -945,6 +945,10 @@ export class CryptoService implements CryptoServiceAbstraction {
}
private async derivePublicKey(privateKey: UserPrivateKey) {
if (privateKey == null) {
return null;
}
return (await this.cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey;
}

View File

@@ -53,6 +53,7 @@ export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
web: "disk-local",
});
export const LOGIN_EMAIL_MEMORY = new StateDefinition("loginEmail", "memory");
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");

View File

@@ -2,6 +2,7 @@ import { Jsonify } from "type-fest";
import { CardLinkedId as LinkedId } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator";
import { normalizeExpiryYearFormat } from "../../utils";
import { ItemView } from "./item.view";
@@ -65,17 +66,16 @@ export class CardView extends ItemView {
}
get expiration(): string {
if (!this.expMonth && !this.expYear) {
const normalizedYear = normalizeExpiryYearFormat(this.expYear);
if (!this.expMonth && !normalizedYear) {
return null;
}
let exp = this.expMonth != null ? ("0" + this.expMonth).slice(-2) : "__";
exp += " / " + (this.expYear != null ? this.formatYear(this.expYear) : "____");
return exp;
}
exp += " / " + (normalizedYear || "____");
private formatYear(year: string): string {
return year.length === 2 ? "20" + year : year;
return exp;
}
static fromJSON(obj: Partial<Jsonify<CardView>>): CardView {

View File

@@ -0,0 +1,122 @@
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { normalizeExpiryYearFormat, isCardExpired } from "@bitwarden/common/vault/utils";
function getExpiryYearValueFormats(currentCentury: string) {
return [
[-12, `${currentCentury}12`],
[0, `${currentCentury}00`],
[2043, "2043"], // valid year with a length of four should be taken directly
[24, `${currentCentury}24`],
[3054, "3054"], // valid year with a length of four should be taken directly
[31423524543, `${currentCentury}43`],
[4, `${currentCentury}04`],
[null, null],
[undefined, null],
["-12", `${currentCentury}12`],
["", null],
["0", `${currentCentury}00`],
["00", `${currentCentury}00`],
["000", `${currentCentury}00`],
["0000", `${currentCentury}00`],
["00000", `${currentCentury}00`],
["0234234", `${currentCentury}34`],
["04", `${currentCentury}04`],
["2043", "2043"], // valid year with a length of four should be taken directly
["24", `${currentCentury}24`],
["3054", "3054"], // valid year with a length of four should be taken directly
["31423524543", `${currentCentury}43`],
["4", `${currentCentury}04`],
["aaaa", null],
["adgshsfhjsdrtyhsrth", null],
["agdredg42grg35grrr. ea3534@#^145345ag$%^ -_#$rdg ", `${currentCentury}45`],
];
}
describe("normalizeExpiryYearFormat", () => {
const currentCentury = `${new Date().getFullYear()}`.slice(0, 2);
const expiryYearValueFormats = getExpiryYearValueFormats(currentCentury);
expiryYearValueFormats.forEach(([inputValue, expectedValue]) => {
it(`should return '${expectedValue}' when '${inputValue}' is passed`, () => {
const formattedValue = normalizeExpiryYearFormat(inputValue);
expect(formattedValue).toEqual(expectedValue);
});
});
describe("in the year 3107", () => {
const theDistantFuture = new Date(Date.UTC(3107, 1, 1));
jest.spyOn(Date, "now").mockReturnValue(theDistantFuture.valueOf());
beforeAll(() => {
jest.useFakeTimers({ advanceTimers: true });
jest.setSystemTime(theDistantFuture);
});
afterAll(() => {
jest.useRealTimers();
});
const currentCentury = `${new Date(Date.now()).getFullYear()}`.slice(0, 2);
expect(currentCentury).toBe("31");
const expiryYearValueFormats = getExpiryYearValueFormats(currentCentury);
expiryYearValueFormats.forEach(([inputValue, expectedValue]) => {
it(`should return '${expectedValue}' when '${inputValue}' is passed`, () => {
const formattedValue = normalizeExpiryYearFormat(inputValue);
expect(formattedValue).toEqual(expectedValue);
});
});
jest.clearAllTimers();
});
});
function getCardExpiryDateValues() {
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
// `Date` months are zero-indexed, our expiry date month inputs are one-indexed
const currentMonth = currentDate.getMonth() + 1;
return [
[null, null, false], // no month, no year
[undefined, undefined, false], // no month, no year, invalid values
["", "", false], // no month, no year, invalid values
["12", "agdredg42grg35grrr. ea3534@#^145345ag$%^ -_#$rdg ", false], // invalid values
["0", `${currentYear - 1}`, true], // invalid 0 month
["00", `${currentYear + 1}`, false], // invalid 0 month
[`${currentMonth}`, "0000", true], // current month, in the year 2000
[null, `${currentYear}`.slice(-2), false], // no month, this year
[null, `${currentYear - 1}`.slice(-2), true], // no month, last year
["1", null, false], // no year, January
["1", `${currentYear - 1}`, true], // January last year
["13", `${currentYear}`, false], // 12 + 1 is Feb. in the next year (Date is zero-indexed)
[`${currentMonth + 36}`, `${currentYear - 1}`, true], // even though the month value would put the date 3 years into the future when calculated with `Date`, an explicit year in the past indicates the card is expired
[`${currentMonth}`, `${currentYear}`, false], // this year, this month (not expired until the month is over)
[`${currentMonth}`, `${currentYear}`.slice(-2), false], // This month, this year (not expired until the month is over)
[`${currentMonth - 1}`, `${currentYear}`, true], // last month
[`${currentMonth - 1}`, `${currentYear + 1}`, false], // 11 months from now
];
}
describe("isCardExpired", () => {
const expiryYearValueFormats = getCardExpiryDateValues();
expiryYearValueFormats.forEach(
([inputMonth, inputYear, expectedValue]: [string | null, string | null, boolean]) => {
it(`should return ${expectedValue} when the card expiry month is ${inputMonth} and the card expiry year is ${inputYear}`, () => {
const testCardView = new CardView();
testCardView.expMonth = inputMonth;
testCardView.expYear = inputYear;
const cardIsExpired = isCardExpired(testCardView);
expect(cardIsExpired).toBe(expectedValue);
});
},
);
});

View File

@@ -0,0 +1,83 @@
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
type NonZeroIntegers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Year = `${NonZeroIntegers}${NonZeroIntegers}${0 | NonZeroIntegers}${0 | NonZeroIntegers}`;
/**
* Takes a string or number value and returns a string value formatted as a valid 4-digit year
*
* @export
* @param {(string | number)} yearInput
* @return {*} {(Year | null)}
*/
export function normalizeExpiryYearFormat(yearInput: string | number): Year | null {
// The input[type="number"] is returning a number, convert it to a string
// An empty field returns null, avoid casting `"null"` to a string
const yearInputIsEmpty = yearInput == null || yearInput === "";
let expirationYear = yearInputIsEmpty ? null : `${yearInput}`;
// Exit early if year is already formatted correctly or empty
if (yearInputIsEmpty || /^[1-9]{1}\d{3}$/.test(expirationYear)) {
return expirationYear as Year;
}
expirationYear = expirationYear
// For safety, because even input[type="number"] will allow decimals
.replace(/[^\d]/g, "")
// remove any leading zero padding (leave the last leading zero if it ends the string)
.replace(/^[0]+(?=.)/, "");
if (expirationYear === "") {
expirationYear = null;
}
// given the context of payment card expiry, a year character length of 3, or over 4
// is more likely to be a mistake than an intentional value for the far past or far future.
if (expirationYear && expirationYear.length !== 4) {
const paddedYear = ("00" + expirationYear).slice(-2);
const currentCentury = `${new Date().getFullYear()}`.slice(0, 2);
expirationYear = currentCentury + paddedYear;
}
return expirationYear as Year | null;
}
/**
* Takes a cipher card view and returns "true" if the month and year affirmativey indicate
* the card is expired.
*
* @export
* @param {CardView} cipherCard
* @return {*} {boolean}
*/
export function isCardExpired(cipherCard: CardView): boolean {
if (cipherCard) {
const { expMonth = null, expYear = null } = cipherCard;
const now = new Date();
const normalizedYear = normalizeExpiryYearFormat(expYear);
// If the card year is before the current year, don't bother checking the month
if (normalizedYear && parseInt(normalizedYear) < now.getFullYear()) {
return true;
}
if (normalizedYear && expMonth) {
// `Date` months are zero-indexed
const parsedMonth =
parseInt(expMonth) - 1 ||
// Add a month floor of 0 to protect against an invalid low month value of "0"
0;
const parsedYear = parseInt(normalizedYear);
// First day of the next month minus one, to get last day of the card month
const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0);
return cardExpiry < now;
}
}
return false;
}

View File

@@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component } from "@angular/core";
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class:
"tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 tw-rounded-lg tw-py-4 tw-px-3",
"tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg tw-py-4 tw-px-3",
},
})
export class CardComponent {}

View File

@@ -1,7 +1,12 @@
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LayoutComponent } from "../layout";
import { SectionComponent } from "../section";
import { TypographyModule } from "../typography";
import { I18nMockService } from "../utils/i18n-mock.service";
import { CardComponent } from "./card.component";
@@ -10,7 +15,20 @@ export default {
component: CardComponent,
decorators: [
moduleMetadata({
imports: [TypographyModule, SectionComponent],
imports: [TypographyModule, SectionComponent, LayoutComponent, RouterTestingModule],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
toggleSideNavigation: "Toggle side navigation",
skipToContent: "Skip to content",
submenu: "submenu",
toggleCollapse: "toggle collapse",
});
},
},
],
}),
componentWrapperDecorator(
(story) => `<div class="tw-bg-background-alt tw-p-10 tw-text-main">${story}</div>`,
@@ -60,3 +78,16 @@ export const WithinSections: Story = {
`,
}),
};
export const WithoutBorderRadius: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-layout>
<bit-card>
<p bitTypography="body1" class="!tw-mb-0">Cards used in <code>bit-layout</code> will not have a border radius</p>
</bit-card>
</bit-layout>
`,
}),
};

View File

@@ -5,7 +5,7 @@ import {
DialogRef,
DIALOG_SCROLL_STRATEGY,
} from "@angular/cdk/dialog";
import { ComponentType, Overlay, OverlayContainer } from "@angular/cdk/overlay";
import { ComponentType, Overlay, OverlayContainer, ScrollStrategy } from "@angular/cdk/overlay";
import {
Inject,
Injectable,
@@ -25,12 +25,35 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component";
import { SimpleDialogOptions, Translation } from "./simple-dialog/types";
/**
* The default `BlockScrollStrategy` does not work well with virtual scrolling.
*
* https://github.com/angular/components/issues/7390
*/
class CustomBlockScrollStrategy implements ScrollStrategy {
enable() {
document.body.classList.add("tw-overflow-hidden");
}
disable() {
document.body.classList.remove("tw-overflow-hidden");
}
/** Noop */
attach() {}
/** Noop */
detach() {}
}
@Injectable()
export class DialogService extends Dialog implements OnDestroy {
private _destroy$ = new Subject<void>();
private backDropClasses = ["tw-fixed", "tw-bg-black", "tw-bg-opacity-30", "tw-inset-0"];
private defaultScrollStrategy = new CustomBlockScrollStrategy();
constructor(
/** Parent class constructor */
_overlay: Overlay,
@@ -73,6 +96,7 @@ export class DialogService extends Dialog implements OnDestroy {
): DialogRef<R, C> {
config = {
backdropClass: this.backDropClasses,
scrollStrategy: this.defaultScrollStrategy,
...config,
};

View File

@@ -1,6 +1,7 @@
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import {
AfterContentChecked,
booleanAttribute,
Component,
ContentChild,
ContentChildren,
@@ -38,6 +39,13 @@ export class BitFormFieldComponent implements AfterContentChecked {
return this._disableMargin;
}
/**
* NOTE: Placeholder to match the API of the form-field component in the `ps/extension` branch,
* no functionality is implemented as of now.
*/
@Input({ transform: booleanAttribute })
disableReadOnlyBorder = false;
@HostBinding("class")
get classList() {
return ["tw-block"].concat(this.disableMargin ? [] : ["tw-mb-6"]);

View File

@@ -1,6 +1,6 @@
<!-- TODO: Colors will be finalized in the extension refresh feature branch -->
<div
class="tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-300/20 tw-text-main tw-border-solid tw-border-b tw-border-0 tw-rounded-lg tw-mb-1.5"
class="tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-300/20 tw-text-main tw-border-solid tw-border-b tw-border-0 [&:not(bit-layout_*)]:tw-rounded-lg tw-mb-1.5"
[ngClass]="
focusVisibleWithin()
? 'tw-z-10 tw-rounded tw-outline-none tw-ring tw-ring-primary-600 tw-border-transparent'

View File

@@ -1,4 +1,4 @@
import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs";
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
import * as stories from "./item.stories";
@@ -15,9 +15,17 @@ import { ItemModule } from "@bitwarden/components";
It is a generic container that can be used for either standalone content, an alternative to tables,
or to list nav links.
<Canvas>
<Story of={stories.Default} />
</Canvas>
<Story of={stories.Default} />
<br />
Items used within a parent `bit-layout` component will not have a border radius, since the
`bit-layout` background is white.
<Story of={stories.WithoutBorderRadius} />
<br />
<br />
## Primary Content
@@ -41,9 +49,7 @@ The content can be a button, anchor, or static container.
</bit-item>
```
<Canvas>
<Story of={stories.ContentTypes} />
</Canvas>
<Story of={stories.ContentTypes} />
### Content Slots
@@ -74,9 +80,7 @@ The content can be a button, anchor, or static container.
</bit-item>
```
<Canvas>
<Story of={stories.ContentSlots} />
</Canvas>
<Story of={stories.ContentSlots} />
## Secondary Actions
@@ -109,13 +113,9 @@ Actions are commonly icon buttons or badge buttons.
Groups of items can be associated by wrapping them in the `<bit-item-group>`.
<Canvas>
<Story of={stories.MultipleActionList} />
</Canvas>
<Story of={stories.MultipleActionList} />
<Canvas>
<Story of={stories.SingleActionList} />
</Canvas>
<Story of={stories.SingleActionList} />
### A11y
@@ -136,6 +136,4 @@ Use `aria-label` or `aria-labelledby` to give groups an accessible name.
### Virtual Scrolling
<Canvas>
<Story of={stories.VirtualScrolling} />
</Canvas>
<Story of={stories.VirtualScrolling} />

View File

@@ -1,12 +1,17 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import { RouterTestingModule } from "@angular/router/testing";
import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { A11yGridDirective } from "../a11y/a11y-grid.directive";
import { AvatarModule } from "../avatar";
import { BadgeModule } from "../badge";
import { IconButtonModule } from "../icon-button";
import { LayoutComponent } from "../layout";
import { TypographyModule } from "../typography";
import { I18nMockService } from "../utils/i18n-mock.service";
import { ItemActionComponent } from "./item-action.component";
import { ItemContentComponent } from "./item-content.component";
@@ -29,6 +34,21 @@ export default {
ItemContentComponent,
A11yGridDirective,
ScrollingModule,
LayoutComponent,
RouterTestingModule,
],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
toggleSideNavigation: "Toggle side navigation",
skipToContent: "Skip to content",
submenu: "submenu",
toggleCollapse: "toggle collapse",
});
},
},
],
}),
componentWrapperDecorator((story) => `<div class="tw-bg-background-alt tw-p-2">${story}</div>`),
@@ -333,3 +353,32 @@ export const VirtualScrolling: Story = {
`,
}),
};
export const WithoutBorderRadius: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-layout>
<bit-item>
<button bit-item-content>
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
Foo
<span slot="secondary">Bar</span>
</button>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitBadge variant="primary">Auto-fill</button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
</bit-item-action>
</ng-container>
</bit-item>
</bit-layout>
`,
}),
};

View File

@@ -0,0 +1,61 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { Component, OnInit } from "@angular/core";
import { DialogModule, DialogService } from "../../../dialog";
import { IconButtonModule } from "../../../icon-button";
import { SectionComponent } from "../../../section";
import { TableDataSource, TableModule } from "../../../table";
@Component({
selector: "dialog-virtual-scroll-block",
standalone: true,
imports: [DialogModule, IconButtonModule, SectionComponent, TableModule, ScrollingModule],
template: ` <bit-section>
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th bitCell bitSortable="id" default>Id</th>
<th bitCell bitSortable="name">Name</th>
<th bitCell>Options</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *cdkVirtualFor="let r of rows$">
<td bitCell>{{ r.id }}</td>
<td bitCell>{{ r.name }}</td>
<td bitCell>
<button
bitIconButton="bwi-ellipsis-v"
type="button"
aria-label="Options"
(click)="openDefaultDialog()"
></button>
</td>
</tr>
</ng-template>
</bit-table>
</cdk-virtual-scroll-viewport>
</bit-section>`,
})
export class DialogVirtualScrollBlockComponent implements OnInit {
constructor(public dialogService: DialogService) {}
protected dataSource = new TableDataSource<{ id: number; name: string; other: string }>();
ngOnInit(): void {
this.dataSource.data = [...Array(100).keys()].map((i) => ({
id: i,
name: `name-${i}`,
other: `other-${i}`,
}));
}
async openDefaultDialog() {
await this.dialogService.openSimpleDialog({
type: "info",
title: "Foo",
content: "Bar",
});
}
}

View File

@@ -8,7 +8,15 @@ import {
componentWrapperDecorator,
moduleMetadata,
} from "@storybook/angular";
import { userEvent, getAllByRole, getByRole, getByLabelText, fireEvent } from "@storybook/test";
import {
userEvent,
getAllByRole,
getByRole,
getByLabelText,
fireEvent,
getByText,
getAllByLabelText,
} from "@storybook/test";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -16,6 +24,7 @@ import { DialogService } from "../../dialog";
import { LayoutComponent } from "../../layout";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { DialogVirtualScrollBlockComponent } from "./components/dialog-virtual-scroll-block.component";
import { KitchenSinkForm } from "./components/kitchen-sink-form.component";
import { KitchenSinkMainComponent } from "./components/kitchen-sink-main.component";
import { KitchenSinkTable } from "./components/kitchen-sink-table.component";
@@ -64,7 +73,9 @@ export default {
skipToContent: "Skip to content",
submenu: "submenu",
toggleCollapse: "toggle collapse",
toggleSideNavigation: "toggle side navigation",
toggleSideNavigation: "Toggle side navigation",
yes: "Yes",
no: "No",
});
},
},
@@ -78,6 +89,7 @@ export default {
[
{ path: "", redirectTo: "bitwarden", pathMatch: "full" },
{ path: "bitwarden", component: KitchenSinkMainComponent },
{ path: "virtual-scroll", component: DialogVirtualScrollBlockComponent },
],
{ useHash: true },
),
@@ -100,6 +112,7 @@ export const Default: Story = {
<bit-nav-item text="Bitwarden" route="bitwarden"></bit-nav-item>
<bit-nav-divider></bit-nav-divider>
</bit-nav-group>
<bit-nav-item text="Virtual Scroll" route="virtual-scroll"></bit-nav-item>
</bit-nav-group>
</bit-side-nav>
<router-outlet></router-outlet>
@@ -165,3 +178,19 @@ export const EmptyTab: Story = {
await userEvent.click(emptyTab);
},
};
export const VirtualScrollBlockingDialog: Story = {
...Default,
play: async (context) => {
const canvas = context.canvasElement;
const navItem = getByText(canvas, "Virtual Scroll");
await userEvent.click(navItem);
const htmlEl = canvas.ownerDocument.documentElement;
htmlEl.scrollTop = 2000;
const dialogButton = getAllByLabelText(canvas, "Options")[0];
await userEvent.click(dialogButton);
},
};

View File

@@ -49,6 +49,8 @@
--color-text-code: 192 17 118;
--color-text-headers: 2 15 102;
--color-marketing-logo: 23 93 220;
--tw-ring-offset-color: #ffffff;
}
@@ -95,6 +97,8 @@
--color-text-code: 240 141 199;
--color-text-headers: 226 227 228;
--color-marketing-logo: 255 255 255;
--tw-ring-offset-color: #1f242e;
}
@@ -134,6 +138,8 @@
--color-text-alt2: 255 255 255;
--color-text-code: 219 177 211;
--color-marketing-logo: 255 255 255;
--tw-ring-offset-color: #434c5e;
}
@@ -173,6 +179,8 @@
--color-text-alt2: 255 255 255;
--color-text-code: 240 141 199;
--color-marketing-logo: 255 255 255;
--tw-ring-offset-color: #002b36;
}

View File

@@ -69,6 +69,7 @@ module.exports = {
alt3: rgba("--color-background-alt3"),
alt4: rgba("--color-background-alt4"),
},
"marketing-logo": rgba("--color-marketing-logo"),
},
textColor: {
main: rgba("--color-text-main"),

View File

@@ -0,0 +1,113 @@
import { CipherType } from "@bitwarden/common/vault/enums";
import { MSecureCsvImporter } from "../src/importers/msecure-csv-importer";
describe("MSecureCsvImporter.parse", () => {
let importer: MSecureCsvImporter;
beforeEach(() => {
importer = new MSecureCsvImporter();
});
it("should correctly parse credit card entries as Secret Notes", async () => {
const mockCsvData =
`myCreditCard|155089404,Credit Card,,,Card Number|12|41111111111111111,Expiration Date|11|05/2026,Security Code|9|123,Name on Card|0|John Doe,PIN|9|1234,Issuing Bank|0|Visa,Phone Number|4|,Billing Address|0|,`.trim();
const result = await importer.parse(mockCsvData);
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(1);
const cipher = result.ciphers[0];
expect(cipher.name).toBe("myCreditCard");
expect(cipher.type).toBe(CipherType.Card);
expect(cipher.card.number).toBe("41111111111111111");
expect(cipher.card.expiration).toBe("05 / 2026");
expect(cipher.card.code).toBe("123");
expect(cipher.card.cardholderName).toBe("John Doe");
expect(cipher.card.brand).toBe("Visa");
});
it("should correctly parse login entries", async () => {
const mockCsvData = `
Bitwarden|810974637,Login,,,Website|2|bitwarden.com,Username|7|bitwarden user,Password|8|bitpassword,
`.trim();
const result = await importer.parse(mockCsvData);
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(1);
const cipher = result.ciphers[0];
expect(cipher.name).toBe("Bitwarden");
expect(cipher.type).toBe(CipherType.Login);
expect(cipher.login.username).toBe("bitwarden user");
expect(cipher.login.password).toBe("bitpassword");
expect(cipher.login.uris[0].uri).toContain("bitwarden.com");
});
it("should correctly parse login entries with notes", async () => {
const mockCsvData =
`Example|188987444,Login,,This is a note |,Website|2|example2.com,Username|7|username || lol,Password|8|this is a password,`.trim();
const result = await importer.parse(mockCsvData);
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(1);
const cipher = result.ciphers[0];
expect(cipher.name).toBe("Example");
expect(cipher.type).toBe(CipherType.Login);
expect(cipher.login.username).toBe("username || lol");
expect(cipher.login.password).toBe("this is a password");
expect(cipher.login.uris[0].uri).toContain("example2.com");
expect(cipher.notes).toBe("This is a note |");
});
it("should correctly parse login entries with a tag", async () => {
const mockCsvData = `
Website with a tag|1401978655,Login,tag holding it,,Website|2|johndoe.com,Username|7|JohnDoeWebsite,Password|8|JohnDoePassword,
`.trim();
const result = await importer.parse(mockCsvData);
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(1);
const cipher = result.ciphers[0];
expect(cipher.name).toBe("Website with a tag");
expect(cipher.type).toBe(CipherType.Login);
expect(cipher.login.username).toBe("JohnDoeWebsite");
expect(cipher.login.password).toBe("JohnDoePassword");
expect(cipher.login.uris[0].uri).toContain("johndoe.com");
expect(cipher.notes).toBeNull();
expect(result.folders[0].name).toContain("tag holding it");
});
it("should handle multiple entries correctly", async () => {
const mockCsvData =
`myCreditCard|155089404,Credit Card,,,Card Number|12|41111111111111111,Expiration Date|11|05/2026,Security Code|9|123,Name on Card|0|John Doe,PIN|9|1234,Issuing Bank|0|Visa,Phone Number|4|,Billing Address|0|,
Bitwarden|810974637,Login,,,Website|2|bitwarden.com,Username|7|bitwarden user,Password|8|bitpassword,
Example|188987444,Login,,This is a note |,Website|2|example2.com,Username|7|username || lol,Password|8|this is a password,
Website with a tag|1401978655,Login,tag holding it,,Website|2|johndoe.com,Username|7|JohnDoeWebsite,Password|8|JohnDoePassword,`.trim();
const result = await importer.parse(mockCsvData);
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(4);
// Check first entry (Credit Card)
const cipher1 = result.ciphers[0];
expect(cipher1.name).toBe("myCreditCard");
expect(cipher1.type).toBe(CipherType.Card);
// Check second entry (Login - Bitwarden)
const cipher2 = result.ciphers[1];
expect(cipher2.name).toBe("Bitwarden");
expect(cipher2.type).toBe(CipherType.Login);
// Check third entry (Login with note - Example)
const cipher3 = result.ciphers[2];
expect(cipher3.name).toBe("Example");
expect(cipher3.type).toBe(CipherType.Login);
// Check fourth entry (Login with tag - Website with a tag)
const cipher4 = result.ciphers[3];
expect(cipher4.name).toBe("Website with a tag");
expect(cipher4.type).toBe(CipherType.Login);
});
});

View File

@@ -389,7 +389,7 @@
<div [hidden]="showLastPassOptions">
<bit-form-field>
<bit-label>{{ "selectImportFile" | i18n }}</bit-label>
<div class="file-selector">
<div class="file-selector tw-pt-2 tw-pb-1">
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>

View File

@@ -11,6 +11,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
import { ImportResult } from "../models/import-result";
@@ -263,7 +264,8 @@ export abstract class BaseImporter {
cipher.card.expMonth = expiryMatch.groups.month;
const year: string = expiryMatch.groups.year;
cipher.card.expYear = year.length === 2 ? "20" + year : year;
cipher.card.expYear = normalizeExpiryYearFormat(year);
return true;
}

View File

@@ -9,7 +9,7 @@ import { Importer } from "./importer";
export class MSecureCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
const result = new ImportResult();
const results = this.parseCsv(data, false);
const results = this.parseCsv(data, false, { delimiter: "," });
if (results == null) {
result.success = false;
return Promise.resolve(result);
@@ -21,17 +21,43 @@ export class MSecureCsvImporter extends BaseImporter implements Importer {
}
const folderName =
this.getValueOrDefault(value[0], "Unassigned") !== "Unassigned" ? value[0] : null;
this.getValueOrDefault(value[2], "Unassigned") !== "Unassigned" ? value[2] : null;
this.processFolder(result, folderName);
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(value[2], "--");
cipher.name = this.getValueOrDefault(value[0].split("|")[0], "--");
if (value[1] === "Web Logins" || value[1] === "Login") {
cipher.login.uris = this.makeUriArray(value[4]);
cipher.login.username = this.getValueOrDefault(value[5]);
cipher.login.password = this.getValueOrDefault(value[6]);
cipher.login.username = this.getValueOrDefault(this.splitValueRetainingLastPart(value[5]));
cipher.login.uris = this.makeUriArray(this.splitValueRetainingLastPart(value[4]));
cipher.login.password = this.getValueOrDefault(this.splitValueRetainingLastPart(value[6]));
cipher.notes = !this.isNullOrWhitespace(value[3]) ? value[3].split("\\n").join("\n") : null;
} else if (value[1] === "Credit Card") {
cipher.type = CipherType.Card;
cipher.card.number = this.getValueOrDefault(this.splitValueRetainingLastPart(value[4]));
const [month, year] = this.getValueOrDefault(
this.splitValueRetainingLastPart(value[5]),
).split("/");
cipher.card.expMonth = month.trim();
cipher.card.expYear = year.trim();
cipher.card.code = this.getValueOrDefault(this.splitValueRetainingLastPart(value[6]));
cipher.card.cardholderName = this.getValueOrDefault(
this.splitValueRetainingLastPart(value[7]),
);
cipher.card.brand = this.getValueOrDefault(this.splitValueRetainingLastPart(value[9]));
cipher.notes =
this.getValueOrDefault(value[8].split("|")[0]) +
": " +
this.getValueOrDefault(this.splitValueRetainingLastPart(value[8]), "") +
"\n" +
this.getValueOrDefault(value[10].split("|")[0]) +
": " +
this.getValueOrDefault(this.splitValueRetainingLastPart(value[10]), "") +
"\n" +
this.getValueOrDefault(value[11].split("|")[0]) +
": " +
this.getValueOrDefault(this.splitValueRetainingLastPart(value[11]), "");
} else if (value.length > 3) {
cipher.type = CipherType.SecureNote;
cipher.secureNote = new SecureNoteView();
@@ -43,7 +69,11 @@ export class MSecureCsvImporter extends BaseImporter implements Importer {
}
}
if (!this.isNullOrWhitespace(value[1]) && cipher.type !== CipherType.Login) {
if (
!this.isNullOrWhitespace(value[1]) &&
cipher.type !== CipherType.Login &&
cipher.type !== CipherType.Card
) {
cipher.name = value[1] + ": " + cipher.name;
}
@@ -58,4 +88,11 @@ export class MSecureCsvImporter extends BaseImporter implements Importer {
result.success = true;
return Promise.resolve(result);
}
// mSecure returns values separated by "|" where after the second separator is the value
// like "Password|8|myPassword", we want to keep the "myPassword" but also ensure that if
// the value contains any "|" it works fine
private splitValueRetainingLastPart(value: string) {
return value.split("|").slice(0, 2).concat(value.split("|").slice(2).join("|")).pop();
}
}

View File

@@ -3,7 +3,7 @@
"compilerOptions": {
"resolveJsonModule": true,
"paths": {
"@bitwarden/admin-console": ["../admin-console/src"],
"@bitwarden/admin-console/common": ["../admin-console/src/common"],
"@bitwarden/angular/*": ["../angular/src/*"],
"@bitwarden/auth/common": ["../auth/src/common"],
"@bitwarden/auth/angular": ["../auth/src/angular"],

View File

@@ -1,6 +1,6 @@
<bit-section *ngIf="sends?.length > 0">
<bit-section-header>
<h2 class="tw-font-bold" bitTypography="h5">
<h2 class="tw-font-bold" bitTypography="h6">
{{ headerText }}
</h2>
<span bitTypography="body1" slot="end">{{ sends.length }}</span>
@@ -10,6 +10,8 @@
<button
bit-item-content
appA11yTitle="{{ 'edit' | i18n }} - {{ send.name }}"
routerLink="/edit-send"
[queryParams]="{ sendId: send.id, type: send.type }"
appStopClick
type="button"
class="tw-pb-1"

View File

@@ -4,7 +4,7 @@
<li *ngFor="let attachment of cipher.attachments">
<bit-item>
<bit-item-content>
<span data-testid="file-name">{{ attachment.fileName }}</span>
<span data-testid="file-name" [title]="attachment.fileName">{{ attachment.fileName }}</span>
<span slot="secondary" data-testid="file-size">{{ attachment.sizeName }}</span>
</bit-item-content>
<ng-container slot="end">

View File

@@ -7,6 +7,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils";
import {
CardComponent,
FormFieldModule,
@@ -101,9 +102,7 @@ export class CardDetailsSectionComponent implements OnInit {
.pipe(takeUntilDestroyed())
.subscribe(({ cardholderName, number, brand, expMonth, expYear, code }) => {
this.cipherFormContainer.patchCipher((cipher) => {
// The input[type="number"] is returning a number, convert it to a string
// An empty field returns null, avoid casting `"null"` to a string
const expirationYear = expYear !== null ? `${expYear}` : null;
const expirationYear = normalizeExpiryYearFormat(expYear);
Object.assign(cipher.card, {
cardholderName,

View File

@@ -3,7 +3,7 @@
<h2 bitTypography="h6">{{ "additionalOptions" | i18n }}</h2>
</bit-section-header>
<bit-card class="[&_bit-form-field:last-of-type]:tw-mb-0">
<bit-form-field>
<bit-form-field disableReadOnlyBorder>
<bit-label>{{ "note" | i18n }}</bit-label>
<textarea readonly bitInput aria-readonly="true">{{ notes }}</textarea>
<button

View File

@@ -5,7 +5,7 @@
<bit-item-group>
<bit-item *ngFor="let attachment of cipher.attachments">
<bit-item-content>
<span data-testid="file-name">{{ attachment.fileName }}</span>
<span data-testid="file-name" [title]="attachment.fileName">{{ attachment.fileName }}</span>
<span slot="secondary" data-testid="file-size">{{ attachment.sizeName }}</span>
</bit-item-content>
<ng-container slot="end">

View File

@@ -4,7 +4,11 @@
</bit-section-header>
<bit-card>
<ng-container *ngFor="let login of loginUris; let last = last">
<bit-form-field [disableMargin]="last" data-testid="autofill-view-list">
<bit-form-field
[disableMargin]="last"
[disableReadOnlyBorder]="last"
data-testid="autofill-view-list"
>
<bit-label>
{{ "website" | i18n }}
</bit-label>

View File

@@ -2,7 +2,7 @@
<bit-section-header>
<h2 bitTypography="h6">{{ setSectionTitle }}</h2>
</bit-section-header>
<bit-card class="[&_bit-form-field:last-of-type]:tw-mb-0">
<read-only-cipher-card>
<bit-form-field *ngIf="card.cardholderName">
<bit-label>{{ "cardholderName" | i18n }}</bit-label>
<input
@@ -81,5 +81,5 @@
data-testid="copy-code"
></button>
</bit-form-field>
</bit-card>
</read-only-cipher-card>
</bit-section>

View File

@@ -13,6 +13,8 @@ import {
IconButtonModule,
} from "@bitwarden/components";
import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component";
@Component({
selector: "app-card-details-view",
templateUrl: "card-details-view.component.html",
@@ -26,6 +28,7 @@ import {
TypographyModule,
FormFieldModule,
IconButtonModule,
ReadOnlyCipherCardComponent,
],
})
export class CardDetailsComponent {

View File

@@ -3,6 +3,15 @@
{{ "cardExpiredMessage" | i18n }}
</bit-callout>
<!-- HELPER TEXT -->
<p
class="tw-text-sm tw-text-muted"
bitTypography="helper"
*ngIf="cipher?.isDeleted && !cipher?.edit"
>
{{ "noEditPermissions" | i18n }}
</p>
<!-- ITEM DETAILS -->
<app-item-details-v2
[cipher]="cipher"

View File

@@ -8,10 +8,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { CollectionId } from "@bitwarden/common/types/guid";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { isCardExpired } from "@bitwarden/common/vault/utils";
import { SearchModule, CalloutModule } from "@bitwarden/components";
import { AdditionalOptionsComponent } from "./additional-options/additional-options.component";
@@ -61,7 +61,7 @@ export class CipherViewComponent implements OnInit, OnDestroy {
async ngOnInit() {
await this.loadCipherData();
this.cardIsExpired = this.isCardExpiryInThePast();
this.cardIsExpired = isCardExpired(this.cipher.card);
}
ngOnDestroy(): void {
@@ -70,8 +70,8 @@ export class CipherViewComponent implements OnInit, OnDestroy {
}
get hasCard() {
const { cardholderName, code, expMonth, expYear, brand, number } = this.cipher.card;
return cardholderName || code || expMonth || expYear || brand || number;
const { cardholderName, code, expMonth, expYear, number } = this.cipher.card;
return cardholderName || code || expMonth || expYear || number;
}
get hasLogin() {
@@ -102,24 +102,4 @@ export class CipherViewComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroyed$));
}
}
isCardExpiryInThePast() {
if (this.cipher.card) {
const { expMonth, expYear }: CardView = this.cipher.card;
if (expYear && expMonth) {
// `Date` months are zero-indexed
const parsedMonth = parseInt(expMonth) - 1;
const parsedYear = parseInt(expYear);
// First day of the next month minus one, to get last day of the card month
const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0);
const now = new Date();
return cardExpiry < now;
}
}
return false;
}
}

View File

@@ -8,7 +8,7 @@
*ngFor="let field of fields; let last = last"
[ngClass]="{ 'tw-mb-4': !last }"
>
<bit-form-field *ngIf="field.type === fieldType.Text">
<bit-form-field *ngIf="field.type === fieldType.Text" [disableReadOnlyBorder]="last">
<bit-label>{{ field.name }}</bit-label>
<input readonly bitInput type="text" [value]="field.value" aria-readonly="true" />
<button
@@ -21,7 +21,7 @@
[appA11yTitle]="'copyValue' | i18n"
></button>
</bit-form-field>
<bit-form-field *ngIf="field.type === fieldType.Hidden">
<bit-form-field *ngIf="field.type === fieldType.Hidden" [disableReadOnlyBorder]="last">
<bit-label>{{ field.name }}</bit-label>
<input readonly bitInput type="password" [value]="field.value" aria-readonly="true" />
<button bitSuffix type="button" bitIconButton bitPasswordInputToggle></button>
@@ -45,7 +45,7 @@
/>
<bit-label> {{ field.name }} </bit-label>
</bit-form-control>
<bit-form-field *ngIf="field.type === fieldType.Linked">
<bit-form-field *ngIf="field.type === fieldType.Linked" [disableReadOnlyBorder]="last">
<bit-label> {{ "linked" | i18n }}: {{ field.name }} </bit-label>
<input
readonly

View File

@@ -5,6 +5,9 @@
<bit-card>
<bit-form-field
[disableMargin]="!cipher.collectionIds?.length && !cipher.organizationId && !cipher.folderId"
[disableReadOnlyBorder]="
!cipher.collectionIds?.length && !cipher.organizationId && !cipher.folderId
"
>
<bit-label>
{{ "itemName" | i18n }}
@@ -22,7 +25,7 @@
<ul
[attr.aria-label]="'itemLocation' | i18n"
*ngIf="cipher.collectionIds?.length || cipher.organizationId || cipher.folderId"
class="tw-mb-0"
class="tw-mb-0 tw-pl-0"
>
<li
*ngIf="cipher.organizationId && organization"
@@ -47,7 +50,7 @@
*ngIf="cipher.collectionIds && collections"
[attr.aria-label]="'collection' | i18n"
>
<ul data-testid="collections" [ngClass]="{ 'tw-mb-0': !cipher.folderId }">
<ul data-testid="collections" [ngClass]="{ 'tw-mb-0': !cipher.folderId }" class="tw-pl-0">
<li
*ngFor="let collection of collections; let last = last"
class="tw-flex tw-items-center tw-list-none"

View File

@@ -27,6 +27,7 @@
</p>
<a
*ngIf="cipher.hasPasswordHistory && isLogin"
bitLink
class="tw-font-bold tw-no-underline"
routerLink="/cipher-password-history"
[queryParams]="{ cipherId: cipher.id }"

View File

@@ -7,6 +7,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
CardComponent,
LinkModule,
SectionComponent,
SectionHeaderComponent,
TypographyModule,
@@ -24,6 +25,7 @@ import {
SectionComponent,
SectionHeaderComponent,
TypographyModule,
LinkModule,
],
})
export class ItemHistoryV2Component {

View File

@@ -2,7 +2,7 @@
<bit-section-header>
<h2 bitTypography="h6">{{ "loginCredentials" | i18n }}</h2>
</bit-section-header>
<bit-card class="[&_bit-form-field:last-of-type]:tw-mb-0">
<read-only-cipher-card>
<bit-form-field *ngIf="cipher.login.username">
<bit-label>
{{ "username" | i18n }}
@@ -109,7 +109,6 @@
[value]="totpCodeCopyObj?.totpCodeFormatted || '*** ***'"
aria-readonly="true"
data-testid="login-totp"
[disabled]="!(isPremium$ | async)"
/>
<button
*ngIf="isPremium$ | async"
@@ -133,5 +132,5 @@
class="disabled:tw-cursor-default"
></button>
</bit-form-field>
</bit-card>
</read-only-cipher-card>
</bit-section>

View File

@@ -19,6 +19,7 @@ import {
} from "@bitwarden/components";
import { BitTotpCountdownComponent } from "../../components/totp-countdown/totp-countdown.component";
import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component";
type TotpCodeValues = {
totpCode: string;
@@ -41,6 +42,7 @@ type TotpCodeValues = {
BadgeModule,
ColorPasswordModule,
BitTotpCountdownComponent,
ReadOnlyCipherCardComponent,
],
})
export class LoginCredentialsViewComponent {

View File

@@ -0,0 +1,3 @@
<bit-card class="[&_bit-form-field:last-of-type]:tw-mb-0">
<ng-content></ng-content>
</bit-card>

View File

@@ -0,0 +1,26 @@
import { AfterViewInit, Component, ContentChildren, QueryList } from "@angular/core";
import { CardComponent, BitFormFieldComponent } from "@bitwarden/components";
@Component({
selector: "read-only-cipher-card",
templateUrl: "./read-only-cipher-card.component.html",
standalone: true,
imports: [CardComponent],
})
/**
* A thin wrapper around the `bit-card` component that disables the bottom border for the last form field.
*/
export class ReadOnlyCipherCardComponent implements AfterViewInit {
@ContentChildren(BitFormFieldComponent) formFields: QueryList<BitFormFieldComponent>;
ngAfterViewInit(): void {
// Disable the bottom border for the last form field
if (this.formFields.last) {
// Delay model update until next change detection cycle
setTimeout(() => {
this.formFields.last.disableReadOnlyBorder = true;
});
}
}
}

View File

@@ -3,7 +3,7 @@
<h2 bitTypography="h6">{{ "personalDetails" | i18n }}</h2>
</bit-section-header>
<bit-card class="[&_bit-form-field:last-of-type]:tw-mb-0">
<read-only-cipher-card>
<bit-form-field *ngIf="cipher.identity.fullName">
<bit-label>{{ "name" | i18n }}</bit-label>
<input bitInput [value]="cipher.identity.fullName" readonly data-testid="name" />
@@ -43,7 +43,7 @@
[valueLabel]="'company' | i18n"
></button>
</bit-form-field>
</bit-card>
</read-only-cipher-card>
</bit-section>
<bit-section *ngIf="showIdentificationDetails">
@@ -51,7 +51,7 @@
<h2 bitTypography="h6">{{ "identification" | i18n }}</h2>
</bit-section-header>
<bit-card class="[&_bit-form-field:last-of-type]:tw-mb-0">
<read-only-cipher-card>
<bit-form-field *ngIf="cipher.identity.ssn">
<bit-label>{{ "ssn" | i18n }}</bit-label>
<input bitInput type="password" [value]="cipher.identity.ssn" readonly data-testid="ssn" />
@@ -111,7 +111,7 @@
[valueLabel]="'licenseNumber' | i18n"
></button>
</bit-form-field>
</bit-card>
</read-only-cipher-card>
</bit-section>
<bit-section *ngIf="showContactDetails">
@@ -119,7 +119,7 @@
<h2 bitTypography="h6">{{ "contactInfo" | i18n }}</h2>
</bit-section-header>
<bit-card class="[&_bit-form-field:last-of-type]:tw-mb-0">
<read-only-cipher-card>
<bit-form-field *ngIf="cipher.identity.email">
<bit-label>{{ "email" | i18n }}</bit-label>
<input bitInput [value]="cipher.identity.email" readonly data-testid="email" />
@@ -166,5 +166,5 @@
[valueLabel]="'address' | i18n"
></button>
</bit-form-field>
</bit-card>
</read-only-cipher-card>
</bit-section>

View File

@@ -12,6 +12,8 @@ import {
TypographyModule,
} from "@bitwarden/components";
import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component";
@Component({
standalone: true,
selector: "app-view-identity-sections",
@@ -25,6 +27,7 @@ import {
TypographyModule,
FormFieldModule,
IconButtonModule,
ReadOnlyCipherCardComponent,
],
})
export class ViewIdentitySectionsComponent implements OnInit {

View File

@@ -0,0 +1,18 @@
import { svgIcon } from "@bitwarden/components";
export const EmptyTrash = svgIcon`
<svg width="174" height="100" viewBox="0 0 174 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M113.938 95.7919L121.802 25.2171C121.882 24.4997 121.32 23.8721 120.599 23.8721H52.8158C52.0939 23.8721 51.5324 24.4997 51.6123 25.2171L59.4759 95.7919C59.5442 96.405 60.0625 96.8687 60.6794 96.8687H112.735C113.352 96.8687 113.87 96.405 113.938 95.7919Z" fill="none"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M70.9462 38.4568C71.1965 38.44 71.4141 38.6291 71.4323 38.8793L74.2991 78.3031C74.3173 78.5532 74.1292 78.7696 73.879 78.7865C73.6288 78.8033 73.4112 78.6142 73.393 78.364L70.5261 38.9402C70.5079 38.6901 70.696 38.4737 70.9462 38.4568Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M87.4314 38.4082C87.6822 38.4082 87.8855 38.6115 87.8855 38.8623L87.8855 78.3824C87.8855 78.6332 87.6822 78.8365 87.4314 78.8365C87.1806 78.8365 86.9773 78.6332 86.9773 78.3824L86.9773 38.8623C86.9773 38.6115 87.1806 38.4082 87.4314 38.4082Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M103.917 38.4572C104.167 38.474 104.355 38.6905 104.337 38.9406L101.47 78.3644C101.452 78.6145 101.234 78.8037 100.984 78.7868C100.734 78.77 100.546 78.5536 100.564 78.3035L103.431 38.8797C103.449 38.6295 103.667 38.4404 103.917 38.4572Z"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" clip-rule="evenodd" d="M52.8159 24.7803C52.6354 24.7803 52.4951 24.9372 52.515 25.1165L59.3506 86.4648H76.54C76.7908 86.4648 76.9941 86.6682 76.9941 86.9189C76.9941 87.1697 76.7908 87.373 76.54 87.373H59.4518L60.3786 95.6913C60.3957 95.8446 60.5252 95.9605 60.6795 95.9605H112.735C112.889 95.9605 113.019 95.8446 113.036 95.6913L120.353 30.0186L58.2399 30.0186C57.9891 30.0186 57.7858 29.8152 57.7858 29.5645C57.7858 29.3137 57.9891 29.1104 58.2399 29.1104L120.455 29.1104L120.9 25.1165C120.919 24.9372 120.779 24.7803 120.599 24.7803H52.8159ZM50.7098 25.3177C50.5699 24.0621 51.5526 22.9639 52.8159 22.9639H120.599C121.862 22.9639 122.845 24.0622 122.705 25.3177L114.841 95.8924C114.722 96.9654 113.815 97.7769 112.735 97.7769H60.6795C59.5999 97.7769 58.6929 96.9654 58.5734 95.8924L50.7098 25.3177Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M88.4499 0.527344C88.9515 0.527344 89.3581 0.933958 89.3581 1.43554V11.2051C89.3581 11.7067 88.9515 12.1133 88.4499 12.1133C87.9484 12.1133 87.5417 11.7067 87.5417 11.2051V1.43554C87.5417 0.933958 87.9484 0.527344 88.4499 0.527344Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M56.8137 6.2397C57.2694 6.03014 57.8774 6.18948 58.1718 6.59559L64.3048 15.0563C64.5992 15.4624 64.4684 15.9615 64.0127 16.1711C63.557 16.3806 62.9489 16.2213 62.6545 15.8152L56.5215 7.35447C56.2272 6.94836 56.358 6.44926 56.8137 6.2397Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M73.1704 2.01822C73.6671 1.94846 74.1576 2.28892 74.266 2.77864L76.396 12.3998C76.5044 12.8895 76.1896 13.3431 75.6929 13.4129C75.1962 13.4826 74.7057 13.1422 74.5973 12.6524L72.4673 3.03126C72.3589 2.54153 72.6737 2.08798 73.1704 2.01822Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M104.344 2.13682C104.835 2.24151 105.103 2.71177 104.943 3.18717L101.768 12.6239C101.609 13.0993 101.081 13.3998 100.591 13.2951C100.1 13.1904 99.8321 12.7202 99.9921 12.2448L103.167 2.80806C103.327 2.33266 103.854 2.03213 104.344 2.13682Z"/>
<path class="tw-fill-info-600" fill-rule="evenodd" clip-rule="evenodd" d="M120.085 6.23979C120.541 6.44935 120.672 6.94845 120.378 7.35456L114.245 15.8153C113.95 16.2214 113.342 16.3807 112.886 16.1712C112.431 15.9616 112.3 15.4625 112.594 15.0564L118.727 6.59568C119.022 6.18957 119.63 6.03023 120.085 6.23979Z"/>
<path d="M129.384 27.2001L124.272 27.9646C123.505 28.0793 123.059 28.8635 123.353 29.579L150.626 95.9087C150.908 96.5946 151.738 96.888 152.38 96.5285L156.79 94.0573C157.31 93.766 157.526 93.1391 157.297 92.5833L130.726 27.9604C130.509 27.4321 129.95 27.1155 129.384 27.2001Z" fill="none"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" clip-rule="evenodd" d="M144.4 49.2028C145.911 49.8345 146.573 50.261 147.061 50.8557C147.593 51.504 147.976 52.4111 148.726 54.2353L151.93 62.0272C152.68 63.8513 153.045 64.7652 153.118 65.587C153.185 66.3407 153.004 67.0854 152.349 68.5355C152.26 68.732 152.174 68.9115 152.09 69.0865C151.969 69.3389 151.852 69.5821 151.738 69.8536C151.527 70.3581 151.273 71.0631 150.824 72.4643C150.693 72.8741 150.581 73.2651 150.49 73.6452L139.404 46.6825C139.741 46.9012 140.101 47.1138 140.489 47.3276C141.814 48.0582 142.501 48.408 143.015 48.6385C143.292 48.7625 143.55 48.864 143.818 48.9693C144.004 49.0424 144.195 49.1173 144.4 49.2028ZM134.933 40.574C134.938 40.5882 134.943 40.6024 134.949 40.6166C134.99 40.7164 135.031 40.8147 135.072 40.9115L151.431 80.6977C151.47 80.7949 151.51 80.8934 151.551 80.9931C151.557 81.0072 151.563 81.0211 151.569 81.0349L156.449 92.9041C156.507 93.043 156.453 93.1998 156.323 93.2726L151.912 95.7438C151.752 95.8337 151.544 95.7603 151.474 95.5888L124.201 29.2592C124.127 29.0803 124.239 28.8843 124.431 28.8556L129.543 28.0911C129.685 28.0699 129.824 28.1491 129.879 28.2812L134.933 40.574ZM136.764 40.2619C137.429 41.8455 137.981 42.8653 138.622 43.6471C139.287 44.4581 140.092 45.0652 141.355 45.7612C142.672 46.4872 143.303 46.8056 143.742 47.0027C144.006 47.1212 144.177 47.1875 144.389 47.2695C144.566 47.338 144.771 47.4175 145.082 47.5476C146.656 48.2055 147.682 48.778 148.476 49.7453C149.205 50.6349 149.689 51.8128 150.366 53.4594L150.422 53.5946L153.626 61.3866L153.681 61.5218C154.359 63.1683 154.843 64.3461 154.943 65.4735C155.051 66.6995 154.708 67.7893 154.026 69.2998C153.891 69.5983 153.797 69.7904 153.717 69.9561L153.717 69.9563C153.621 70.1545 153.543 70.3148 153.434 70.5741C153.253 71.0054 153.019 71.6508 152.572 73.0431C152.144 74.3778 151.988 75.3479 152.079 76.3759C152.166 77.3668 152.489 78.4733 153.13 80.066L158.145 92.2635C158.545 93.2361 158.168 94.3331 157.258 94.8429L152.847 97.3142C151.724 97.9433 150.271 97.4298 149.778 96.2295L122.505 29.8998C121.99 28.6476 122.771 27.2752 124.113 27.0746L129.225 26.3101C130.216 26.162 131.194 26.716 131.574 27.6406L136.764 40.2619Z"/>
</svg>
`;

View File

@@ -1,3 +1,4 @@
export * from "./deactivated-org";
export * from "./no-folders";
export * from "./vault";
export * from "./empty-trash";

View File

@@ -62,12 +62,6 @@ describe("CopyCipherFieldService", () => {
expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled();
});
it("should return early when cipher.viewPassword is false", async () => {
cipher.viewPassword = false;
await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled();
});
it("should copy value to clipboard", async () => {
await service.copy(valueToCopy, actionType, cipher, skipReprompt);
expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith(valueToCopy);

View File

@@ -106,7 +106,7 @@ export class CopyCipherFieldService {
return;
}
if (valueToCopy == null || !cipher.viewPassword) {
if (valueToCopy == null) {
return;
}