1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-29 14:43:31 +00:00

merge main, fix conflicts

This commit is contained in:
rr-bw
2024-09-13 10:05:57 -07:00
323 changed files with 6478 additions and 1448 deletions

View File

@@ -4,8 +4,6 @@ import { firstValueFrom, map } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -27,7 +25,6 @@ export class CollectionsComponent implements OnInit {
collectionIds: string[];
collections: CollectionView[] = [];
organization: Organization;
restrictProviderAccess: boolean;
protected cipherDomain: Cipher;
@@ -38,15 +35,11 @@ export class CollectionsComponent implements OnInit {
protected cipherService: CipherService,
protected organizationService: OrganizationService,
private logService: LogService,
private configService: ConfigService,
private accountService: AccountService,
private toastService: ToastService,
) {}
async ngOnInit() {
this.restrictProviderAccess = await this.configService.getFeatureFlag(
FeatureFlag.RestrictProviderAccess,
);
await this.load();
}
@@ -76,7 +69,7 @@ export class CollectionsComponent implements OnInit {
async submit(): Promise<boolean> {
const selectedCollectionIds = this.collections
.filter((c) => {
if (this.organization.canEditAllCiphers(this.restrictProviderAccess)) {
if (this.organization.canEditAllCiphers) {
return !!(c as any).checked;
} else {
return !!(c as any).checked && c.readOnly == null;

View File

@@ -0,0 +1,22 @@
import { inject } from "@angular/core";
import { UrlTree, Router } from "@angular/router";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
/**
* Helper function to redirect to a new URL based on the ExtensionRefresh feature flag.
* @param redirectUrl - The URL to redirect to if the ExtensionRefresh flag is enabled.
*/
export function extensionRefreshRedirect(redirectUrl: string): () => Promise<boolean | UrlTree> {
return async () => {
const configService = inject(ConfigService);
const router = inject(Router);
const shouldRedirect = await configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
if (shouldRedirect) {
return router.parseUrl(redirectUrl);
} else {
return true;
}
};
}

View File

@@ -13,7 +13,6 @@ import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -92,8 +91,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
private personalOwnershipPolicyAppliesToActiveUser: boolean;
private previousCipherId: string;
protected restrictProviderAccess = false;
get fido2CredentialCreationDateValue(): string {
const dateCreated = this.i18nService.t("dateCreated");
const creationDate = this.datePipe.transform(
@@ -182,10 +179,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
async ngOnInit() {
this.restrictProviderAccess = await this.configService.getFeatureFlag(
FeatureFlag.RestrictProviderAccess,
);
this.policyService
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
.pipe(
@@ -683,11 +676,11 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected saveCipher(cipher: Cipher) {
const isNotClone = this.editMode && !this.cloneMode;
let orgAdmin = this.organization?.canEditAllCiphers(this.restrictProviderAccess);
let orgAdmin = this.organization?.canEditAllCiphers;
// if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection
if (!cipher.collectionIds) {
orgAdmin = this.organization?.canEditUnassignedCiphers(this.restrictProviderAccess);
orgAdmin = this.organization?.canEditUnassignedCiphers;
}
return this.cipher.id == null
@@ -696,14 +689,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
protected deleteCipher() {
const asAdmin = this.organization?.canEditAllCiphers(this.restrictProviderAccess);
const asAdmin = this.organization?.canEditAllCiphers;
return this.cipher.isDeleted
? this.cipherService.deleteWithServer(this.cipher.id, asAdmin)
: this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
}
protected restoreCipher() {
const asAdmin = this.organization?.canEditAllCiphers(this.restrictProviderAccess);
const asAdmin = this.organization?.canEditAllCiphers;
return this.cipherService.restoreWithServer(this.cipher.id, asAdmin);
}

View File

@@ -17,7 +17,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
@Directive()
export class AttachmentsComponent implements OnInit {
@@ -49,6 +49,7 @@ export class AttachmentsComponent implements OnInit {
protected dialogService: DialogService,
protected billingAccountProfileStateService: BillingAccountProfileStateService,
protected accountService: AccountService,
protected toastService: ToastService,
) {}
async ngOnInit() {
@@ -182,6 +183,11 @@ export class AttachmentsComponent implements OnInit {
fileName: attachment.fileName,
blobData: decBuf,
});
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("fileSavedToDevice"),
});
} catch (e) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
}

View File

@@ -1,9 +1,12 @@
<!-- The calc() reductions are to account for browser/desktop headers -->
<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"
class="tw-flex 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,
'tw-relative tw-top-0': clientType === 'browser',
'tw-min-h-screen': clientType === 'web',
'tw-min-h-[calc(100vh-72px)]': clientType === 'browser',
'tw-min-h-[calc(100vh-54px)]': clientType === 'desktop',
}"
>
<bit-icon *ngIf="!hideLogo" [icon]="logo" class="tw-w-[128px] [&>*]:tw-align-top"></bit-icon>
@@ -23,6 +26,7 @@
{{ title }}
</h1>
</ng-container>
<div *ngIf="subtitle" class="tw-text-sm sm:tw-text-base">{{ subtitle }}</div>
</div>

View File

@@ -3,5 +3,6 @@ export * from "./bitwarden-shield.icon";
export * from "./lock.icon";
export * from "./registration-check-email.icon";
export * from "./registration-expired-link.icon";
export * from "./user-lock.icon";
export * from "./user-verification-biometrics-fingerprint.icon";
export * from "./wave.icon";

View File

@@ -2,16 +2,16 @@ import { svgIcon } from "@bitwarden/components";
export const LockIcon = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 100" fill="none">
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M27.5 48.218a9 9 0 0 1 9-9h47a9 9 0 0 1 9 9v7.5h-2v-7.5a7 7 0 0 0-7-7h-47a7 7 0 0 0-7 7v7.5h-2v-7.5Zm2 30.75v3.75a7 7 0 0 0 7 7h47a7 7 0 0 0 7-7v-3.75h2v3.75a9 9 0 0 1-9 9h-47a9 9 0 0 1-9-9v-3.75h2Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M60 10.718c-11.144 0-20 7.942-20 17.414v11.586h-2V28.132C38 17.317 48.007 8.718 60 8.718c11.991 0 22 8.552 22 19.414v11.586h-2V28.132c0-9.516-8.855-17.414-20-17.414ZM32.028 61.28a1 1 0 0 1 1 1v5.678a1 1 0 1 1-2 0v-5.679a1 1 0 0 1 1-1Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M38.452 65.897a1 1 0 0 1-.647 1.258l-5.472 1.755a1 1 0 1 1-.61-1.904l5.471-1.755a1 1 0 0 1 1.258.646Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M31.442 67.147a1 1 0 0 1 1.396.225l3.356 4.646a1 1 0 0 1-1.622 1.171l-3.355-4.646a1 1 0 0 1 .225-1.396Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M32.607 67.143a1 1 0 0 1 .236 1.394l-3.304 4.646a1 1 0 0 1-1.63-1.159l3.304-4.646a1 1 0 0 1 1.394-.235Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M25.656 65.895a1 1 0 0 1 1.26-.644l5.42 1.755a1 1 0 1 1-.616 1.903l-5.42-1.755a1 1 0 0 1-.644-1.26ZM50.508 61.28a1 1 0 0 1 1 1v5.678a1 1 0 1 1-2 0v-5.679a1 1 0 0 1 1-1Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M56.88 65.895a1 1 0 0 1-.644 1.26l-5.42 1.754a1 1 0 1 1-.616-1.903l5.42-1.755a1 1 0 0 1 1.26.644Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M49.922 67.147a1 1 0 0 1 1.397.225l3.355 4.646a1 1 0 1 1-1.621 1.171l-3.356-4.646a1 1 0 0 1 .225-1.396Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M51.093 67.147a1 1 0 0 1 .226 1.396l-3.356 4.646a1 1 0 0 1-1.621-1.17l3.355-4.647a1 1 0 0 1 1.396-.225Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M44.136 65.895a1 1 0 0 1 1.26-.644l5.42 1.755a1 1 0 1 1-.616 1.903l-5.42-1.755a1 1 0 0 1-.644-1.26ZM62.568 72.603a1 1 0 0 1 1-1h10.84a1 1 0 1 1 0 2h-10.84a1 1 0 0 1-1-1ZM81.049 72.603a1 1 0 0 1 1-1h10.84a1 1 0 1 1 0 2H82.05a1 1 0 0 1-1-1Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M17.5 67.468c0-7.042 5.708-12.75 12.75-12.75h59.5c7.041 0 12.75 5.708 12.75 12.75s-5.709 12.75-12.75 12.75h-59.5c-7.042 0-12.75-5.708-12.75-12.75Zm12.75-10.75c-5.937 0-10.75 4.813-10.75 10.75s4.813 10.75 10.75 10.75h59.5c5.937 0 10.75-4.813 10.75-10.75s-4.813-10.75-10.75-10.75h-59.5Z" clip-rule="evenodd"/>
<path class="tw-fill-art-primary" fill-rule="evenodd" d="M27.5 48.218a9 9 0 0 1 9-9h47a9 9 0 0 1 9 9v7.5h-2v-7.5a7 7 0 0 0-7-7h-47a7 7 0 0 0-7 7v7.5h-2v-7.5Zm2 30.75v3.75a7 7 0 0 0 7 7h47a7 7 0 0 0 7-7v-3.75h2v3.75a9 9 0 0 1-9 9h-47a9 9 0 0 1-9-9v-3.75h2Z" clip-rule="evenodd"/>
<path class="tw-fill-art-primary" fill-rule="evenodd" d="M60 10.718c-11.144 0-20 7.942-20 17.414v11.586h-2V28.132C38 17.317 48.007 8.718 60 8.718c11.991 0 22 8.552 22 19.414v11.586h-2V28.132c0-9.516-8.855-17.414-20-17.414ZM32.028 61.28a1 1 0 0 1 1 1v5.678a1 1 0 1 1-2 0v-5.679a1 1 0 0 1 1-1Z" clip-rule="evenodd"/>
<path class="tw-fill-art-primary" fill-rule="evenodd" d="M38.452 65.897a1 1 0 0 1-.647 1.258l-5.472 1.755a1 1 0 1 1-.61-1.904l5.471-1.755a1 1 0 0 1 1.258.646Z" clip-rule="evenodd"/>
<path class="tw-fill-art-primary" fill-rule="evenodd" d="M31.442 67.147a1 1 0 0 1 1.396.225l3.356 4.646a1 1 0 0 1-1.622 1.171l-3.355-4.646a1 1 0 0 1 .225-1.396Z" clip-rule="evenodd"/>
<path class="tw-fill-art-primary" fill-rule="evenodd" d="M32.607 67.143a1 1 0 0 1 .236 1.394l-3.304 4.646a1 1 0 0 1-1.63-1.159l3.304-4.646a1 1 0 0 1 1.394-.235Z" clip-rule="evenodd"/>
<path class="tw-fill-art-primary" fill-rule="evenodd" d="M25.656 65.895a1 1 0 0 1 1.26-.644l5.42 1.755a1 1 0 1 1-.616 1.903l-5.42-1.755a1 1 0 0 1-.644-1.26ZM50.508 61.28a1 1 0 0 1 1 1v5.678a1 1 0 1 1-2 0v-5.679a1 1 0 0 1 1-1Z" clip-rule="evenodd"/>
<path class="tw-fill-art-primary" fill-rule="evenodd" d="M56.88 65.895a1 1 0 0 1-.644 1.26l-5.42 1.754a1 1 0 1 1-.616-1.903l5.42-1.755a1 1 0 0 1 1.26.644Z" clip-rule="evenodd"/>
<path class="tw-fill-art-primary" fill-rule="evenodd" d="M49.922 67.147a1 1 0 0 1 1.397.225l3.355 4.646a1 1 0 1 1-1.621 1.171l-3.356-4.646a1 1 0 0 1 .225-1.396Z" clip-rule="evenodd"/>
<path class="tw-fill-art-primary" fill-rule="evenodd" d="M51.093 67.147a1 1 0 0 1 .226 1.396l-3.356 4.646a1 1 0 0 1-1.621-1.17l3.355-4.647a1 1 0 0 1 1.396-.225Z" clip-rule="evenodd"/>
<path class="tw-fill-art-primary" fill-rule="evenodd" d="M44.136 65.895a1 1 0 0 1 1.26-.644l5.42 1.755a1 1 0 1 1-.616 1.903l-5.42-1.755a1 1 0 0 1-.644-1.26ZM62.568 72.603a1 1 0 0 1 1-1h10.84a1 1 0 1 1 0 2h-10.84a1 1 0 0 1-1-1ZM81.049 72.603a1 1 0 0 1 1-1h10.84a1 1 0 1 1 0 2H82.05a1 1 0 0 1-1-1Z" clip-rule="evenodd"/>
<path class="tw-fill-art-accent" fill-rule="evenodd" d="M17.5 67.468c0-7.042 5.708-12.75 12.75-12.75h59.5c7.041 0 12.75 5.708 12.75 12.75s-5.709 12.75-12.75 12.75h-59.5c-7.042 0-12.75-5.708-12.75-12.75Zm12.75-10.75c-5.937 0-10.75 4.813-10.75 10.75s4.813 10.75 10.75 10.75h59.5c5.937 0 10.75-4.813 10.75-10.75s-4.813-10.75-10.75-10.75h-59.5Z" clip-rule="evenodd"/>
</svg>
`;

View File

@@ -0,0 +1,22 @@
import { svgIcon } from "@bitwarden/components";
export const UserLockIcon = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="100" fill="none">
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M0 18.207a7.798 7.798 0 0 1 7.798-7.798H89.38a7.798 7.798 0 0 1 7.799 7.798v8.763h-2.4v-8.763a5.399 5.399 0 0 0-5.398-5.399H7.797A5.399 5.399 0 0 0 2.4 18.207v49.19a5.399 5.399 0 0 0 5.4 5.398h9.483v2.4H7.798A7.798 7.798 0 0 1 0 67.396V18.207Zm49.378 54.588h13.498v2.4H49.378v-2.4Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M88.78 58.398c8.946 0 16.197-7.251 16.197-16.196s-7.251-16.197-16.196-16.197-16.197 7.252-16.197 16.197 7.252 16.196 16.197 16.196Zm0 2.4c10.27 0 18.597-8.326 18.597-18.596s-8.326-18.596-18.596-18.596-18.596 8.326-18.596 18.596S78.51 60.798 88.78 60.798Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M61.005 87.192h54.28c1.303 0 1.833-.344 2.01-.53.128-.134.396-.517.226-1.586-2.187-13.74-14.213-24.278-28.752-24.278S62.203 71.337 60.017 85.076c-.09.57.026 1.226.279 1.662.115.2.23.305.316.359.072.046.184.095.393.095Zm0 2.4h54.28c3.346 0 5.104-1.76 4.605-4.893-2.371-14.903-15.402-26.3-31.121-26.3-15.72 0-28.75 11.397-31.122 26.3-.337 2.121.744 4.893 3.358 4.893Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M77.983 17.607a1.2 1.2 0 0 1-1.2-1.2v-.6a1.2 1.2 0 1 1 2.4 0v.6a1.2 1.2 0 0 1-1.2 1.2ZM83.382 17.607a1.2 1.2 0 0 1-1.2-1.2v-.498a1.2 1.2 0 1 1 2.4 0v.498a1.2 1.2 0 0 1-1.2 1.2ZM88.78 17.607a1.2 1.2 0 0 1-1.2-1.2v-.498a1.2 1.2 0 1 1 2.4 0v.498a1.2 1.2 0 0 1-1.2 1.2Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M95.083 20.607H0v-1.2h95.083v1.2ZM15.12 54.571a5.999 5.999 0 0 1 6-5.998h23.23a5.999 5.999 0 0 1 6 5.998V57.2h-2.4V54.57a3.6 3.6 0 0 0-3.6-3.599H21.12a3.6 3.6 0 0 0-3.6 3.6v2.627h-2.4V54.57Zm2.4 15.825v2.693a3.6 3.6 0 0 0 3.6 3.599h23.23a3.6 3.6 0 0 0 3.6-3.6v-2.692h2.4v2.693a5.999 5.999 0 0 1-6 5.998H21.12a5.999 5.999 0 0 1-6-5.998v-2.693h2.4Z" clip-rule="evenodd"/>
<path class="tw-fill-text-headers" fill-rule="evenodd" d="M32.479 33.255c-5.31 0-9.641 4.332-9.641 9.64v6.822h-2.4v-6.821c0-6.635 5.406-12.04 12.04-12.04 6.633 0 12.041 5.377 12.041 12.04v6.821h-2.4v-6.821c0-5.334-4.33-9.641-9.64-9.641Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M10.498 63.797a7.498 7.498 0 0 1 7.498-7.498h30.593a7.498 7.498 0 0 1 0 14.997H17.996a7.498 7.498 0 0 1-7.498-7.499Zm43.79 0a5.699 5.699 0 0 0-5.699-5.699H17.996a5.699 5.699 0 0 0 0 11.398h30.593a5.699 5.699 0 0 0 5.7-5.699Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M19.02 60.669a.6.6 0 0 1 .6.6v2.959a.6.6 0 0 1-1.2 0v-2.96a.6.6 0 0 1 .6-.6Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M22.443 63.13a.6.6 0 0 1-.388.755l-2.851.914a.6.6 0 1 1-.367-1.142l2.852-.915a.6.6 0 0 1 .754.388Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M18.67 63.742a.6.6 0 0 1 .837.135l1.748 2.42a.6.6 0 0 1-.972.703l-1.749-2.42a.6.6 0 0 1 .136-.838Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M19.368 63.739a.6.6 0 0 1 .142.837l-1.722 2.42a.6.6 0 0 1-.978-.695l1.722-2.42a.6.6 0 0 1 .836-.142Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M15.626 63.129a.6.6 0 0 1 .755-.386l2.825.914a.6.6 0 0 1-.37 1.142l-2.824-.915a.6.6 0 0 1-.386-.755ZM28.651 60.669a.6.6 0 0 1 .6.6v2.959a.6.6 0 1 1-1.2 0v-2.96a.6.6 0 0 1 .6-.6Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M32.046 63.129a.6.6 0 0 1-.386.755l-2.824.915a.6.6 0 1 1-.37-1.142l2.825-.914a.6.6 0 0 1 .755.386Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M28.3 63.742a.6.6 0 0 1 .837.135l1.749 2.42a.6.6 0 0 1-.973.703l-1.748-2.42a.6.6 0 0 1 .135-.838Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M29.002 63.742a.6.6 0 0 1 .136.837L27.387 67a.6.6 0 0 1-.972-.702l1.749-2.421a.6.6 0 0 1 .837-.136Z" clip-rule="evenodd"/>
<path class="tw-fill-info-600" fill-rule="evenodd" d="M25.256 63.129a.6.6 0 0 1 .755-.386l2.825.914a.6.6 0 1 1-.37 1.142l-2.824-.915a.6.6 0 0 1-.386-.755ZM34.857 66.649a.6.6 0 0 1 .6-.6h5.649a.6.6 0 0 1 0 1.2h-5.649a.6.6 0 0 1-.6-.6ZM44.487 66.649a.6.6 0 0 1 .6-.6h5.65a.6.6 0 0 1 0 1.2h-5.65a.6.6 0 0 1-.6-.6Z" clip-rule="evenodd"/>
</svg>
`;

View File

@@ -27,6 +27,9 @@ export * from "./login/default-login.service";
// password callout
export * from "./password-callout/password-callout.component";
// password hint
export * from "./password-hint/password-hint.component";
// registration
export * from "./registration/registration-start/registration-start.component";
export * from "./registration/registration-finish/registration-finish.component";

View File

@@ -0,0 +1,40 @@
<form [bitSubmit]="submit" [formGroup]="formGroup">
<!-- <ng-template> must be within the <form> to ensure that `formControlName` has a parent `formGroup` directive. -->
<ng-template #formContentTemplate>
<bit-form-field>
<bit-label>{{ "accountEmail" | i18n }}</bit-label>
<input
bitInput
appAutofocus
inputmode="email"
appInputVerbatim="false"
type="email"
formControlName="email"
/>
</bit-form-field>
<button
class="tw-mb-2"
type="submit"
bitButton
bitFormButton
buttonType="primary"
[block]="true"
>
{{ "requestHint" | i18n }}
</button>
<button type="button" bitButton buttonType="secondary" (click)="cancel()" [block]="true">
{{ "cancel" | i18n }}
</button>
</ng-template>
<!-- Browser -->
<main *ngIf="clientType === 'browser'" tabindex="-1">
<ng-container *ngTemplateOutlet="formContentTemplate"></ng-container>
</main>
<!-- Web, Desktop -->
<ng-container *ngIf="clientType !== 'browser'">
<ng-container *ngTemplateOutlet="formContentTemplate"></ng-container>
</ng-container>
</form>

View File

@@ -0,0 +1,107 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { Router, RouterModule } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/password-hint.request";
import { ClientType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
AsyncActionsModule,
ButtonModule,
FormFieldModule,
ToastService,
} from "@bitwarden/components";
@Component({
standalone: true,
templateUrl: "./password-hint.component.html",
imports: [
AsyncActionsModule,
ButtonModule,
CommonModule,
FormFieldModule,
JslibModule,
ReactiveFormsModule,
RouterModule,
],
})
export class PasswordHintComponent implements OnInit {
protected clientType: ClientType;
protected formGroup = this.formBuilder.group({
email: ["", [Validators.required, Validators.email]],
});
protected get email() {
return this.formGroup.controls.email.value;
}
constructor(
private apiService: ApiService,
private formBuilder: FormBuilder,
private i18nService: I18nService,
private loginEmailService: LoginEmailServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private toastService: ToastService,
private router: Router,
) {
this.clientType = this.platformUtilsService.getClientType();
}
async ngOnInit(): Promise<void> {
const email = (await firstValueFrom(this.loginEmailService.loginEmail$)) ?? "";
this.formGroup.controls.email.setValue(email);
}
submit = async () => {
const isEmailValid = this.validateEmailOrShowToast(this.email);
if (!isEmailValid) {
return;
}
await this.apiService.postPasswordHint(new PasswordHintRequest(this.email));
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("masterPassSent"),
});
await this.router.navigate(["login"]);
};
protected async cancel() {
this.loginEmailService.setLoginEmail(this.email);
await this.router.navigate(["login"]);
}
private validateEmailOrShowToast(email: string): boolean {
// If email is null or empty, show error toast and return false
if (email == null || email === "") {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("emailRequired"),
});
return false;
}
// If not a valid email format, show error toast and return false
if (email.indexOf("@") === -1) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidEmail"),
});
return false;
}
return true; // email is valid
}
}

View File

@@ -23,6 +23,9 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
email: string,
passwordInputResult: PasswordInputResult,
emailVerificationToken?: string,
orgSponsoredFreeFamilyPlanToken?: string,
acceptEmergencyAccessInviteToken?: string,
emergencyAccessId?: string,
): Promise<string> {
const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey(
passwordInputResult.masterKey,
@@ -35,10 +38,13 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
const registerRequest = await this.buildRegisterRequest(
email,
emailVerificationToken,
passwordInputResult,
newEncUserKey.encryptedString,
userAsymmetricKeys,
emailVerificationToken,
orgSponsoredFreeFamilyPlanToken,
acceptEmergencyAccessInviteToken,
emergencyAccessId,
);
const capchaBypassToken = await this.accountApiService.registerFinish(registerRequest);
@@ -48,19 +54,21 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
protected async buildRegisterRequest(
email: string,
emailVerificationToken: string,
passwordInputResult: PasswordInputResult,
encryptedUserKey: EncryptedString,
userAsymmetricKeys: [string, EncString],
emailVerificationToken?: string,
orgSponsoredFreeFamilyPlanToken?: string, // web only
acceptEmergencyAccessInviteToken?: string, // web only
emergencyAccessId?: string, // web only
): Promise<RegisterFinishRequest> {
const userAsymmetricKeysRequest = new KeysRequest(
userAsymmetricKeys[0],
userAsymmetricKeys[1].encryptedString,
);
return new RegisterFinishRequest(
const registerFinishRequest = new RegisterFinishRequest(
email,
emailVerificationToken,
passwordInputResult.masterKeyHash,
passwordInputResult.hint,
encryptedUserKey,
@@ -68,5 +76,11 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
passwordInputResult.kdfConfig.kdfType,
passwordInputResult.kdfConfig.iterations,
);
if (emailVerificationToken) {
registerFinishRequest.emailVerificationToken = emailVerificationToken;
}
return registerFinishRequest;
}
}

View File

@@ -33,11 +33,21 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
submitting = false;
email: string;
// Note: this token is the email verification token. It is always supplied as a query param, but
// Note: this token is the email verification token. When it is supplied as a query param,
// it either comes from the email verification email or, if email verification is disabled server side
// via global settings, it comes directly from the registration-start component directly.
// It is not provided when the user is coming from another emailed invite (ex: org invite or enterprise
// org sponsored free family plan invite).
emailVerificationToken: string;
// this token is provided when the user is coming from an emailed invite to
// setup a free family plan sponsored by an organization but they don't have an account yet.
orgSponsoredFreeFamilyPlanToken: string;
// this token is provided when the user is coming from an emailed invite to accept an emergency access invite
acceptEmergencyAccessInviteToken: string;
emergencyAccessId: string;
masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
constructor(
@@ -69,6 +79,15 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
if (qParams.token != null) {
this.emailVerificationToken = qParams.token;
}
if (qParams.orgSponsoredFreeFamilyPlanToken != null) {
this.orgSponsoredFreeFamilyPlanToken = qParams.orgSponsoredFreeFamilyPlanToken;
}
if (qParams.acceptEmergencyAccessInviteToken != null && qParams.emergencyAccessId) {
this.acceptEmergencyAccessInviteToken = qParams.acceptEmergencyAccessInviteToken;
this.emergencyAccessId = qParams.emergencyAccessId;
}
}),
switchMap((qParams: Params) => {
if (
@@ -100,6 +119,9 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
this.email,
passwordInputResult,
this.emailVerificationToken,
this.orgSponsoredFreeFamilyPlanToken,
this.acceptEmergencyAccessInviteToken,
this.emergencyAccessId,
);
} catch (e) {
this.validationService.showError(e);

View File

@@ -14,12 +14,18 @@ export abstract class RegistrationFinishService {
*
* @param email The email address of the user.
* @param passwordInputResult The password input result.
* @param emailVerificationToken The optional email verification token. Not present in org invite scenarios.
* @param emailVerificationToken The optional email verification token. Not present in emailed invite scenarios (ex: org invite).
* @param orgSponsoredFreeFamilyPlanToken The optional org sponsored free family plan token.
* @param acceptEmergencyAccessInviteToken The optional accept emergency access invite token.
* @param emergencyAccessId The optional emergency access id which is required to validate the emergency access invite token.
* Returns a promise which resolves to the captcha bypass token string upon a successful account creation.
*/
abstract finishRegistration(
email: string,
passwordInputResult: PasswordInputResult,
emailVerificationToken?: string,
orgSponsoredFreeFamilyPlanToken?: string,
acceptEmergencyAccessInviteToken?: string,
emergencyAccessId?: string,
): Promise<string>;
}

View File

@@ -113,6 +113,9 @@ export abstract class OrganizationService {
* https://bitwarden.atlassian.net/browse/AC-2252.
*/
getFromState: (id: string) => Promise<Organization>;
/**
* Emits true if the user can create or manage a Free Bitwarden Families sponsorship.
*/
canManageSponsorships$: Observable<boolean>;
hasOrganizations: () => Promise<boolean>;
get$: (id: string) => Observable<Organization | undefined>;

View File

@@ -183,14 +183,7 @@ export class Organization {
return this.isAdmin || this.permissions.editAnyCollection;
}
canEditUnassignedCiphers(restrictProviderAccessFlagEnabled: boolean) {
// Providers can access items until the restrictProviderAccess flag is enabled
// After the flag is enabled and removed, this block will be deleted
// so that they permanently lose access to items
if (this.isProviderUser && !restrictProviderAccessFlagEnabled) {
return true;
}
get canEditUnassignedCiphers() {
return (
this.type === OrganizationUserType.Admin ||
this.type === OrganizationUserType.Owner ||
@@ -198,14 +191,7 @@ export class Organization {
);
}
canEditAllCiphers(restrictProviderAccessFlagEnabled: boolean) {
// Providers can access items until the restrictProviderAccess flag is enabled
// After the flag is enabled and removed, this block will be deleted
// so that they permanently lose access to items
if (this.isProviderUser && !restrictProviderAccessFlagEnabled) {
return true;
}
get canEditAllCiphers() {
// The allowAdminAccessToAllCollectionItems flag can restrict admins
// Custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag
return (

View File

@@ -16,5 +16,5 @@ export abstract class AuthService {
abstract authStatusFor$(userId: UserId): Observable<AuthenticationStatus>;
/** @deprecated use {@link activeAccountStatus$} instead */
abstract getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
abstract logOut: (callback: () => void) => void;
abstract logOut: (callback: () => void, userId?: string) => void;
}

View File

@@ -5,7 +5,6 @@ import { EncryptedString } from "../../../../platform/models/domain/enc-string";
export class RegisterFinishRequest {
constructor(
public email: string,
public emailVerificationToken: string,
public masterPasswordHash: string,
public masterPasswordHint: string,
@@ -18,6 +17,11 @@ export class RegisterFinishRequest {
public kdfMemory?: number,
public kdfParallelism?: number,
public emailVerificationToken?: string,
public orgSponsoredFreeFamilyPlanToken?: string,
public acceptEmergencyAccessInviteToken?: string,
public acceptEmergencyAccessId?: string,
// Org Invite data (only applies on web)
public organizationUserId?: string,
public orgInviteToken?: string,

View File

@@ -93,8 +93,8 @@ export class AuthService implements AuthServiceAbstraction {
return await firstValueFrom(this.authStatusFor$(userId as UserId));
}
logOut(callback: () => void) {
logOut(callback: () => void, userId?: string): void {
callback();
this.messageSender.send("loggedOut");
this.messageSender.send("loggedOut", { userId });
}
}

View File

@@ -12,7 +12,6 @@ export enum FeatureFlag {
EnableDeleteProvider = "AC-1218-delete-provider",
ExtensionRefresh = "extension-refresh",
PersistPopupView = "persist-popup-view",
RestrictProviderAccess = "restrict-provider-access",
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
EmailVerification = "email-verification",
@@ -59,7 +58,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.EnableDeleteProvider]: FALSE,
[FeatureFlag.ExtensionRefresh]: FALSE,
[FeatureFlag.PersistPopupView]: FALSE,
[FeatureFlag.RestrictProviderAccess]: FALSE,
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
[FeatureFlag.EmailVerification]: FALSE,

View File

@@ -1,6 +1,7 @@
const NoValue = Symbol("NoValue");
export class Lazy<T> {
private _value: T | undefined = undefined;
private _isCreated = false;
private _value: T | typeof NoValue = NoValue;
constructor(private readonly factory: () => T) {}
@@ -10,11 +11,10 @@ export class Lazy<T> {
* @returns The value produced by your factory.
*/
get(): T {
if (!this._isCreated) {
this._value = this.factory();
this._isCreated = true;
if (this._value === NoValue) {
return (this._value = this.factory());
}
return this._value as T;
return this._value;
}
}

View File

@@ -1,4 +1,4 @@
import { combineLatest, filter, firstValueFrom, map, switchMap, timeout } from "rxjs";
import { combineLatest, concatMap, filter, firstValueFrom, map, timeout } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -79,7 +79,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
this.accountService.activeAccount$,
this.accountService.accountActivity$,
]).pipe(
switchMap(async ([activeAccount, accountActivity]) => {
concatMap(async ([activeAccount, accountActivity]) => {
const activeUserId = activeAccount?.id;
for (const userIdString in accountActivity) {
const userId = userIdString as UserId;

View File

@@ -77,7 +77,7 @@ export type SingleUserDependency = {
export type OnDependency = {
/** The stream that controls emissions
*/
on$: Observable<void>;
on$: Observable<any>;
};
/** A pattern for types that emit when a dependency is `true`.

View File

@@ -1,11 +1,6 @@
import { GenerationRequest } from "../../types";
/** Options that provide contextual information about the application state
* when an integration is invoked.
*/
export type IntegrationRequest = {
/** @param website The domain of the website the requested integration is used
* within. This should be set to `null` when the request is not specific
* to any website.
* @remarks this field contains sensitive data
*/
website: string | null;
};
export type IntegrationRequest = Partial<GenerationRequest>;

View File

@@ -46,3 +46,20 @@ export type Constraints<T> = {
/** utility type for methods that evaluate constraints generically. */
export type AnyConstraint = PrimitiveConstraint & StringConstraints & NumberConstraints;
/** Options that provide contextual information about the application state
* when a generator is invoked.
*/
export type VaultItemRequest = {
/** The domain of the website the requested credential is used
* within. This should be set to `null` when the request is not specific
* to any website.
* @remarks this field contains sensitive data
*/
website: string | null;
};
/** Options that provide contextual information about the application state
* when a generator is invoked.
*/
export type GenerationRequest = Partial<VaultItemRequest>;

View File

@@ -5,4 +5,5 @@ export class FolderApiServiceAbstraction {
save: (folder: Folder) => Promise<any>;
delete: (id: string) => Promise<any>;
get: (id: string) => Promise<FolderResponse>;
deleteAll: () => Promise<void>;
}

View File

@@ -38,18 +38,14 @@ export class CollectionView implements View, ITreeNodeObject {
}
}
canEditItems(org: Organization, restrictProviderAccess: boolean): boolean {
canEditItems(org: Organization): boolean {
if (org != null && org.id !== this.organizationId) {
throw new Error(
"Id of the organization provided does not match the org id of the collection.",
);
}
return (
org?.canEditAllCiphers(restrictProviderAccess) ||
this.manage ||
(this.assigned && !this.readOnly)
);
return org?.canEditAllCiphers || this.manage || (this.assigned && !this.readOnly);
}
/**

View File

@@ -32,6 +32,11 @@ export class FolderApiService implements FolderApiServiceAbstraction {
await this.folderService.delete(id);
}
async deleteAll(): Promise<void> {
await this.apiService.send("DELETE", "/folders/all", null, true, false);
await this.folderService.clear();
}
async get(id: string): Promise<FolderResponse> {
const r = await this.apiService.send("GET", "/folders/" + id, null, true, true);
return new FolderResponse(r);

View File

@@ -2,17 +2,17 @@ import { svgIcon } from "../icon";
export const NoResults = svgIcon`
<svg width="98" height="96" viewBox="0 0 98 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="tw-stroke-text-headers" d="M8.8545 86.7919L56.9901 86.7919C60.2321 86.7919 62.8603 84.1637 62.8603 80.9217L62.8603 32.2678C62.8603 30.7472 62.2702 29.2859 61.2143 28.1916L47.5536 14.0345C46.4473 12.8881 44.9225 12.2405 43.3293 12.2405L8.85451 12.2405C5.61249 12.2405 2.98431 14.8687 2.98431 18.1107L2.98431 80.9217C2.98431 84.1637 5.61248 86.7919 8.8545 86.7919Z" stroke-width="1.76106"/>
<path class="tw-fill-background tw-stroke-text-headers" d="M18.8335 76.8125L66.9691 76.8125C70.2111 76.8125 72.8393 74.1844 72.8393 70.9423L72.8393 21.8271C72.8393 20.3144 72.2554 18.8601 71.2093 17.7675L57.5349 3.48471C56.4276 2.32814 54.8959 1.67408 53.2947 1.67408L18.8335 1.67407C15.5915 1.67407 12.9633 4.30225 12.9633 7.54427L12.9633 70.9423C12.9633 74.1844 15.5915 76.8125 18.8335 76.8125Z" stroke-width="1.76106"/>
<path class="tw-stroke-text-headers" d="M54.3484 2.26123L54.3484 14.0016C54.3484 17.2436 56.9766 19.8718 60.2186 19.8718L72.546 19.8718" stroke-width="1.76106"/>
<path class="tw-stroke-info-600" d="M20.0914 15.9861L43.5722 15.9861" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-stroke-info-600" d="M20.0914 30.8945L51.2034 30.8945" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-stroke-info-600" d="M20.0914 45.803L45.9203 45.803" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-stroke-info-600" d="M20.0914 60.7112L45.9203 60.7112" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-fill-background tw-stroke-text-headers" d="M85.4233 53.9449C81.9863 66.772 68.6684 74.3484 55.6768 70.8674C42.6853 67.3863 34.9398 54.1659 38.3768 41.3388C41.8138 28.5117 55.1318 20.9353 68.1234 24.4163C81.1149 27.8974 88.8604 41.1178 85.4233 53.9449Z" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-info-600" d="M55.1859 41.5395C55.1859 41.5395 55.2828 39.2314 57.5434 37.273C58.8998 36.084 60.5145 35.7692 61.9678 35.7343C63.2919 35.6993 64.4868 35.9441 65.1649 36.3288C66.3921 36.9583 68.7497 38.462 68.7497 41.7144C68.7497 45.1416 66.6828 46.6804 64.3576 48.394C62.0324 50.1076 62.3667 52.3385 62.3667 54.227" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-fill-info-600 tw-stroke-secondary-600" d="M62.2727 59.2015C62.759 59.2015 63.1533 58.8073 63.1533 58.321C63.1533 57.8347 62.759 57.4404 62.2727 57.4404C61.7864 57.4404 61.3922 57.8347 61.3922 58.321C61.3922 58.8073 61.7864 59.2015 62.2727 59.2015Z"/>
<path class="tw-fill-secondary-300 tw-stroke-text-headers" d="M96.0333 89.0621L95.4703 89.5329C94.2269 90.5728 92.3758 90.4078 91.3359 89.1644L78.2766 73.5488L74.79 69.3798C74.4843 69.0105 74.6096 68.4514 75.0271 68.2155C76.7198 67.2592 78.097 65.9974 78.8894 65.1364C79.1502 64.853 79.6089 64.8477 79.856 65.1431L83.3425 69.3121L96.4018 84.9277C97.4418 86.1712 97.2768 88.0222 96.0333 89.0621Z" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M8.8545 86.7919L56.9901 86.7919C60.2321 86.7919 62.8603 84.1637 62.8603 80.9217L62.8603 32.2678C62.8603 30.7472 62.2702 29.2859 61.2143 28.1916L47.5536 14.0345C46.4473 12.8881 44.9225 12.2405 43.3293 12.2405L8.85451 12.2405C5.61249 12.2405 2.98431 14.8687 2.98431 18.1107L2.98431 80.9217C2.98431 84.1637 5.61248 86.7919 8.8545 86.7919Z" stroke-width="1.76106"/>
<path class="tw-fill-background tw-stroke-art-primary" d="M18.8335 76.8125L66.9691 76.8125C70.2111 76.8125 72.8393 74.1844 72.8393 70.9423L72.8393 21.8271C72.8393 20.3144 72.2554 18.8601 71.2093 17.7675L57.5349 3.48471C56.4276 2.32814 54.8959 1.67408 53.2947 1.67408L18.8335 1.67407C15.5915 1.67407 12.9633 4.30225 12.9633 7.54427L12.9633 70.9423C12.9633 74.1844 15.5915 76.8125 18.8335 76.8125Z" stroke-width="1.76106"/>
<path class="tw-stroke-art-primary" d="M54.3484 2.26123L54.3484 14.0016C54.3484 17.2436 56.9766 19.8718 60.2186 19.8718L72.546 19.8718" stroke-width="1.76106"/>
<path class="tw-stroke-art-accent" d="M20.0914 15.9861L43.5722 15.9861" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-stroke-art-accent" d="M20.0914 30.8945L51.2034 30.8945" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-stroke-art-accent" d="M20.0914 45.803L45.9203 45.803" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-stroke-art-accent" d="M20.0914 60.7112L45.9203 60.7112" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
<path class="tw-fill-background tw-stroke-art-primary" d="M85.4233 53.9449C81.9863 66.772 68.6684 74.3484 55.6768 70.8674C42.6853 67.3863 34.9398 54.1659 38.3768 41.3388C41.8138 28.5117 55.1318 20.9353 68.1234 24.4163C81.1149 27.8974 88.8604 41.1178 85.4233 53.9449Z" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-accent" d="M55.1859 41.5395C55.1859 41.5395 55.2828 39.2314 57.5434 37.273C58.8998 36.084 60.5145 35.7692 61.9678 35.7343C63.2919 35.6993 64.4868 35.9441 65.1649 36.3288C66.3921 36.9583 68.7497 38.462 68.7497 41.7144C68.7497 45.1416 66.6828 46.6804 64.3576 48.394C62.0324 50.1076 62.3667 52.3385 62.3667 54.227" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-fill-art-accent tw-stroke-secondary-600" d="M62.2727 59.2015C62.759 59.2015 63.1533 58.8073 63.1533 58.321C63.1533 57.8347 62.759 57.4404 62.2727 57.4404C61.7864 57.4404 61.3922 57.8347 61.3922 58.321C61.3922 58.8073 61.7864 59.2015 62.2727 59.2015Z"/>
<path class="tw-fill-secondary-300 tw-stroke-art-primary" d="M96.0333 89.0621L95.4703 89.5329C94.2269 90.5728 92.3758 90.4078 91.3359 89.1644L78.2766 73.5488L74.79 69.3798C74.4843 69.0105 74.6096 68.4514 75.0271 68.2155C76.7198 67.2592 78.097 65.9974 78.8894 65.1364C79.1502 64.853 79.6089 64.8477 79.856 65.1431L83.3425 69.3121L96.4018 84.9277C97.4418 86.1712 97.2768 88.0222 96.0333 89.0621Z" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;

View File

@@ -42,6 +42,9 @@
--color-info-600: 85 85 85;
--color-info-700: 59 58 58;
--color-art-primary: 2 15 102;
--color-art-accent: 85 85 85;
--color-text-main: 33 37 41;
--color-text-muted: 109 117 126;
--color-text-contrast: 255 255 255;
@@ -90,6 +93,9 @@
--color-info-600: 164 176 198;
--color-info-700: 209 215 226;
--color-art-primary: 226 227 228;
--color-art-accent: 164 176 198;
--color-text-main: 255 255 255;
--color-text-muted: 186 192 206;
--color-text-contrast: 25 30 38;

View File

@@ -54,6 +54,10 @@ module.exports = {
600: rgba("--color-info-600"),
700: rgba("--color-info-700"),
},
art: {
primary: rgba("--color-art-primary"),
accent: rgba("--color-art-accent"),
},
text: {
main: rgba("--color-text-main"),
muted: rgba("--color-text-muted"),

View File

@@ -87,7 +87,9 @@
[disabled]="!filePassword"
appStopClick
bitSuffix
(click)="copyPasswordToClipboard()"
[appCopyClick]="filePassword"
[valueLabel]="'password' | i18n"
showToast
></button>
<bit-hint>{{ "exportPasswordDescription" | i18n }}</bit-hint>
</bit-form-field>

View File

@@ -121,7 +121,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
encryptedExportType = EncryptedExportType;
protected showFilePassword: boolean;
filePasswordValue: string = null;
private _disabledByPolicy = false;
organizations$: Observable<Organization[]>;
@@ -278,18 +277,9 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
generatePassword = async () => {
const [options] = await this.passwordGenerationService.getOptions();
this.filePasswordValue = await this.passwordGenerationService.generatePassword(options);
this.exportForm.get("filePassword").setValue(this.filePasswordValue);
this.exportForm.get("confirmFilePassword").setValue(this.filePasswordValue);
};
copyPasswordToClipboard = async () => {
this.platformUtilsService.copyToClipboard(this.filePasswordValue);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t("password")),
});
const generatedPassword = await this.passwordGenerationService.generatePassword(options);
this.exportForm.get("filePassword").setValue(generatedPassword);
this.exportForm.get("confirmFilePassword").setValue(generatedPassword);
};
submit = async () => {

View File

@@ -4,41 +4,60 @@ import { ReactiveFormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import {
CardComponent,
CheckboxModule,
ColorPasswordModule,
FormFieldModule,
IconButtonModule,
InputModule,
ItemModule,
SectionComponent,
SectionHeaderComponent,
ToggleGroupModule,
} from "@bitwarden/components";
import { CredentialGeneratorService } from "@bitwarden/generator-core";
import {
createRandomizer,
CredentialGeneratorService,
Randomizer,
} from "@bitwarden/generator-core";
const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
/** Shared module containing generator component dependencies */
@NgModule({
imports: [SectionComponent, SectionHeaderComponent, CardComponent],
imports: [CardComponent, SectionComponent, SectionHeaderComponent],
exports: [
CardComponent,
CheckboxModule,
CommonModule,
ColorPasswordModule,
FormFieldModule,
IconButtonModule,
InputModule,
ItemModule,
JslibModule,
JslibServicesModule,
FormFieldModule,
CommonModule,
ReactiveFormsModule,
ColorPasswordModule,
InputModule,
CheckboxModule,
SectionComponent,
SectionHeaderComponent,
CardComponent,
ToggleGroupModule,
],
providers: [
safeProvider({
provide: RANDOMIZER,
useFactory: createRandomizer,
deps: [CryptoService],
}),
safeProvider({
provide: CredentialGeneratorService,
useClass: CredentialGeneratorService,
deps: [StateProvider, PolicyService],
deps: [RANDOMIZER, StateProvider, PolicyService],
}),
],
declarations: [],

View File

@@ -2,3 +2,4 @@ export { PassphraseSettingsComponent } from "./passphrase-settings.component";
export { CredentialGeneratorHistoryComponent } from "./credential-generator-history.component";
export { EmptyCredentialHistoryComponent } from "./empty-credential-history.component";
export { PasswordSettingsComponent } from "./password-settings.component";
export { PasswordGeneratorComponent } from "./password-generator.component";

View File

@@ -0,0 +1,44 @@
<bit-toggle-group
fullWidth
class="tw-mb-4"
[selected]="credentialType$ | async"
(selectedChange)="onCredentialTypeChanged($event)"
attr.aria-label="{{ 'type' | i18n }}"
>
<bit-toggle value="password">
{{ "password" | i18n }}
</bit-toggle>
<bit-toggle value="passphrase">
{{ "passphrase" | i18n }}
</bit-toggle>
</bit-toggle-group>
<bit-card class="tw-flex tw-justify-between tw-mb-4">
<div class="tw-grow">
<bit-color-password class="tw-font-mono" [password]="value$ | async"></bit-color-password>
</div>
<div class="tw-space-x-1 tw-flex-none tw-w-4">
<button type="button" bitIconButton="bwi-generate" buttonType="main" (click)="generate$.next()">
{{ "generatePassword" | i18n }}
</button>
<button
type="button"
bitIconButton="bwi-clone"
buttonType="main"
[appCopyClick]="value$ | async"
>
{{ "copyPassword" | i18n }}
</button>
</div>
</bit-card>
<bit-password-settings
class="tw-mt-6"
*ngIf="(credentialType$ | async) === 'password'"
[userId]="this.userId$ | async"
(onUpdated)="generate$.next()"
/>
<bit-passphrase-settings
class="tw-mt-6"
*ngIf="(credentialType$ | async) === 'passphrase'"
[userId]="this.userId$ | async"
(onUpdated)="generate$.next()"
/>

View File

@@ -0,0 +1,117 @@
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
import { BehaviorSubject, distinctUntilChanged, map, Subject, switchMap, takeUntil } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CredentialGeneratorService, Generators, GeneratorType } from "@bitwarden/generator-core";
import { GeneratedCredential } from "@bitwarden/generator-history";
import { DependenciesModule } from "./dependencies";
import { PassphraseSettingsComponent } from "./passphrase-settings.component";
import { PasswordSettingsComponent } from "./password-settings.component";
/** Options group for passwords */
@Component({
standalone: true,
selector: "bit-password-generator",
templateUrl: "password-generator.component.html",
imports: [DependenciesModule, PasswordSettingsComponent, PassphraseSettingsComponent],
})
export class PasswordGeneratorComponent implements OnInit, OnDestroy {
constructor(
private generatorService: CredentialGeneratorService,
private accountService: AccountService,
private zone: NgZone,
) {}
/** Binds the passphrase component to a specific user's settings.
* When this input is not provided, the form binds to the active
* user
*/
@Input()
userId: UserId | null;
/** tracks the currently selected credential type */
protected credentialType$ = new BehaviorSubject<GeneratorType>("password");
/** Emits the last generated value. */
protected readonly value$ = new BehaviorSubject<string>("");
/** Emits when the userId changes */
protected readonly userId$ = new BehaviorSubject<UserId>(null);
/** Emits when a new credential is requested */
protected readonly generate$ = new Subject<void>();
/** Tracks changes to the selected credential type
* @param type the new credential type
*/
protected onCredentialTypeChanged(type: GeneratorType) {
if (this.credentialType$.value !== type) {
this.credentialType$.next(type);
this.generate$.next();
}
}
/** Emits credentials created from a generation request. */
@Output()
readonly onGenerated = new EventEmitter<GeneratedCredential>();
async ngOnInit() {
if (this.userId) {
this.userId$.next(this.userId);
} else {
this.accountService.activeAccount$
.pipe(
map((acct) => acct.id),
distinctUntilChanged(),
takeUntil(this.destroyed),
)
.subscribe(this.userId$);
}
this.credentialType$
.pipe(
switchMap((type) => this.typeToGenerator$(type)),
takeUntil(this.destroyed),
)
.subscribe((generated) => {
// update subjects within the angular zone so that the
// template bindings refresh immediately
this.zone.run(() => {
this.onGenerated.next(generated);
this.value$.next(generated.credential);
});
});
}
private typeToGenerator$(type: GeneratorType) {
const dependencies = {
on$: this.generate$,
userId$: this.userId$,
};
switch (type) {
case "password":
return this.generatorService.generate$(Generators.Password, dependencies);
case "passphrase":
return this.generatorService.generate$(Generators.Passphrase, dependencies);
default:
throw new Error(`Invalid generator type: "${type}"`);
}
}
private readonly destroyed = new Subject<void>();
ngOnDestroy(): void {
// tear down subscriptions
this.destroyed.complete();
// finalize subjects
this.generate$.complete();
this.value$.complete();
// finalize component bindings
this.onGenerated.complete();
}
}

View File

@@ -1,5 +1,8 @@
import { Randomizer } from "../abstractions";
import { PasswordRandomizer } from "../engine";
import { PASSPHRASE_SETTINGS, PASSWORD_SETTINGS } from "../strategies/storage";
import {
CredentialGenerator,
PassphraseGenerationOptions,
PassphraseGeneratorPolicy,
PasswordGenerationOptions,
@@ -14,6 +17,12 @@ import { DefaultPasswordGenerationOptions } from "./default-password-generation-
import { Policies } from "./policies";
const PASSPHRASE = Object.freeze({
category: "passphrase",
engine: {
create(randomizer: Randomizer): CredentialGenerator<PassphraseGenerationOptions> {
return new PasswordRandomizer(randomizer);
},
},
settings: {
initial: DefaultPassphraseGenerationOptions,
constraints: {
@@ -32,6 +41,12 @@ const PASSPHRASE = Object.freeze({
>);
const PASSWORD = Object.freeze({
category: "password",
engine: {
create(randomizer: Randomizer): CredentialGenerator<PasswordGenerationOptions> {
return new PasswordRandomizer(randomizer);
},
},
settings: {
initial: DefaultPasswordGenerationOptions,
constraints: {

View File

@@ -335,4 +335,40 @@ describe("PasswordRandomizer", () => {
expect(result).toEqual("foo-foo");
});
});
describe("generate", () => {
it("processes password generation options", async () => {
const password = new PasswordRandomizer(randomizer);
const result = await password.generate(
{},
{
length: 10,
},
);
expect(result.category).toEqual("password");
});
it("processes passphrase generation options", async () => {
const password = new PasswordRandomizer(randomizer);
const result = await password.generate(
{},
{
numWords: 10,
},
);
expect(result.category).toEqual("passphrase");
});
it("throws when it cannot recognize the options type", async () => {
const password = new PasswordRandomizer(randomizer);
const result = password.generate({}, {});
await expect(result).rejects.toBeInstanceOf(Error);
});
});
});

View File

@@ -1,13 +1,26 @@
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
import { GenerationRequest } from "@bitwarden/common/tools/types";
import {
CredentialGenerator,
GeneratedCredential,
PassphraseGenerationOptions,
PasswordGenerationOptions,
} from "../types";
import { optionsToEffWordListRequest, optionsToRandomAsciiRequest } from "../util";
import { Randomizer } from "./abstractions";
import { Ascii } from "./data";
import { CharacterSet, EffWordListRequest, RandomAsciiRequest } from "./types";
/** Generation algorithms that produce randomized secrets */
export class PasswordRandomizer {
export class PasswordRandomizer
implements
CredentialGenerator<PassphraseGenerationOptions>,
CredentialGenerator<PasswordGenerationOptions>
{
/** Instantiates the password randomizer
* @param random data source for random data
* @param randomizer data source for random data
*/
constructor(private randomizer: Randomizer) {}
@@ -52,6 +65,41 @@ export class PasswordRandomizer {
return wordList.join(request.separator);
}
generate(
request: GenerationRequest,
settings: PasswordGenerationOptions,
): Promise<GeneratedCredential>;
generate(
request: GenerationRequest,
settings: PassphraseGenerationOptions,
): Promise<GeneratedCredential>;
async generate(
_request: GenerationRequest,
settings: PasswordGenerationOptions | PassphraseGenerationOptions,
) {
if (isPasswordGenerationOptions(settings)) {
const request = optionsToRandomAsciiRequest(settings);
const password = await this.randomAscii(request);
return new GeneratedCredential(password, "password", Date.now());
} else if (isPassphraseGenerationOptions(settings)) {
const request = optionsToEffWordListRequest(settings);
const passphrase = await this.randomEffLongWords(request);
return new GeneratedCredential(passphrase, "passphrase", Date.now());
}
throw new Error("Invalid settings received by generator.");
}
}
function isPasswordGenerationOptions(settings: any): settings is PasswordGenerationOptions {
return "length" in (settings ?? {});
}
function isPassphraseGenerationOptions(settings: any): settings is PassphraseGenerationOptions {
return "numWords" in (settings ?? {});
}
// given a generator request, convert each of its `number | undefined` properties

View File

@@ -1,5 +1,5 @@
import { mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { BehaviorSubject, filter, firstValueFrom, Subject } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -8,9 +8,14 @@ import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/st
import { Constraints } from "@bitwarden/common/tools/types";
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
import { FakeStateProvider, FakeAccountService, awaitAsync } from "../../../../../common/spec";
import { PolicyEvaluator } from "../abstractions";
import { CredentialGeneratorConfiguration } from "../types";
import {
FakeStateProvider,
FakeAccountService,
awaitAsync,
ObservableTracker,
} from "../../../../../common/spec";
import { PolicyEvaluator, Randomizer } from "../abstractions";
import { CredentialGeneratorConfiguration, GeneratedCredential } from "../types";
import { CredentialGeneratorService } from "./credential-generator.service";
@@ -34,8 +39,23 @@ const somePolicy = new Policy({
enabled: true,
});
const SomeTime = new Date(1);
const SomeCategory = "passphrase";
// fake the configuration
const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePolicy> = {
category: SomeCategory,
engine: {
create: (randomizer) => {
return {
generate: (request, settings) => {
const credential = request.website ? `${request.website}|${settings.foo}` : settings.foo;
const result = new GeneratedCredential(credential, SomeCategory, SomeTime);
return Promise.resolve(result);
},
};
},
},
settings: {
initial: { foo: "initial" },
constraints: { foo: {} },
@@ -87,6 +107,9 @@ const accountService = new FakeAccountService({
// fake state
const stateProvider = new FakeStateProvider(accountService);
// fake randomizer
const randomizer = mock<Randomizer>();
describe("CredentialGeneratorService", () => {
beforeEach(async () => {
await accountService.switchAccount(SomeUser);
@@ -94,10 +117,242 @@ describe("CredentialGeneratorService", () => {
jest.clearAllMocks();
});
describe("generate$", () => {
it("emits a generation for the active user when subscribed", async () => {
const settings = { foo: "value" };
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
const result = await generated.expectEmission();
expect(result).toEqual(new GeneratedCredential("value", SomeCategory, SomeTime));
});
it("follows the active user", async () => {
const someSettings = { foo: "some value" };
const anotherSettings = { foo: "another value" };
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
await accountService.switchAccount(AnotherUser);
await generated.pauseUntilReceived(2);
generated.unsubscribe();
expect(generated.emissions).toEqual([
new GeneratedCredential("some value", SomeCategory, SomeTime),
new GeneratedCredential("another value", SomeCategory, SomeTime),
]);
});
it("emits a generation when the settings change", async () => {
const someSettings = { foo: "some value" };
const anotherSettings = { foo: "another value" };
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
await stateProvider.setUserState(SettingsKey, anotherSettings, SomeUser);
await generated.pauseUntilReceived(2);
generated.unsubscribe();
expect(generated.emissions).toEqual([
new GeneratedCredential("some value", SomeCategory, SomeTime),
new GeneratedCredential("another value", SomeCategory, SomeTime),
]);
});
// FIXME: test these when the fake state provider can create the required emissions
it.todo("errors when the settings error");
it.todo("completes when the settings complete");
it("includes `website$`'s last emitted value", async () => {
const settings = { foo: "value" };
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const website$ = new BehaviorSubject("some website");
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { website$ }));
const result = await generated.expectEmission();
expect(result).toEqual(new GeneratedCredential("some website|value", SomeCategory, SomeTime));
});
it("errors when `website$` errors", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const website$ = new BehaviorSubject("some website");
let error = null;
generator.generate$(SomeConfiguration, { website$ }).subscribe({
error: (e: unknown) => {
error = e;
},
});
website$.error({ some: "error" });
await awaitAsync();
expect(error).toEqual({ some: "error" });
});
it("completes when `website$` completes", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const website$ = new BehaviorSubject("some website");
let completed = false;
generator.generate$(SomeConfiguration, { website$ }).subscribe({
complete: () => {
completed = true;
},
});
website$.complete();
await awaitAsync();
expect(completed).toBeTruthy();
});
it("emits a generation for a specific user when `user$` supplied", async () => {
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ }));
const result = await generated.expectEmission();
expect(result).toEqual(new GeneratedCredential("another", SomeCategory, SomeTime));
});
it("emits a generation for a specific user when `user$` emits", async () => {
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.pipe(filter((u) => !!u));
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ }));
userId.next(AnotherUser);
const result = await generated.pauseUntilReceived(2);
expect(result).toEqual([
new GeneratedCredential("value", SomeCategory, SomeTime),
new GeneratedCredential("another", SomeCategory, SomeTime),
]);
});
it("errors when `user$` errors", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId$ = new BehaviorSubject(SomeUser);
let error = null;
generator.generate$(SomeConfiguration, { userId$ }).subscribe({
error: (e: unknown) => {
error = e;
},
});
userId$.error({ some: "error" });
await awaitAsync();
expect(error).toEqual({ some: "error" });
});
it("completes when `user$` completes", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId$ = new BehaviorSubject(SomeUser);
let completed = false;
generator.generate$(SomeConfiguration, { userId$ }).subscribe({
complete: () => {
completed = true;
},
});
userId$.complete();
await awaitAsync();
expect(completed).toBeTruthy();
});
it("emits a generation only when `on$` emits", async () => {
// This test breaks from arrange/act/assert because it is testing causality
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const on$ = new Subject<void>();
const results: any[] = [];
// confirm no emission during subscription
const sub = generator
.generate$(SomeConfiguration, { on$ })
.subscribe((result) => results.push(result));
await awaitAsync();
expect(results.length).toEqual(0);
// confirm forwarded emission
on$.next();
await awaitAsync();
expect(results).toEqual([new GeneratedCredential("value", SomeCategory, SomeTime)]);
// confirm updating settings does not cause an emission
await stateProvider.setUserState(SettingsKey, { foo: "next" }, SomeUser);
await awaitAsync();
expect(results.length).toBe(1);
// confirm forwarded emission takes latest value
on$.next();
await awaitAsync();
sub.unsubscribe();
expect(results).toEqual([
new GeneratedCredential("value", SomeCategory, SomeTime),
new GeneratedCredential("next", SomeCategory, SomeTime),
]);
});
it("errors when `on$` errors", async () => {
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const on$ = new Subject<void>();
let error: any = null;
// confirm no emission during subscription
generator.generate$(SomeConfiguration, { on$ }).subscribe({
error: (e: unknown) => {
error = e;
},
});
on$.error({ some: "error" });
await awaitAsync();
expect(error).toEqual({ some: "error" });
});
it("completes when `on$` completes", async () => {
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const on$ = new Subject<void>();
let complete = false;
// confirm no emission during subscription
generator.generate$(SomeConfiguration, { on$ }).subscribe({
complete: () => {
complete = true;
},
});
on$.complete();
await awaitAsync();
expect(complete).toBeTruthy();
});
});
describe("settings$", () => {
it("defaults to the configuration's initial settings if settings aren't found", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
@@ -107,7 +362,7 @@ describe("CredentialGeneratorService", () => {
it("reads from the active user's configuration-defined storage", async () => {
const settings = { foo: "value" };
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
@@ -119,7 +374,7 @@ describe("CredentialGeneratorService", () => {
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
const policy$ = new BehaviorSubject([somePolicy]);
policyService.getAll$.mockReturnValue(policy$);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const result = await firstValueFrom(generator.settings$(SomeConfiguration));
@@ -131,7 +386,7 @@ describe("CredentialGeneratorService", () => {
const anotherSettings = { foo: "another" };
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const results: any = [];
const sub = generator.settings$(SomeConfiguration).subscribe((r) => results.push(r));
@@ -148,7 +403,7 @@ describe("CredentialGeneratorService", () => {
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
const anotherSettings = { foo: "another" };
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
const result = await firstValueFrom(generator.settings$(SomeConfiguration, { userId$ }));
@@ -161,7 +416,7 @@ describe("CredentialGeneratorService", () => {
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
const anotherSettings = { foo: "another" };
await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
const results: any = [];
@@ -180,7 +435,7 @@ describe("CredentialGeneratorService", () => {
it("errors when the arbitrary user's stream errors", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
let error = null;
@@ -198,7 +453,7 @@ describe("CredentialGeneratorService", () => {
it("completes when the arbitrary user's stream completes", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
let completed = false;
@@ -216,7 +471,7 @@ describe("CredentialGeneratorService", () => {
it("ignores repeated arbitrary user emissions", async () => {
await stateProvider.setUserState(SettingsKey, null, SomeUser);
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
let count = 0;
@@ -240,7 +495,7 @@ describe("CredentialGeneratorService", () => {
describe("settings", () => {
it("writes to the user's state", async () => {
const singleUserId$ = new BehaviorSubject(SomeUser).asObservable();
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const subject = await generator.settings(SomeConfiguration, { singleUserId$ });
subject.next({ foo: "next value" });
@@ -253,7 +508,7 @@ describe("CredentialGeneratorService", () => {
it("waits for the user to become available", async () => {
const singleUserId = new BehaviorSubject(null);
const singleUserId$ = singleUserId.asObservable();
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
let completed = false;
const promise = generator.settings(SomeConfiguration, { singleUserId$ }).then((settings) => {
@@ -271,7 +526,7 @@ describe("CredentialGeneratorService", () => {
describe("policy$", () => {
it("creates a disabled policy evaluator when there is no policy", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId$ = new BehaviorSubject(SomeUser).asObservable();
const result = await firstValueFrom(generator.policy$(SomeConfiguration, { userId$ }));
@@ -281,7 +536,7 @@ describe("CredentialGeneratorService", () => {
});
it("creates an active policy evaluator when there is a policy", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId$ = new BehaviorSubject(SomeUser).asObservable();
const policy$ = new BehaviorSubject([somePolicy]);
policyService.getAll$.mockReturnValue(policy$);
@@ -293,7 +548,7 @@ describe("CredentialGeneratorService", () => {
});
it("follows policy emissions", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
const somePolicySubject = new BehaviorSubject([somePolicy]);
@@ -316,7 +571,7 @@ describe("CredentialGeneratorService", () => {
});
it("follows user emissions", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
const somePolicy$ = new BehaviorSubject([somePolicy]).asObservable();
@@ -340,7 +595,7 @@ describe("CredentialGeneratorService", () => {
});
it("errors when the user errors", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();
const expectedError = { some: "error" };
@@ -358,7 +613,7 @@ describe("CredentialGeneratorService", () => {
});
it("completes when the user completes", async () => {
const generator = new CredentialGeneratorService(stateProvider, policyService);
const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService);
const userId = new BehaviorSubject(SomeUser);
const userId$ = userId.asObservable();

View File

@@ -1,5 +1,7 @@
import {
BehaviorSubject,
combineLatest,
concatMap,
distinctUntilChanged,
endWith,
filter,
@@ -8,32 +10,84 @@ import {
map,
mergeMap,
Observable,
race,
switchMap,
takeUntil,
withLatestFrom,
} from "rxjs";
import { Simplify } from "type-fest";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { StateProvider } from "@bitwarden/common/platform/state";
import { SingleUserDependency, UserDependency } from "@bitwarden/common/tools/dependencies";
import {
OnDependency,
SingleUserDependency,
UserDependency,
} from "@bitwarden/common/tools/dependencies";
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
import { Constraints } from "@bitwarden/common/tools/types";
import { PolicyEvaluator } from "../abstractions";
import { PolicyEvaluator, Randomizer } from "../abstractions";
import { mapPolicyToEvaluatorV2 } from "../rx";
import { CredentialGeneratorConfiguration as Configuration } from "../types/credential-generator-configuration";
type Policy$Dependencies = UserDependency;
type Settings$Dependencies = Partial<UserDependency>;
type Generate$Dependencies = Simplify<Partial<OnDependency> & Partial<UserDependency>> & {
/** Emits the active website when subscribed.
*
* The generator does not respond to emissions of this interface;
* If it is provided, the generator blocks until a value becomes available.
* When `website$` is omitted, the generator uses the empty string instead.
* When `website$` completes, the generator completes.
* When `website$` errors, the generator forwards the error.
*/
website$?: Observable<string>;
};
// FIXME: once the modernization is complete, switch the type parameters
// in `PolicyEvaluator<P, S>` and bake-in the constraints type.
type Evaluator<Settings, Policy> = PolicyEvaluator<Policy, Settings> & Constraints<Settings>;
export class CredentialGeneratorService {
constructor(
private randomizer: Randomizer,
private stateProvider: StateProvider,
private policyService: PolicyService,
) {}
/** Generates a stream of credentials
* @param configuration determines which generator's settings are loaded
* @param dependencies.on$ when specified, a new credential is emitted when
* this emits. Otherwise, a new credential is emitted when the settings
* update.
*/
generate$<Settings, Policy>(
configuration: Readonly<Configuration<Settings, Policy>>,
dependencies?: Generate$Dependencies,
) {
// instantiate the engine
const engine = configuration.engine.create(this.randomizer);
// stream blocks until all of these values are received
const website$ = dependencies?.website$ ?? new BehaviorSubject<string>(null);
const request$ = website$.pipe(map((website) => ({ website })));
const settings$ = this.settings$(configuration, dependencies);
// monitor completion
const requestComplete$ = request$.pipe(ignoreElements(), endWith(true));
const settingsComplete$ = request$.pipe(ignoreElements(), endWith(true));
const complete$ = race(requestComplete$, settingsComplete$);
// generation proper
const generate$ = (dependencies?.on$ ?? settings$).pipe(
withLatestFrom(request$, settings$),
concatMap(([, request, settings]) => engine.generate(request, settings)),
takeUntil(complete$),
);
return generate$;
}
/** Get the settings for the provided configuration
* @param configuration determines which generator's settings are loaded
* @param dependencies.userId$ identifies the user to which the settings are bound.
@@ -82,7 +136,7 @@ export class CredentialGeneratorService {
* @remarks the subject enforces policy for the settings
*/
async settings<Settings, Policy>(
configuration: Configuration<Settings, Policy>,
configuration: Readonly<Configuration<Settings, Policy>>,
dependencies: SingleUserDependency,
) {
const userId = await firstValueFrom(

View File

@@ -17,7 +17,7 @@ import { PASSPHRASE_SETTINGS } from "./storage";
const SomeUser = "some user" as UserId;
describe("Password generation strategy", () => {
describe("Passphrase generation strategy", () => {
describe("toEvaluator()", () => {
it("should map to the policy evaluator", async () => {
const strategy = new PassphraseGeneratorStrategy(null, null);

View File

@@ -2,11 +2,11 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { StateProvider } from "@bitwarden/common/platform/state";
import { GeneratorStrategy } from "../abstractions";
import { DefaultPassphraseBoundaries, DefaultPassphraseGenerationOptions, Policies } from "../data";
import { DefaultPassphraseGenerationOptions, Policies } from "../data";
import { PasswordRandomizer } from "../engine";
import { mapPolicyToEvaluator } from "../rx";
import { PassphraseGenerationOptions, PassphraseGeneratorPolicy } from "../types";
import { observe$PerUserId, sharedStateByUserId } from "../util";
import { observe$PerUserId, optionsToEffWordListRequest, sharedStateByUserId } from "../util";
import { PASSPHRASE_SETTINGS } from "./storage";
@@ -33,13 +33,7 @@ export class PassphraseGeneratorStrategy
// algorithm
async generate(options: PassphraseGenerationOptions): Promise<string> {
const requestWords = options.numWords ?? DefaultPassphraseGenerationOptions.numWords;
const request = {
numberOfWords: Math.max(requestWords, DefaultPassphraseBoundaries.numWords.min),
capitalize: options.capitalize ?? DefaultPassphraseGenerationOptions.capitalize,
number: options.includeNumber ?? DefaultPassphraseGenerationOptions.includeNumber,
separator: options.wordSeparator ?? DefaultPassphraseGenerationOptions.wordSeparator,
};
const request = optionsToEffWordListRequest(options);
return this.randomizer.randomEffLongWords(request);
}

View File

@@ -6,7 +6,7 @@ import { Policies, DefaultPasswordGenerationOptions } from "../data";
import { PasswordRandomizer } from "../engine";
import { mapPolicyToEvaluator } from "../rx";
import { PasswordGenerationOptions, PasswordGeneratorPolicy } from "../types";
import { observe$PerUserId, sharedStateByUserId, sum } from "../util";
import { observe$PerUserId, optionsToRandomAsciiRequest, sharedStateByUserId } from "../util";
import { PASSWORD_SETTINGS } from "./storage";
@@ -32,62 +32,7 @@ export class PasswordGeneratorStrategy
// algorithm
async generate(options: PasswordGenerationOptions): Promise<string> {
// converts password generation option sets, which are defined by
// an "enabled" and "quantity" parameter, to the password engine's
// parameters, which represent disabled options as `undefined`
// properties.
function process(
// values read from the options
enabled: boolean,
quantity: number,
// value used if an option is missing
defaultEnabled: boolean,
defaultQuantity: number,
) {
const isEnabled = enabled ?? defaultEnabled;
const actualQuantity = quantity ?? defaultQuantity;
const result = isEnabled ? actualQuantity : undefined;
return result;
}
const request = {
uppercase: process(
options.uppercase,
options.minUppercase,
DefaultPasswordGenerationOptions.uppercase,
DefaultPasswordGenerationOptions.minUppercase,
),
lowercase: process(
options.lowercase,
options.minLowercase,
DefaultPasswordGenerationOptions.lowercase,
DefaultPasswordGenerationOptions.minLowercase,
),
digits: process(
options.number,
options.minNumber,
DefaultPasswordGenerationOptions.number,
DefaultPasswordGenerationOptions.minNumber,
),
special: process(
options.special,
options.minSpecial,
DefaultPasswordGenerationOptions.special,
DefaultPasswordGenerationOptions.minSpecial,
),
ambiguous: options.ambiguous ?? DefaultPasswordGenerationOptions.ambiguous,
all: 0,
};
// engine represents character sets as "include only"; you assert how many all
// characters there can be rather than a total length. This conversion has
// the character classes win, so that the result is always consistent with policy
// minimums.
const required = sum(request.uppercase, request.lowercase, request.digits, request.special);
const remaining = (options.length ?? 0) - required;
request.all = Math.max(remaining, 0);
const request = optionsToRandomAsciiRequest(options);
const result = await this.randomizer.randomAscii(request);
return result;

View File

@@ -0,0 +1,5 @@
/** Kinds of credentials that can be stored by the history service
* password - a secret consisting of arbitrary characters used to authenticate a user
* passphrase - a secret consisting of words used to authenticate a user
*/
export type CredentialCategory = "password" | "passphrase";

View File

@@ -1,9 +1,29 @@
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
import { Constraints } from "@bitwarden/common/tools/types";
import { Randomizer } from "../abstractions";
import { PolicyConfiguration } from "../types";
import { CredentialCategory } from "./credential-category";
import { CredentialGenerator } from "./credential-generator";
export type CredentialGeneratorConfiguration<Settings, Policy> = {
/** Category describing usage of the credential generated by this configuration
*/
category: CredentialCategory;
/** An algorithm that generates credentials when ran. */
engine: {
/** Factory for the generator
*/
// FIXME: note that this erases the engine's type so that credentials are
// generated uniformly. This property needs to be maintained for
// the credential generator, but engine configurations should return
// the underlying type. `create` may be able to do double-duty w/ an
// engine definition if `CredentialGenerator` can be made covariant.
create: (randomizer: Randomizer) => CredentialGenerator<Settings>;
};
/** Defines the stored parameters for credential generation */
settings: {
/** value used when an account's settings haven't been initialized */
initial: Readonly<Partial<Settings>>;

View File

@@ -0,0 +1,12 @@
import { GenerationRequest } from "@bitwarden/common/tools/types";
import { GeneratedCredential } from "./generated-credential";
/** An algorithm that generates credentials. */
export type CredentialGenerator<Settings> = {
/** Generates a credential
* @param request runtime parameters
* @param settings stored parameters
*/
generate: (request: GenerationRequest, settings: Settings) => Promise<GeneratedCredential>;
};

View File

@@ -0,0 +1,58 @@
import { CredentialCategory, GeneratedCredential } from ".";
describe("GeneratedCredential", () => {
describe("constructor", () => {
it("assigns credential", () => {
const result = new GeneratedCredential("example", "passphrase", new Date(100));
expect(result.credential).toEqual("example");
});
it("assigns category", () => {
const result = new GeneratedCredential("example", "passphrase", new Date(100));
expect(result.category).toEqual("passphrase");
});
it("passes through date parameters", () => {
const result = new GeneratedCredential("example", "password", new Date(100));
expect(result.generationDate).toEqual(new Date(100));
});
it("converts numeric dates to Dates", () => {
const result = new GeneratedCredential("example", "password", 100);
expect(result.generationDate).toEqual(new Date(100));
});
});
it("toJSON converts from a credential into a JSON object", () => {
const credential = new GeneratedCredential("example", "password", new Date(100));
const result = credential.toJSON();
expect(result).toEqual({
credential: "example",
category: "password" as CredentialCategory,
generationDate: 100,
});
});
it("fromJSON converts Json objects into credentials", () => {
const jsonValue = {
credential: "example",
category: "password" as CredentialCategory,
generationDate: 100,
};
const result = GeneratedCredential.fromJSON(jsonValue);
expect(result).toBeInstanceOf(GeneratedCredential);
expect(result).toEqual({
credential: "example",
category: "password",
generationDate: new Date(100),
});
});
});

View File

@@ -0,0 +1,47 @@
import { Jsonify } from "type-fest";
import { CredentialCategory } from "./credential-category";
/** A credential generation result */
export class GeneratedCredential {
/**
* Instantiates a generated credential
* @param credential The value of the generated credential (e.g. a password)
* @param category The kind of credential
* @param generationDate The date that the credential was generated.
* Numeric values should are interpreted using {@link Date.valueOf}
* semantics.
*/
constructor(
readonly credential: string,
readonly category: CredentialCategory,
generationDate: Date | number,
) {
if (typeof generationDate === "number") {
this.generationDate = new Date(generationDate);
} else {
this.generationDate = generationDate;
}
}
/** The date that the credential was generated */
generationDate: Date;
/** Constructs a credential from its `toJSON` representation */
static fromJSON(jsonValue: Jsonify<GeneratedCredential>) {
return new GeneratedCredential(
jsonValue.credential,
jsonValue.category,
jsonValue.generationDate,
);
}
/** Serializes a credential to a JSON-compatible object */
toJSON() {
return {
credential: this.credential,
category: this.category,
generationDate: this.generationDate.valueOf(),
};
}
}

View File

@@ -1,8 +1,11 @@
export * from "./boundary";
export * from "./catchall-generator-options";
export * from "./credential-category";
export * from "./credential-generator";
export * from "./credential-generator-configuration";
export * from "./eff-username-generator-options";
export * from "./forwarder-options";
export * from "./generated-credential";
export * from "./generator-options";
export * from "./generator-type";
export * from "./no-policy";

View File

@@ -1,4 +1,5 @@
import { sum } from "./util";
import { DefaultPassphraseGenerationOptions } from "./data";
import { optionsToEffWordListRequest, optionsToRandomAsciiRequest, sum } from "./util";
describe("sum", () => {
it("returns 0 when the list is empty", () => {
@@ -15,3 +16,411 @@ describe("sum", () => {
expect(sum(1, 2, 3)).toBe(6);
});
});
describe("optionsToRandomAsciiRequest", () => {
it("should map options", async () => {
const result = optionsToRandomAsciiRequest({
length: 20,
ambiguous: true,
uppercase: true,
lowercase: true,
number: true,
special: true,
minUppercase: 1,
minLowercase: 2,
minNumber: 3,
minSpecial: 4,
});
expect(result).toEqual({
all: 10,
uppercase: 1,
lowercase: 2,
digits: 3,
special: 4,
ambiguous: true,
});
});
it("should disable uppercase", async () => {
const result = optionsToRandomAsciiRequest({
length: 3,
ambiguous: true,
uppercase: false,
lowercase: true,
number: true,
special: true,
minUppercase: 1,
minLowercase: 1,
minNumber: 1,
minSpecial: 1,
});
expect(result).toEqual({
all: 0,
uppercase: undefined,
lowercase: 1,
digits: 1,
special: 1,
ambiguous: true,
});
});
it("should disable lowercase", async () => {
const result = optionsToRandomAsciiRequest({
length: 3,
ambiguous: true,
uppercase: true,
lowercase: false,
number: true,
special: true,
minUppercase: 1,
minLowercase: 1,
minNumber: 1,
minSpecial: 1,
});
expect(result).toEqual({
all: 0,
uppercase: 1,
lowercase: undefined,
digits: 1,
special: 1,
ambiguous: true,
});
});
it("should disable digits", async () => {
const result = optionsToRandomAsciiRequest({
length: 3,
ambiguous: true,
uppercase: true,
lowercase: true,
number: false,
special: true,
minUppercase: 1,
minLowercase: 1,
minNumber: 1,
minSpecial: 1,
});
expect(result).toEqual({
all: 0,
uppercase: 1,
lowercase: 1,
digits: undefined,
special: 1,
ambiguous: true,
});
});
it("should disable special", async () => {
const result = optionsToRandomAsciiRequest({
length: 3,
ambiguous: true,
uppercase: true,
lowercase: true,
number: true,
special: false,
minUppercase: 1,
minLowercase: 1,
minNumber: 1,
minSpecial: 1,
});
expect(result).toEqual({
all: 0,
uppercase: 1,
lowercase: 1,
digits: 1,
special: undefined,
ambiguous: true,
});
});
it("should override length with minimums", async () => {
const result = optionsToRandomAsciiRequest({
length: 20,
ambiguous: true,
uppercase: true,
lowercase: true,
number: true,
special: true,
minUppercase: 1,
minLowercase: 2,
minNumber: 3,
minSpecial: 4,
});
expect(result).toEqual({
all: 10,
uppercase: 1,
lowercase: 2,
digits: 3,
special: 4,
ambiguous: true,
});
});
it("should default uppercase", async () => {
const result = optionsToRandomAsciiRequest({
length: 2,
ambiguous: true,
lowercase: true,
number: true,
special: true,
minUppercase: 2,
minLowercase: 0,
minNumber: 0,
minSpecial: 0,
});
expect(result).toEqual({
all: 0,
uppercase: 2,
lowercase: 0,
digits: 0,
special: 0,
ambiguous: true,
});
});
it("should default lowercase", async () => {
const result = optionsToRandomAsciiRequest({
length: 0,
ambiguous: true,
uppercase: true,
number: true,
special: true,
minUppercase: 0,
minLowercase: 2,
minNumber: 0,
minSpecial: 0,
});
expect(result).toEqual({
all: 0,
uppercase: 0,
lowercase: 2,
digits: 0,
special: 0,
ambiguous: true,
});
});
it("should default number", async () => {
const result = optionsToRandomAsciiRequest({
length: 0,
ambiguous: true,
uppercase: true,
lowercase: true,
special: true,
minUppercase: 0,
minLowercase: 0,
minNumber: 2,
minSpecial: 0,
});
expect(result).toEqual({
all: 0,
uppercase: 0,
lowercase: 0,
digits: 2,
special: 0,
ambiguous: true,
});
});
it("should default special", async () => {
const result = optionsToRandomAsciiRequest({
length: 0,
ambiguous: true,
uppercase: true,
lowercase: true,
number: true,
minUppercase: 0,
minLowercase: 0,
minNumber: 0,
minSpecial: 0,
});
expect(result).toEqual({
all: 0,
uppercase: 0,
lowercase: 0,
digits: 0,
special: undefined,
ambiguous: true,
});
});
it("should default minUppercase", async () => {
const result = optionsToRandomAsciiRequest({
length: 0,
ambiguous: true,
uppercase: true,
lowercase: true,
number: true,
special: true,
minLowercase: 0,
minNumber: 0,
minSpecial: 0,
});
expect(result).toEqual({
all: 0,
uppercase: 1,
lowercase: 0,
digits: 0,
special: 0,
ambiguous: true,
});
});
it("should default minLowercase", async () => {
const result = optionsToRandomAsciiRequest({
length: 0,
ambiguous: true,
uppercase: true,
lowercase: true,
number: true,
special: true,
minUppercase: 0,
minNumber: 0,
minSpecial: 0,
});
expect(result).toEqual({
all: 0,
uppercase: 0,
lowercase: 1,
digits: 0,
special: 0,
ambiguous: true,
});
});
it("should default minNumber", async () => {
const result = optionsToRandomAsciiRequest({
length: 0,
ambiguous: true,
uppercase: true,
lowercase: true,
number: true,
special: true,
minUppercase: 0,
minLowercase: 0,
minSpecial: 0,
});
expect(result).toEqual({
all: 0,
uppercase: 0,
lowercase: 0,
digits: 1,
special: 0,
ambiguous: true,
});
});
it("should default minSpecial", async () => {
const result = optionsToRandomAsciiRequest({
length: 0,
ambiguous: true,
uppercase: true,
lowercase: true,
number: true,
special: true,
minUppercase: 0,
minLowercase: 0,
minNumber: 0,
});
expect(result).toEqual({
all: 0,
uppercase: 0,
lowercase: 0,
digits: 0,
special: 0,
ambiguous: true,
});
});
});
describe("optionsToEffWordListRequest", () => {
it("should map options", async () => {
const result = optionsToEffWordListRequest({
numWords: 4,
capitalize: true,
includeNumber: true,
wordSeparator: "!",
});
expect(result).toEqual({
numberOfWords: 4,
capitalize: true,
number: true,
separator: "!",
});
});
it("should default numWords", async () => {
const result = optionsToEffWordListRequest({
capitalize: true,
includeNumber: true,
wordSeparator: "!",
});
expect(result).toEqual({
numberOfWords: DefaultPassphraseGenerationOptions.numWords,
capitalize: true,
number: true,
separator: "!",
});
});
it("should default capitalize", async () => {
const result = optionsToEffWordListRequest({
numWords: 4,
includeNumber: true,
wordSeparator: "!",
});
expect(result).toEqual({
numberOfWords: 4,
capitalize: DefaultPassphraseGenerationOptions.capitalize,
number: true,
separator: "!",
});
});
it("should default includeNumber", async () => {
const result = optionsToEffWordListRequest({
numWords: 4,
capitalize: true,
wordSeparator: "!",
});
expect(result).toEqual({
numberOfWords: 4,
capitalize: true,
number: DefaultPassphraseGenerationOptions.includeNumber,
separator: "!",
});
});
it("should default wordSeparator", async () => {
const result = optionsToEffWordListRequest({
numWords: 4,
capitalize: true,
includeNumber: true,
});
expect(result).toEqual({
numberOfWords: 4,
capitalize: true,
number: true,
separator: DefaultPassphraseGenerationOptions.wordSeparator,
});
});
});

View File

@@ -7,6 +7,13 @@ import {
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import {
DefaultPassphraseBoundaries,
DefaultPassphraseGenerationOptions,
DefaultPasswordGenerationOptions,
} from "./data";
import { PassphraseGenerationOptions, PasswordGenerationOptions } from "./types";
/** construct a method that outputs a copy of `defaultValue` as an observable. */
export function observe$PerUserId<Value>(
create: () => Partial<Value>,
@@ -50,3 +57,79 @@ export function sharedStateByUserId<Value>(key: UserKeyDefinition<Value>, provid
/** returns the sum of items in the list. */
export const sum = (...items: number[]) =>
(items ?? []).reduce((sum: number, current: number) => sum + (current ?? 0), 0);
/* converts password generation option sets, which are defined by
* an "enabled" and "quantity" parameter, to the password engine's
* parameters, which represent disabled options as `undefined`
* properties.
*/
export function optionsToRandomAsciiRequest(options: PasswordGenerationOptions) {
// helper for processing common option sets
function process(
// values read from the options
enabled: boolean,
quantity: number,
// value used if an option is missing
defaultEnabled: boolean,
defaultQuantity: number,
) {
const isEnabled = enabled ?? defaultEnabled;
const actualQuantity = quantity ?? defaultQuantity;
const result = isEnabled ? actualQuantity : undefined;
return result;
}
const request = {
uppercase: process(
options.uppercase,
options.minUppercase,
DefaultPasswordGenerationOptions.uppercase,
DefaultPasswordGenerationOptions.minUppercase,
),
lowercase: process(
options.lowercase,
options.minLowercase,
DefaultPasswordGenerationOptions.lowercase,
DefaultPasswordGenerationOptions.minLowercase,
),
digits: process(
options.number,
options.minNumber,
DefaultPasswordGenerationOptions.number,
DefaultPasswordGenerationOptions.minNumber,
),
special: process(
options.special,
options.minSpecial,
DefaultPasswordGenerationOptions.special,
DefaultPasswordGenerationOptions.minSpecial,
),
ambiguous: options.ambiguous ?? DefaultPasswordGenerationOptions.ambiguous,
all: 0,
};
// engine represents character sets as "include only"; you assert how many all
// characters there can be rather than a total length. This conversion has
// the character classes win, so that the result is always consistent with policy
// minimums.
const required = sum(request.uppercase, request.lowercase, request.digits, request.special);
const remaining = (options.length ?? 0) - required;
request.all = Math.max(remaining, 0);
return request;
}
/* converts passphrase generation option sets to the eff word list request
*/
export function optionsToEffWordListRequest(options: PassphraseGenerationOptions) {
const requestWords = options.numWords ?? DefaultPassphraseGenerationOptions.numWords;
const request = {
numberOfWords: Math.max(requestWords, DefaultPassphraseBoundaries.numWords.min),
capitalize: options.capitalize ?? DefaultPassphraseGenerationOptions.capitalize,
number: options.includeNumber ?? DefaultPassphraseGenerationOptions.includeNumber,
separator: options.wordSeparator ?? DefaultPassphraseGenerationOptions.wordSeparator,
};
return request;
}

View File

@@ -2,12 +2,12 @@ import { svgIcon } from "@bitwarden/components";
export const NoSendsIcon = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="125" fill="none">
<path class="tw-stroke-text-headers" stroke-width="3" d="M13.425 11.994H5.99a4.311 4.311 0 0 0-4.311 4.312v62.09a4.311 4.311 0 0 0 4.311 4.311h40.09"/>
<path class="tw-stroke-text-headers tw-fill-background" stroke-width="3" d="M66.27 75.142h-49.9a3.234 3.234 0 0 1-3.233-3.234V9.818a3.234 3.234 0 0 1 3.234-3.233h35.764a3.233 3.233 0 0 1 2.293.953l14.134 14.216c.602.605.94 1.425.94 2.28v47.874a3.233 3.233 0 0 1-3.233 3.234Z"/>
<path class="tw-stroke-info-600" stroke-width="2" d="M47.021 35.586c0-3.818-2.728-6.915-6.095-6.915-3.367 0-6.096 3.097-6.096 6.915"/>
<path class="tw-stroke-info-600" stroke-width="2" d="M47.38 35.335H34.058a3.593 3.593 0 0 0-3.593 3.592v9.817a3.593 3.593 0 0 0 3.593 3.593H47.38a3.593 3.593 0 0 0 3.593-3.593v-9.817a3.593 3.593 0 0 0-3.593-3.592Z"/>
<path class="tw-stroke-text-headers" stroke-linecap="round" stroke-width="2" d="M40.72 44.34v2.618"/>
<path class="tw-stroke-text-headers" stroke-linecap="round" stroke-width="4" d="M40.72 42.7v-.373"/>
<path class="tw-stroke-text-headers" fill="rgb(var(--color-background-alt))" stroke-width="3" d="M89.326 64.022s1.673-.73 2.252.572c.512 1.138-.822 2.033-.822 2.033L56.757 88.133a3.886 3.886 0 0 0-1.583 2.188l-4.732 16.705a2.665 2.665 0 0 0 .059 1.611 2.596 2.596 0 0 0 1.891 1.663c.331.07.673.071 1.004.004.402-.077.78-.25 1.102-.503l10.11-7.88a3.138 3.138 0 0 1 1.92-.663 3.08 3.08 0 0 1 1.905.662l13.926 10.948a2.556 2.556 0 0 0 3.162 0 2.71 2.71 0 0 0 .727-.879l31.777-61.762c.231-.448.33-.952.284-1.455a2.606 2.606 0 0 0-1.721-2.226 2.499 2.499 0 0 0-1.457-.06l-81.18 20.418c-.465.12-.888.364-1.223.708a2.672 2.672 0 0 0-.632 2.676c.146.46.417.865.78 1.174L46.2 83.1a4.463 4.463 0 0 0 2.565 1.572 4.489 4.489 0 0 0 2.984-.413l37.578-20.237Z"/>
<path class="tw-stroke-art-primary" stroke-width="3" d="M13.425 11.994H5.99a4.311 4.311 0 0 0-4.311 4.312v62.09a4.311 4.311 0 0 0 4.311 4.311h40.09"/>
<path class="tw-stroke-art-primary tw-fill-background" stroke-width="3" d="M66.27 75.142h-49.9a3.234 3.234 0 0 1-3.233-3.234V9.818a3.234 3.234 0 0 1 3.234-3.233h35.764a3.233 3.233 0 0 1 2.293.953l14.134 14.216c.602.605.94 1.425.94 2.28v47.874a3.233 3.233 0 0 1-3.233 3.234Z"/>
<path class="tw-stroke-art-accent" stroke-width="2" d="M47.021 35.586c0-3.818-2.728-6.915-6.095-6.915-3.367 0-6.096 3.097-6.096 6.915"/>
<path class="tw-stroke-art-accent" stroke-width="2" d="M47.38 35.335H34.058a3.593 3.593 0 0 0-3.593 3.592v9.817a3.593 3.593 0 0 0 3.593 3.593H47.38a3.593 3.593 0 0 0 3.593-3.593v-9.817a3.593 3.593 0 0 0-3.593-3.592Z"/>
<path class="tw-stroke-art-primary" stroke-linecap="round" stroke-width="2" d="M40.72 44.34v2.618"/>
<path class="tw-stroke-art-primary" stroke-linecap="round" stroke-width="4" d="M40.72 42.7v-.373"/>
<path class="tw-stroke-art-primary" fill="rgb(var(--color-background-alt))" stroke-width="3" d="M89.326 64.022s1.673-.73 2.252.572c.512 1.138-.822 2.033-.822 2.033L56.757 88.133a3.886 3.886 0 0 0-1.583 2.188l-4.732 16.705a2.665 2.665 0 0 0 .059 1.611 2.596 2.596 0 0 0 1.891 1.663c.331.07.673.071 1.004.004.402-.077.78-.25 1.102-.503l10.11-7.88a3.138 3.138 0 0 1 1.92-.663 3.08 3.08 0 0 1 1.905.662l13.926 10.948a2.556 2.556 0 0 0 3.162 0 2.71 2.71 0 0 0 .727-.879l31.777-61.762c.231-.448.33-.952.284-1.455a2.606 2.606 0 0 0-1.721-2.226 2.499 2.499 0 0 0-1.457-.06l-81.18 20.418c-.465.12-.888.364-1.223.708a2.672 2.672 0 0 0-.632 2.676c.146.46.417.865.78 1.174L46.2 83.1a4.463 4.463 0 0 0 2.565 1.572 4.489 4.489 0 0 0 2.984-.413l37.578-20.237Z"/>
</svg>
`;

View File

@@ -2,12 +2,12 @@ import { svgIcon } from "@bitwarden/components";
export const SendCreatedIcon = svgIcon`
<svg width="96" height="95" viewBox="0 0 96 95" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="tw-stroke-text-headers" d="M89.4998 48.3919C89.4998 70.5749 70.9198 88.5573 47.9998 88.5573C46.0374 88.5573 44.1068 88.4257 42.217 88.1707M6.49976 48.3919C6.49976 26.2092 25.08 8.22656 47.9998 8.22656C51.8283 8.22656 55.5353 8.72824 59.0553 9.66744" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5.47085 67.8617C2.60335 61.9801 1 55.4075 1 48.4729C1 23.3503 22.0426 2.98438 48 2.98438C52.3355 2.98438 56.534 3.55257 60.5205 4.61618M92.211 32.9993C94.016 37.8295 95 43.0399 95 48.4729C95 73.5956 73.9575 93.9614 48 93.9614C45.7775 93.9614 43.5911 93.8119 41.4508 93.5235" />
<path class="tw-fill-text-headers" d="M20.8242 84.8672C20.8242 84.3149 20.3765 83.8672 19.8242 83.8672C19.2719 83.8672 18.8242 84.3149 18.8242 84.8672H20.8242ZM18.8242 87.2442C18.8242 87.7965 19.2719 88.2442 19.8242 88.2442C20.3765 88.2442 20.8242 87.7965 20.8242 87.2442H18.8242ZM18.8242 84.1908C18.8242 84.7431 19.2719 85.1908 19.8242 85.1908C20.3765 85.1908 20.8242 84.7431 20.8242 84.1908H18.8242ZM20.8242 83.8516C20.8242 83.2993 20.3765 82.8516 19.8242 82.8516C19.2719 82.8516 18.8242 83.2993 18.8242 83.8516H20.8242ZM26.7882 76.042C26.7882 72.0015 23.7427 68.5898 19.8238 68.5898V70.5898C22.4931 70.5898 24.7882 72.9552 24.7882 76.042H26.7882ZM19.8238 68.5898C15.9049 68.5898 12.8594 72.0015 12.8594 76.042H14.8594C14.8594 72.9552 17.1545 70.5898 19.8238 70.5898V68.5898ZM11.5 77.0391H28.1475V75.0391H11.5V77.0391ZM28.1475 77.0391C28.4548 77.0391 28.6475 77.2719 28.6475 77.4908H30.6475C30.6475 76.1062 29.4972 75.0391 28.1475 75.0391V77.0391ZM28.6475 77.4908V90.5469H30.6475V77.4908H28.6475ZM28.6475 90.5469C28.6475 90.7658 28.4548 90.9987 28.1475 90.9987V92.9987C29.4972 92.9987 30.6475 91.9315 30.6475 90.5469H28.6475ZM28.1475 90.9987H11.5V92.9987H28.1475V90.9987ZM11.5 90.9987C11.1928 90.9987 11 90.7658 11 90.5469H9C9 91.9315 10.1504 92.9987 11.5 92.9987V90.9987ZM11 90.5469V77.4908H9V90.5469H11ZM11 77.4908C11 77.2719 11.1928 77.0391 11.5 77.0391V75.0391C10.1504 75.0391 9 76.1062 9 77.4908H11ZM18.8242 84.8672V87.2442H20.8242V84.8672H18.8242ZM20.8242 84.1908V83.8516H18.8242V84.1908H20.8242Z"/>
<path class="tw-stroke-text-headers" d="M36 64L37 63" stroke-width="2" stroke-linecap="round"/>
<path class="tw-stroke-text-headers" d="M29.9998 69.9995L30.9998 68.9995" stroke-width="2" stroke-linecap="round"/>
<path class="tw-stroke-text-headers" d="M40.0174 51.8491L33 48.7544L61.5083 33L56.6108 58.4604L48.4968 55.4359L44.2571 60.2185V53.8888L55.5873 40.8772" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M89.4998 48.3919C89.4998 70.5749 70.9198 88.5573 47.9998 88.5573C46.0374 88.5573 44.1068 88.4257 42.217 88.1707M6.49976 48.3919C6.49976 26.2092 25.08 8.22656 47.9998 8.22656C51.8283 8.22656 55.5353 8.72824 59.0553 9.66744" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5.47085 67.8617C2.60335 61.9801 1 55.4075 1 48.4729C1 23.3503 22.0426 2.98438 48 2.98438C52.3355 2.98438 56.534 3.55257 60.5205 4.61618M92.211 32.9993C94.016 37.8295 95 43.0399 95 48.4729C95 73.5956 73.9575 93.9614 48 93.9614C45.7775 93.9614 43.5911 93.8119 41.4508 93.5235" />
<path class="tw-fill-art-primary" d="M20.8242 84.8672C20.8242 84.3149 20.3765 83.8672 19.8242 83.8672C19.2719 83.8672 18.8242 84.3149 18.8242 84.8672H20.8242ZM18.8242 87.2442C18.8242 87.7965 19.2719 88.2442 19.8242 88.2442C20.3765 88.2442 20.8242 87.7965 20.8242 87.2442H18.8242ZM18.8242 84.1908C18.8242 84.7431 19.2719 85.1908 19.8242 85.1908C20.3765 85.1908 20.8242 84.7431 20.8242 84.1908H18.8242ZM20.8242 83.8516C20.8242 83.2993 20.3765 82.8516 19.8242 82.8516C19.2719 82.8516 18.8242 83.2993 18.8242 83.8516H20.8242ZM26.7882 76.042C26.7882 72.0015 23.7427 68.5898 19.8238 68.5898V70.5898C22.4931 70.5898 24.7882 72.9552 24.7882 76.042H26.7882ZM19.8238 68.5898C15.9049 68.5898 12.8594 72.0015 12.8594 76.042H14.8594C14.8594 72.9552 17.1545 70.5898 19.8238 70.5898V68.5898ZM11.5 77.0391H28.1475V75.0391H11.5V77.0391ZM28.1475 77.0391C28.4548 77.0391 28.6475 77.2719 28.6475 77.4908H30.6475C30.6475 76.1062 29.4972 75.0391 28.1475 75.0391V77.0391ZM28.6475 77.4908V90.5469H30.6475V77.4908H28.6475ZM28.6475 90.5469C28.6475 90.7658 28.4548 90.9987 28.1475 90.9987V92.9987C29.4972 92.9987 30.6475 91.9315 30.6475 90.5469H28.6475ZM28.1475 90.9987H11.5V92.9987H28.1475V90.9987ZM11.5 90.9987C11.1928 90.9987 11 90.7658 11 90.5469H9C9 91.9315 10.1504 92.9987 11.5 92.9987V90.9987ZM11 90.5469V77.4908H9V90.5469H11ZM11 77.4908C11 77.2719 11.1928 77.0391 11.5 77.0391V75.0391C10.1504 75.0391 9 76.1062 9 77.4908H11ZM18.8242 84.8672V87.2442H20.8242V84.8672H18.8242ZM20.8242 84.1908V83.8516H18.8242V84.1908H20.8242Z"/>
<path class="tw-stroke-art-primary" d="M36 64L37 63" stroke-width="2" stroke-linecap="round"/>
<path class="tw-stroke-art-primary" d="M29.9998 69.9995L30.9998 68.9995" stroke-width="2" stroke-linecap="round"/>
<path class="tw-stroke-art-primary" d="M40.0174 51.8491L33 48.7544L61.5083 33L56.6108 58.4604L48.4968 55.4359L44.2571 60.2185V53.8888L55.5873 40.8772" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<mask id="path-7-inside-1_752_3985" fill="white">
<path d="M94 17.5C94 26.6127 86.6127 34 77.5 34C68.3873 34 61 26.6127 61 17.5C61 8.3873 68.3873 1 77.5 1C86.6127 1 94 8.3873 94 17.5Z"/>
</mask>

View File

@@ -10,6 +10,7 @@ import {
import { BehaviorSubject } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
@@ -168,6 +169,12 @@ export default {
autofillOnPageLoadDefault$: new BehaviorSubject(true),
},
},
{
provide: EventCollectionService,
useValue: {
collect: () => Promise.resolve(),
},
},
],
}),
componentWrapperDecorator(

View File

@@ -11,12 +11,12 @@ import {
} from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import {
Observable,
Subject,
combineLatest,
firstValueFrom,
map,
Observable,
shareReplay,
Subject,
switchMap,
takeUntil,
tap,
@@ -27,8 +27,6 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -170,7 +168,6 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
constructor(
private cipherService: CipherService,
private i18nService: I18nService,
private configService: ConfigService,
private organizationService: OrganizationService,
private collectionService: CollectionService,
private formBuilder: FormBuilder,
@@ -179,10 +176,6 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
) {}
async ngOnInit() {
const restrictProviderAccess = await this.configService.getFeatureFlag(
FeatureFlag.RestrictProviderAccess,
);
this.activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
@@ -193,7 +186,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
this.showOrgSelector = true;
}
await this.initializeItems(this.selectedOrgId, restrictProviderAccess);
await this.initializeItems(this.selectedOrgId);
if (this.selectedOrgId && this.selectedOrgId !== MY_VAULT_ID) {
await this.handleOrganizationCiphers();
@@ -339,7 +332,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
}
}
private async initializeItems(organizationId: OrganizationId, restrictProviderAccess: boolean) {
private async initializeItems(organizationId: OrganizationId) {
this.totalItemCount = this.params.ciphers.length;
// If organizationId is not present or organizationId is MyVault, then all ciphers are considered personal items
@@ -354,7 +347,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
const org = await this.organizationService.get(organizationId);
this.orgName = org.name;
this.editableItems = org.canEditAllCiphers(restrictProviderAccess)
this.editableItems = org.canEditAllCiphers
? this.params.ciphers
: this.params.ciphers.filter((c) => c.edit);

View File

@@ -3,22 +3,22 @@ import { svgIcon } from "@bitwarden/components";
export const DeactivatedOrg = svgIcon`
<svg width="138" height="118" viewBox="0 0 138 118" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2929_17380)">
<path class="tw-stroke-text-headers" d="M80.0852 15.889V11.7504C80.0852 9.75243 78.6181 8.18262 76.7509 8.18262H53.1445C51.2773 8.18262 49.8102 9.75243 49.8102 11.7504V16.0317" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M73.3568 7.06126V3.568C73.3568 1.75668 71.8648 0.333496 69.9658 0.333496H59.9285C58.0295 0.333496 56.5374 1.75668 56.5374 3.568V7.06126" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M41.9611 29.8517V20.5736C41.9611 18.658 43.4441 17.1528 45.3315 17.1528H84.5637C86.4511 17.1528 87.9341 18.658 87.9341 20.5736V83.2728" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M12.8074 103.493V32.9262C12.8074 31.0004 14.3311 29.4873 16.2703 29.4873H56.4389C58.3781 29.4873 59.9018 31.0004 59.9018 32.9262V103.493" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M36.3545 39.5791V94.5225" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M47.5677 39.5791V94.5225" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M78.9634 26.1235V37.3365" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M78.9634 45.1851V56.398" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M78.9634 64.2476V75.4605" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M78.9634 83.3091V94.522" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M69.9932 26.1235V37.3365" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M69.9932 45.1851V56.398" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M69.9932 64.2476V75.4605" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M69.9932 83.3091V94.522" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M24.0202 39.5791V94.5225" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M0.473145 104.614H75.3408" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M80.0852 15.889V11.7504C80.0852 9.75243 78.6181 8.18262 76.7509 8.18262H53.1445C51.2773 8.18262 49.8102 9.75243 49.8102 11.7504V16.0317" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M73.3568 7.06126V3.568C73.3568 1.75668 71.8648 0.333496 69.9658 0.333496H59.9285C58.0295 0.333496 56.5374 1.75668 56.5374 3.568V7.06126" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M41.9611 29.8517V20.5736C41.9611 18.658 43.4441 17.1528 45.3315 17.1528H84.5637C86.4511 17.1528 87.9341 18.658 87.9341 20.5736V83.2728" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M12.8074 103.493V32.9262C12.8074 31.0004 14.3311 29.4873 16.2703 29.4873H56.4389C58.3781 29.4873 59.9018 31.0004 59.9018 32.9262V103.493" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M36.3545 39.5791V94.5225" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M47.5677 39.5791V94.5225" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M78.9634 26.1235V37.3365" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M78.9634 45.1851V56.398" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M78.9634 64.2476V75.4605" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M78.9634 83.3091V94.522" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M69.9932 26.1235V37.3365" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M69.9932 45.1851V56.398" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M69.9932 64.2476V75.4605" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M69.9932 83.3091V94.522" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M24.0202 39.5791V94.5225" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M0.473145 104.614H75.3408" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-fill-danger-600" fill-rule="evenodd" clip-rule="evenodd" d="M121.425 111.921L99.1265 73.2989C98.3006 71.8685 96.236 71.8685 95.4101 73.2989L73.1119 111.921C72.286 113.351 73.3183 115.139 74.97 115.139H119.567C121.218 115.139 122.251 113.351 121.425 111.921ZM101.604 71.8685C99.6771 68.5308 94.8595 68.5308 92.9325 71.8685L70.6343 110.49C68.7073 113.828 71.116 118 74.97 118H119.567C123.421 118 125.829 113.828 123.902 110.49L101.604 71.8685Z"/>
<path class="tw-fill-danger-600" d="M98.2704 84.3848C98.8321 84.3848 99.2836 84.8473 99.2701 85.4088L98.8811 101.584C98.8681 102.127 98.4243 102.56 97.8814 102.56H96.6544C96.1118 102.56 95.6682 102.127 95.6547 101.585L95.254 85.4095C95.24 84.8477 95.6917 84.3848 96.2537 84.3848H98.2704Z" />
<circle class="tw-fill-danger-600" cx="97.2682" cy="106.556" r="2.14565" />

View File

@@ -3,16 +3,16 @@ 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 class="tw-fill-art-accent" 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-art-accent" 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-art-accent" 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-art-primary" 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-art-accent" 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-art-accent" 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-art-accent" 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-art-accent" 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-art-accent" 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"/>
<path class="tw-fill-art-primary" 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

@@ -2,18 +2,18 @@ import { svgIcon } from "@bitwarden/components";
export const NoFolders = svgIcon`
<svg width="147" height="91" viewBox="0 0 147 91" fill="none" xmlns="http://www.w3.org/2000/svg">
<path class="tw-stroke-info-600" d="M64.8263 1.09473V9.90589" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-info-600" d="M42.1936 6.09082L46.6564 13.7215" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-info-600" d="M53.8507 2.4209L55.4006 11.0982" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-info-600" d="M76.1821 2.50293L73.8719 11.0139" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-info-600" d="M87.4594 6.09082L82.9966 13.7215" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-text-headers" d="M100.358 89.3406H28.3419C26.1377 89.3406 24.3422 87.8759 24.3422 86.0715V24.5211C24.3422 22.7167 26.127 21.252 28.3419 21.252H61.6082C63.8124 21.252 65.608 22.7167 65.608 24.5211V28.5013C65.608 30.5073 67.3928 32.1313 69.6077 32.1313H82.105" stroke-width="2.5" stroke-miterlimit="10"/>
<path class="tw-stroke-text-headers" d="M82.344 41.7686H40.1042C37.9646 41.7686 36.1475 42.7663 35.8465 44.1036L26.3203 86.2411C25.9547 87.8757 27.9653 89.3404 30.5781 89.3404H107.906C110.045 89.3404 111.862 88.3427 112.163 87.0053L116.904 62.5844" stroke-width="2.5" stroke-miterlimit="10"/>
<path class="tw-stroke-info-600" d="M28.1209 36.6897V27.0451C28.1209 25.9523 29.0068 25.0664 30.0996 25.0664H40.7775" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-text-headers" d="M105.433 64.0751C119.875 65.725 132.919 55.3558 134.569 40.9147C136.219 26.4737 125.849 13.4294 111.408 11.7794C96.9673 10.1295 83.923 20.4988 82.2731 34.9398C80.6232 49.3809 90.9924 62.4252 105.433 64.0751Z" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-info-600" d="M105.834 60.5706C118.253 61.9895 129.484 52.9549 130.92 40.3912M85.9456 35.2528C87.381 22.6891 98.6125 13.6544 111.032 15.0734" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M122.014 60.6246L125.553 65.0773L138.676 81.5851C139.806 83.0073 141.876 83.2437 143.298 82.1132L143.558 81.906C144.98 80.7755 145.217 78.7062 144.086 77.2841L130.964 60.7762L127.424 56.3235" stroke-width="2.19854" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-text-headers" d="M101.309 31.3349C101.309 31.3349 101.415 28.8033 103.894 26.6553C105.382 25.3511 107.153 25.0059 108.747 24.9675C110.199 24.9291 111.51 25.1976 112.254 25.6196C113.6 26.31 116.186 27.9594 116.186 31.5267C116.186 35.2858 113.919 36.9735 111.368 38.8531C108.818 40.7326 109.185 43.1796 109.185 45.2509" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-fill-text-headers" d="M109.191 51.7764C110.02 51.7764 110.691 51.1049 110.691 50.2764C110.691 49.448 110.02 48.7764 109.191 48.7764C108.363 48.7764 107.691 49.448 107.691 50.2764C107.691 51.1049 108.363 51.7764 109.191 51.7764Z" />
<path class="tw-stroke-art-accent" d="M64.8263 1.09473V9.90589" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-art-accent" d="M42.1936 6.09082L46.6564 13.7215" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-art-accent" d="M53.8507 2.4209L55.4006 11.0982" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-art-accent" d="M76.1821 2.50293L73.8719 11.0139" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-art-accent" d="M87.4594 6.09082L82.9966 13.7215" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-art-primary" d="M100.358 89.3406H28.3419C26.1377 89.3406 24.3422 87.8759 24.3422 86.0715V24.5211C24.3422 22.7167 26.127 21.252 28.3419 21.252H61.6082C63.8124 21.252 65.608 22.7167 65.608 24.5211V28.5013C65.608 30.5073 67.3928 32.1313 69.6077 32.1313H82.105" stroke-width="2.5" stroke-miterlimit="10"/>
<path class="tw-stroke-art-primary" d="M82.344 41.7686H40.1042C37.9646 41.7686 36.1475 42.7663 35.8465 44.1036L26.3203 86.2411C25.9547 87.8757 27.9653 89.3404 30.5781 89.3404H107.906C110.045 89.3404 111.862 88.3427 112.163 87.0053L116.904 62.5844" stroke-width="2.5" stroke-miterlimit="10"/>
<path class="tw-stroke-art-accent" d="M28.1209 36.6897V27.0451C28.1209 25.9523 29.0068 25.0664 30.0996 25.0664H40.7775" stroke-width="1.25" stroke-linecap="round"/>
<path class="tw-stroke-art-primary" d="M105.433 64.0751C119.875 65.725 132.919 55.3558 134.569 40.9147C136.219 26.4737 125.849 13.4294 111.408 11.7794C96.9673 10.1295 83.923 20.4988 82.2731 34.9398C80.6232 49.3809 90.9924 62.4252 105.433 64.0751Z" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-accent" d="M105.834 60.5706C118.253 61.9895 129.484 52.9549 130.92 40.3912M85.9456 35.2528C87.381 22.6891 98.6125 13.6544 111.032 15.0734" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M122.014 60.6246L125.553 65.0773L138.676 81.5851C139.806 83.0073 141.876 83.2437 143.298 82.1132L143.558 81.906C144.98 80.7755 145.217 78.7062 144.086 77.2841L130.964 60.7762L127.424 56.3235" stroke-width="2.19854" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-stroke-art-primary" d="M101.309 31.3349C101.309 31.3349 101.415 28.8033 103.894 26.6553C105.382 25.3511 107.153 25.0059 108.747 24.9675C110.199 24.9291 111.51 25.1976 112.254 25.6196C113.6 26.31 116.186 27.9594 116.186 31.5267C116.186 35.2858 113.919 36.9735 111.368 38.8531C108.818 40.7326 109.185 43.1796 109.185 45.2509" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path class="tw-fill-art-primary" d="M109.191 51.7764C110.02 51.7764 110.691 51.1049 110.691 50.2764C110.691 49.448 110.02 48.7764 109.191 48.7764C108.363 48.7764 107.691 49.448 107.691 50.2764C107.691 51.1049 108.363 51.7764 109.191 51.7764Z" />
</svg>
`;

View File

@@ -3,15 +3,15 @@ import { svgIcon } from "@bitwarden/components";
export const Vault = svgIcon`
<svg fill="none" width="100" height="90" viewBox="0 0 100 90" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="m73.446 81.044h17.001v3.4685c0 2.7615-2.2385 5-5 5h-7.0011c-2.7615 0-5-2.2385-5-5v-3.4685zm2 2v1.4685c0 1.6569 1.3431 3 3 3h7.0011c1.6569 0 3-1.3431 3-3v-1.4685h-13.001z" clip-rule="evenodd" class="tw-fill-info-600" fill-rule="evenodd"/>
<path d="m10.108 81.044h17.001v3.4685c0 2.7615-2.2385 5-5 5h-7.0011c-2.7614 0-5-2.2385-5-5v-3.4685zm2 2v1.4685c0 1.6569 1.3431 3 3 3h7.0011c1.6569 0 3-1.3431 3-3v-1.4685h-13.001z" clip-rule="evenodd" class="tw-fill-info-600" fill-rule="evenodd"/>
<path d="m4.2281 2.4304c-1.1655 0-2.1208 0.95235-2.1208 2.1402v74.318c0 1.1878 0.95534 2.1402 2.1208 2.1402h91.544c1.1655 0 2.1208-0.9524 2.1208-2.1402v-74.318c0-1.1878-0.9553-2.1402-2.1208-2.1402h-91.544zm-4.1208 2.1402c0-2.2807 1.8391-4.1402 4.1208-4.1402h91.544c2.2817 0 4.1208 1.8595 4.1208 4.1402v74.318c0 2.2807-1.8391 4.1402-4.1208 4.1402h-91.544c-2.2817 0-4.1208-1.8595-4.1208-4.1402v-74.318z" clip-rule="evenodd" class="tw-fill-text-headers" fill-rule="evenodd"/>
<path d="m89.258 21.816c-0.7304 0-1.3307 0.5963-1.3307 1.3421v9.3686c0 0.7459 0.6003 1.3422 1.3307 1.3422 0.7303 0 1.3307-0.5963 1.3307-1.3422v-9.3686c0-0.7458-0.6004-1.3421-1.3307-1.3421zm-3.3307 1.3421c0-1.8412 1.4866-3.3421 3.3307-3.3421s3.3307 1.5009 3.3307 3.3421v9.3686c0 1.8412-1.4866 3.3422-3.3307 3.3422s-3.3307-1.501-3.3307-3.3422v-9.3686z" clip-rule="evenodd" class="tw-fill-info-600" fill-rule="evenodd"/>
<path d="m89.258 45.237c-0.7304 0-1.3307 0.5962-1.3307 1.3421v9.3686c0 0.7459 0.6003 1.3422 1.3307 1.3422 0.7303 0 1.3307-0.5963 1.3307-1.3422v-9.3686c0-0.7459-0.6004-1.3421-1.3307-1.3421zm-3.3307 1.3421c0-1.8412 1.4866-3.3421 3.3307-3.3421s3.3307 1.5009 3.3307 3.3421v9.3686c0 1.8412-1.4866 3.3422-3.3307 3.3422s-3.3307-1.501-3.3307-3.3422v-9.3686z" clip-rule="evenodd" class="tw-fill-info-600" fill-rule="evenodd"/>
<path d="m33.443 25.468c0-0.5523 0.4477-1 1-1 1.4163 0 2.6668 1.0953 2.6668 2.5705v21.595c0 1.4752-1.2505 2.5705-2.6668 2.5705-0.5523 0-1-0.4477-1-1s0.4477-1 1-1c0.4255 0 0.6668-0.3103 0.6668-0.5705v-21.595c0-0.2602-0.2413-0.5705-0.6668-0.5705-0.5523 0-1-0.4477-1-1z" clip-rule="evenodd" class="tw-fill-info-600" fill-rule="evenodd"/>
<path d="m60.556 48.551c-3.2028 0-5.7978-3.1022-5.7978-6.9179 0-3.8156 2.595-6.9114 5.7978-6.9114 3.2029 0 5.7913 3.1022 5.7913 6.9114 0 3.8093-2.5949 6.9179-5.7913 6.9179zm0-14.791c-3.6408 0-6.6018 3.529-6.6018 7.8733 0 4.3444 2.961 7.8798 6.6018 7.8798s6.5953-3.529 6.5953-7.8798c0-4.3507-2.961-7.8733-6.5953-7.8733z" class="tw-fill-info-600"/>
<path d="m60.556 26.027c-0.4379 0-0.804 0.4267-0.804 0.9555l-0.0201 3.257c-2.0247 0.2075-3.8681 1.1748-5.3381 2.6521l-1.9561-2.2909c-0.156-0.1901-0.3638-0.2856-0.5654-0.2866h0.0033-0.0065 0.0032c-0.2015 1e-3 -0.4028 0.0965-0.5588 0.2866-0.3138 0.3695-0.3138 0.9746 0 1.3441l1.9348 2.3123 0.0034 0.0042c-1.2532 1.7574-2.0625 3.9789-2.2323 6.4166h0.7647c0.0488 0 0.0966 0.0053 0.143 0.0154-0.0465-0.01-0.0942-0.0152-0.143-0.0152h-3.497c-0.438 0-0.804 0.4268-0.804 0.9491 0 0.5224 0.366 0.9555 0.804 0.9555h2.7323c0.1698 2.4381 0.986 4.66 2.2331 6.4175l-0.0028 0.0034-1.9297 2.3187c-0.3138 0.3694-0.3138 0.9746 0 1.344 0.1568 0.1848 0.3595 0.2803 0.5621 0.2803s0.4118-0.0955 0.5687-0.2803l1.9282-2.3123 1e-4 -1e-4c1.4757 1.4954 3.3361 2.4695 5.3729 2.6684v3.2622c0 0.5287 0.3661 0.9555 0.804 0.9555 0.438 0 0.7975-0.4268 0.7975-0.9555l0.0212-3.263c2.0293-0.2066 3.8833-1.1701 5.3555-2.6581l0.0028 0.0033 1.9282 2.306c0.1569 0.1847 0.3661 0.2803 0.5687 0.2803s0.4118-0.0956 0.5687-0.2803c0.3137-0.3695 0.3137-0.9746 0-1.3441l-1.9269-2.3334c1.2466-1.7626 2.0628-3.9762 2.2337-6.4125h2.7195c0.438 0 0.804-0.4268 0.804-0.9555s-0.366-0.9491-0.804-0.9491l-2.7198-0.0166c-0.1709-2.4276-0.9825-4.634-2.2222-6.3932l1.9157-2.3235c0.3137-0.3695 0.3137-0.9746 0-1.3441-0.1569-0.1911-0.3661-0.2866-0.5687-0.2866s-0.4118 0.0955-0.5687 0.2866l-1.9222 2.2988c-1.4756-1.4884-3.3591-2.454-5.3855-2.665v-3.252c0-0.5288-0.353-0.9555-0.7975-0.9555zm6.72 8.9311c0.0201-0.02 0.0396-0.0413 0.0584-0.0642l0.0144-0.0173-0.021 0.0239c-0.0167 0.0203-0.034 0.0395-0.0518 0.0576zm1.2545 6.6691c-0.0028-0.0609-0.0032-0.1186-0.0013-0.1732-0.0775-5.1648-3.6205-9.3364-7.9594-9.3438l-0.0138 1e-4 -0.0114-1e-4c-4.3862 0.0089-7.9565 4.2734-7.9565 9.5168 0 5.2239 3.5479 9.4824 7.9117 9.5229 0.0178-9e-4 0.036-0.0014 0.0546-0.0014 0.0194 0 0.0385 5e-4 0.0572 0.0015 4.3591-0.0317 7.9116-4.2849 7.9189-9.5068v-0.016zm-13.411 7.6696c0.0205-0.0858 0.0307-0.174 0.0307-0.2615 0-0.242-0.0784-0.4904-0.2353-0.6752-0.1503-0.1911-0.3595-0.2803-0.5621-0.2803l-0.0114 1e-4h0.0113c0.2026 0 0.4118 0.0892 0.5621 0.2803 0.1569 0.1847 0.2353 0.4332 0.2353 0.6752 0 0.0874-0.0102 0.1757-0.0306 0.2614zm-2.5382-7.6696c0-0.0236-7e-4 -0.0471-0.0021-0.0702 0.0014 0.0231 0.0022 0.0465 0.0022 0.07 0 0.0175-4e-4 0.0349-0.0012 0.0521 7e-4 -0.0172 0.0011-0.0345 0.0011-0.0519z" clip-rule="evenodd" class="tw-fill-text-headers" fill-rule="evenodd"/>
<path d="m25.442 10.125c0-1.2133 1.0146-2.1704 2.2154-2.1008l58.262 3.4199c1.1054 0.0669 1.9723 0.9842 1.9723 2.1009v7.3296h2v-7.3296c0-2.1736-1.6899-3.9673-3.853-4.0974l-58.264-3.42c-2.1001-0.12216-3.8976 1.356-4.264 3.347h-8.7578c-2.2641 0-4.0891 1.845-4.0891 4.1081v55.945c0 2.2631 1.825 4.1081 4.0891 4.1081h8.7036c0.1798 2.1936 2.0771 3.8865 4.3187 3.7561l58.264-3.4201c2.1631-0.1301 3.853-1.9237 3.853-4.0973v-11.184h-2v11.184c0 1.117-0.8674 2.0344-1.9731 2.1009l-58.261 3.4199c-1.2008 0.0696-2.2155-0.8875-2.2155-2.1009v-63.07zm-2 61.411v-60.162h-8.6897c-1.148 0-2.0891 0.9381-2.0891 2.1081v55.945c0 1.1701 0.9411 2.1081 2.0891 2.1081h8.6897zm64.449-36.67v9.4289h2v-9.4289h-2z" clip-rule="evenodd" class="tw-fill-text-headers" fill-rule="evenodd"/>
<path d="m73.446 81.044h17.001v3.4685c0 2.7615-2.2385 5-5 5h-7.0011c-2.7615 0-5-2.2385-5-5v-3.4685zm2 2v1.4685c0 1.6569 1.3431 3 3 3h7.0011c1.6569 0 3-1.3431 3-3v-1.4685h-13.001z" clip-rule="evenodd" class="tw-fill-art-accent" fill-rule="evenodd"/>
<path d="m10.108 81.044h17.001v3.4685c0 2.7615-2.2385 5-5 5h-7.0011c-2.7614 0-5-2.2385-5-5v-3.4685zm2 2v1.4685c0 1.6569 1.3431 3 3 3h7.0011c1.6569 0 3-1.3431 3-3v-1.4685h-13.001z" clip-rule="evenodd" class="tw-fill-art-accent" fill-rule="evenodd"/>
<path d="m4.2281 2.4304c-1.1655 0-2.1208 0.95235-2.1208 2.1402v74.318c0 1.1878 0.95534 2.1402 2.1208 2.1402h91.544c1.1655 0 2.1208-0.9524 2.1208-2.1402v-74.318c0-1.1878-0.9553-2.1402-2.1208-2.1402h-91.544zm-4.1208 2.1402c0-2.2807 1.8391-4.1402 4.1208-4.1402h91.544c2.2817 0 4.1208 1.8595 4.1208 4.1402v74.318c0 2.2807-1.8391 4.1402-4.1208 4.1402h-91.544c-2.2817 0-4.1208-1.8595-4.1208-4.1402v-74.318z" clip-rule="evenodd" class="tw-fill-art-primary" fill-rule="evenodd"/>
<path d="m89.258 21.816c-0.7304 0-1.3307 0.5963-1.3307 1.3421v9.3686c0 0.7459 0.6003 1.3422 1.3307 1.3422 0.7303 0 1.3307-0.5963 1.3307-1.3422v-9.3686c0-0.7458-0.6004-1.3421-1.3307-1.3421zm-3.3307 1.3421c0-1.8412 1.4866-3.3421 3.3307-3.3421s3.3307 1.5009 3.3307 3.3421v9.3686c0 1.8412-1.4866 3.3422-3.3307 3.3422s-3.3307-1.501-3.3307-3.3422v-9.3686z" clip-rule="evenodd" class="tw-fill-art-accent" fill-rule="evenodd"/>
<path d="m89.258 45.237c-0.7304 0-1.3307 0.5962-1.3307 1.3421v9.3686c0 0.7459 0.6003 1.3422 1.3307 1.3422 0.7303 0 1.3307-0.5963 1.3307-1.3422v-9.3686c0-0.7459-0.6004-1.3421-1.3307-1.3421zm-3.3307 1.3421c0-1.8412 1.4866-3.3421 3.3307-3.3421s3.3307 1.5009 3.3307 3.3421v9.3686c0 1.8412-1.4866 3.3422-3.3307 3.3422s-3.3307-1.501-3.3307-3.3422v-9.3686z" clip-rule="evenodd" class="tw-fill-art-accent" fill-rule="evenodd"/>
<path d="m33.443 25.468c0-0.5523 0.4477-1 1-1 1.4163 0 2.6668 1.0953 2.6668 2.5705v21.595c0 1.4752-1.2505 2.5705-2.6668 2.5705-0.5523 0-1-0.4477-1-1s0.4477-1 1-1c0.4255 0 0.6668-0.3103 0.6668-0.5705v-21.595c0-0.2602-0.2413-0.5705-0.6668-0.5705-0.5523 0-1-0.4477-1-1z" clip-rule="evenodd" class="tw-fill-art-accent" fill-rule="evenodd"/>
<path d="m60.556 48.551c-3.2028 0-5.7978-3.1022-5.7978-6.9179 0-3.8156 2.595-6.9114 5.7978-6.9114 3.2029 0 5.7913 3.1022 5.7913 6.9114 0 3.8093-2.5949 6.9179-5.7913 6.9179zm0-14.791c-3.6408 0-6.6018 3.529-6.6018 7.8733 0 4.3444 2.961 7.8798 6.6018 7.8798s6.5953-3.529 6.5953-7.8798c0-4.3507-2.961-7.8733-6.5953-7.8733z" class="tw-fill-art-accent"/>
<path d="m60.556 26.027c-0.4379 0-0.804 0.4267-0.804 0.9555l-0.0201 3.257c-2.0247 0.2075-3.8681 1.1748-5.3381 2.6521l-1.9561-2.2909c-0.156-0.1901-0.3638-0.2856-0.5654-0.2866h0.0033-0.0065 0.0032c-0.2015 1e-3 -0.4028 0.0965-0.5588 0.2866-0.3138 0.3695-0.3138 0.9746 0 1.3441l1.9348 2.3123 0.0034 0.0042c-1.2532 1.7574-2.0625 3.9789-2.2323 6.4166h0.7647c0.0488 0 0.0966 0.0053 0.143 0.0154-0.0465-0.01-0.0942-0.0152-0.143-0.0152h-3.497c-0.438 0-0.804 0.4268-0.804 0.9491 0 0.5224 0.366 0.9555 0.804 0.9555h2.7323c0.1698 2.4381 0.986 4.66 2.2331 6.4175l-0.0028 0.0034-1.9297 2.3187c-0.3138 0.3694-0.3138 0.9746 0 1.344 0.1568 0.1848 0.3595 0.2803 0.5621 0.2803s0.4118-0.0955 0.5687-0.2803l1.9282-2.3123 1e-4 -1e-4c1.4757 1.4954 3.3361 2.4695 5.3729 2.6684v3.2622c0 0.5287 0.3661 0.9555 0.804 0.9555 0.438 0 0.7975-0.4268 0.7975-0.9555l0.0212-3.263c2.0293-0.2066 3.8833-1.1701 5.3555-2.6581l0.0028 0.0033 1.9282 2.306c0.1569 0.1847 0.3661 0.2803 0.5687 0.2803s0.4118-0.0956 0.5687-0.2803c0.3137-0.3695 0.3137-0.9746 0-1.3441l-1.9269-2.3334c1.2466-1.7626 2.0628-3.9762 2.2337-6.4125h2.7195c0.438 0 0.804-0.4268 0.804-0.9555s-0.366-0.9491-0.804-0.9491l-2.7198-0.0166c-0.1709-2.4276-0.9825-4.634-2.2222-6.3932l1.9157-2.3235c0.3137-0.3695 0.3137-0.9746 0-1.3441-0.1569-0.1911-0.3661-0.2866-0.5687-0.2866s-0.4118 0.0955-0.5687 0.2866l-1.9222 2.2988c-1.4756-1.4884-3.3591-2.454-5.3855-2.665v-3.252c0-0.5288-0.353-0.9555-0.7975-0.9555zm6.72 8.9311c0.0201-0.02 0.0396-0.0413 0.0584-0.0642l0.0144-0.0173-0.021 0.0239c-0.0167 0.0203-0.034 0.0395-0.0518 0.0576zm1.2545 6.6691c-0.0028-0.0609-0.0032-0.1186-0.0013-0.1732-0.0775-5.1648-3.6205-9.3364-7.9594-9.3438l-0.0138 1e-4 -0.0114-1e-4c-4.3862 0.0089-7.9565 4.2734-7.9565 9.5168 0 5.2239 3.5479 9.4824 7.9117 9.5229 0.0178-9e-4 0.036-0.0014 0.0546-0.0014 0.0194 0 0.0385 5e-4 0.0572 0.0015 4.3591-0.0317 7.9116-4.2849 7.9189-9.5068v-0.016zm-13.411 7.6696c0.0205-0.0858 0.0307-0.174 0.0307-0.2615 0-0.242-0.0784-0.4904-0.2353-0.6752-0.1503-0.1911-0.3595-0.2803-0.5621-0.2803l-0.0114 1e-4h0.0113c0.2026 0 0.4118 0.0892 0.5621 0.2803 0.1569 0.1847 0.2353 0.4332 0.2353 0.6752 0 0.0874-0.0102 0.1757-0.0306 0.2614zm-2.5382-7.6696c0-0.0236-7e-4 -0.0471-0.0021-0.0702 0.0014 0.0231 0.0022 0.0465 0.0022 0.07 0 0.0175-4e-4 0.0349-0.0012 0.0521 7e-4 -0.0172 0.0011-0.0345 0.0011-0.0519z" clip-rule="evenodd" class="tw-fill-art-primary" fill-rule="evenodd"/>
<path d="m25.442 10.125c0-1.2133 1.0146-2.1704 2.2154-2.1008l58.262 3.4199c1.1054 0.0669 1.9723 0.9842 1.9723 2.1009v7.3296h2v-7.3296c0-2.1736-1.6899-3.9673-3.853-4.0974l-58.264-3.42c-2.1001-0.12216-3.8976 1.356-4.264 3.347h-8.7578c-2.2641 0-4.0891 1.845-4.0891 4.1081v55.945c0 2.2631 1.825 4.1081 4.0891 4.1081h8.7036c0.1798 2.1936 2.0771 3.8865 4.3187 3.7561l58.264-3.4201c2.1631-0.1301 3.853-1.9237 3.853-4.0973v-11.184h-2v11.184c0 1.117-0.8674 2.0344-1.9731 2.1009l-58.261 3.4199c-1.2008 0.0696-2.2155-0.8875-2.2155-2.1009v-63.07zm-2 61.411v-60.162h-8.6897c-1.148 0-2.0891 0.9381-2.0891 2.1081v55.945c0 1.1701 0.9411 2.1081 2.0891 2.1081h8.6897zm64.449-36.67v9.4289h2v-9.4289h-2z" clip-rule="evenodd" class="tw-fill-art-primary" fill-rule="evenodd"/>
</g>
</svg>
`;