1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

Merge remote-tracking branch 'refs/remotes/origin/pm-15808-Show-suspended-org-modals-for-orgs-in-unpaid-and-canceled-status' into pm-15808-Show-suspended-org-modals-for-orgs-in-unpaid-and-canceled-status

This commit is contained in:
Cy Okeke
2024-12-20 18:42:38 +01:00
12 changed files with 369 additions and 44 deletions

View File

@@ -14,11 +14,11 @@
"build:watch:firefox": "npm run build:firefox -- --watch",
"build:watch:opera": "npm run build:opera -- --watch",
"build:watch:safari": "npm run build:safari -- --watch",
"build:prod:chrome": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" npm run build:chrome",
"build:prod:edge": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" npm run build:edge",
"build:prod:firefox": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" npm run build:firefox",
"build:prod:opera": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" npm run build:opera",
"build:prod:safari": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" npm run build:safari",
"build:prod:chrome": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:chrome",
"build:prod:edge": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:edge",
"build:prod:firefox": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:firefox",
"build:prod:opera": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:opera",
"build:prod:safari": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:safari",
"dist:chrome": "npm run build:prod:chrome && mkdir -p dist && ./scripts/compress.ps1 dist-chrome.zip",
"dist:edge": "npm run build:prod:edge && mkdir -p dist && ./scripts/compress.ps1 dist-edge.zip",
"dist:firefox": "npm run build:prod:firefox && mkdir -p dist && ./scripts/compress.ps1 dist-firefox.zip",

View File

@@ -2813,6 +2813,254 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f
</div>
`;
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user renders correctly when there are multiple TOTP elements with username displayed 1`] = `
<div
class="inline-menu-list-container theme_light"
>
<ul
class="inline-menu-list-actions"
role="list"
>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: user1"
aria-label="fillCredentialsFor website login 1"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
>
<div
style="position: relative;"
>
<svg
aria-hidden="true"
viewBox="0 0 29 29"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="12.5"
stroke-dasharray="78.5"
stroke-dashoffset="78.5"
stroke-width="3"
style="stroke-dashoffset: NaN;"
transform="rotate(-90 14.5 14.5)"
/>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="14"
stroke-width="1"
/>
</svg>
<span
aria-label=""
bittypography="helper"
class="totp-sec-span"
>
NaN
</span>
</div>
</span>
<div
class="cipher-details"
>
<span
aria-label=""
class="cipher-name"
/>
<span
class="cipher-subtitle"
title="user1"
>
user1
</span>
<span
aria-label=""
class="cipher-subtitle"
data-testid="totp-code"
>
123 456
</span>
</div>
</button>
<button
aria-label="view website login 1, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
<li
class="inline-menu-list-actions-item"
role="listitem"
>
<div
class="cipher-container"
>
<button
aria-description="username: user2"
aria-label="fillCredentialsFor website login 2"
class="fill-cipher-button inline-menu-list-action"
tabindex="-1"
>
<span
aria-hidden="true"
class="cipher-icon"
>
<div
style="position: relative;"
>
<svg
aria-hidden="true"
viewBox="0 0 29 29"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="12.5"
stroke-dasharray="78.5"
stroke-dashoffset="78.5"
stroke-width="3"
style="stroke-dashoffset: NaN;"
transform="rotate(-90 14.5 14.5)"
/>
<circle
class="circle-color"
cx="14.5"
cy="14.5"
fill="none"
r="14"
stroke-width="1"
/>
</svg>
<span
aria-label=""
bittypography="helper"
class="totp-sec-span"
>
NaN
</span>
</div>
</span>
<div
class="cipher-details"
>
<span
aria-label=""
class="cipher-name"
/>
<span
class="cipher-subtitle"
title="user2"
>
user2
</span>
<span
aria-label=""
class="cipher-subtitle"
data-testid="totp-code"
>
654 321
</span>
</div>
</button>
<button
aria-label="view website login 2, opensInANewWindow"
class="view-cipher-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M16.587 7.932H5.9a.455.455 0 0 1-.31-.12.393.393 0 0 1-.127-.287c0-.108.046-.211.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.076.128.179.128.287a.393.393 0 0 1-.128.288.455.455 0 0 1-.31.119Zm0 2.474H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm0 2.468H5.9a.455.455 0 0 1-.31-.119.393.393 0 0 1-.127-.287c0-.108.046-.212.128-.288a.455.455 0 0 1 .309-.119h10.687c.117 0 .228.043.31.12.082.075.128.179.128.287a.393.393 0 0 1-.128.287.455.455 0 0 1-.31.12Zm2.163-8.103v10.457H1.25V4.771h17.5Zm0-1.162H1.25a1.3 1.3 0 0 0-.884.34A1.122 1.122 0 0 0 0 4.772v10.457c0 .308.132.604.366.822a1.3 1.3 0 0 0 .884.34h17.5a1.3 1.3 0 0 0 .884-.34c.234-.218.366-.514.366-.822V4.771c0-.308-.132-.603-.366-.821a1.3 1.3 0 0 0-.884-.34ZM3.213 8.01c.287 0 .52-.217.52-.484s-.234-.483-.52-.483c-.288 0-.52.216-.52.483s.233.483.52.483Zm0 4.903c.287 0 .52-.217.52-.484 0-.266-.234-.483-.52-.483-.287 0-.52.216-.52.483s.233.484.52.484Zm0-2.452c.287 0 .52-.216.52-.483 0-.268-.234-.484-.52-.484-.288 0-.52.216-.52.484 0 .267.233.483.52.483Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .113h20v19.773H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
</button>
</div>
</li>
</ul>
</div>
`;
exports[`AutofillInlineMenuList initAutofillInlineMenuList the locked inline menu for an unauthenticated user creates the views for the locked inline menu 1`] = `
<div
class="inline-menu-list-container theme_light"

View File

@@ -140,6 +140,47 @@ describe("AutofillInlineMenuList", () => {
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
});
it("renders correctly when there are multiple TOTP elements with username displayed", async () => {
const totpCipher1 = createAutofillOverlayCipherDataMock(1, {
type: CipherType.Login,
login: {
totp: "123456",
totpField: true,
username: "user1",
},
});
const totpCipher2 = createAutofillOverlayCipherDataMock(2, {
type: CipherType.Login,
login: {
totp: "654321",
totpField: true,
username: "user2",
},
});
postWindowMessage(
createInitAutofillInlineMenuListMessageMock({
inlineMenuFillType: CipherType.Login,
ciphers: [totpCipher1, totpCipher2],
}),
);
await flushPromises();
const checkSubtitleElement = (username: string) => {
const subtitleElement = autofillInlineMenuList["inlineMenuListContainer"].querySelector(
`span.cipher-subtitle[title="${username}"]`,
);
expect(subtitleElement).not.toBeNull();
expect(subtitleElement.textContent).toBe(username);
};
checkSubtitleElement("user1");
checkSubtitleElement("user2");
expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
});
it("creates the view for a totp field", () => {
postWindowMessage(
createInitAutofillInlineMenuListMessageMock({

View File

@@ -1163,7 +1163,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
}
if (cipher.login?.totpField && cipher.login?.totp) {
return this.buildTotpElement(cipher.login?.totp);
return this.buildTotpElement(cipher.login?.totp, cipher.login?.username);
}
const subTitleText = this.getSubTitleText(cipher);
const cipherSubtitleElement = this.buildCipherSubtitleElement(subTitleText);
@@ -1174,13 +1174,24 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
return cipherDetailsElement;
}
/**
* Checks if there is more than one TOTP element being displayed.
*
* @returns {boolean} - Returns true if more than one TOTP element is displayed, otherwise false.
*/
private multipleTotpElements(): boolean {
return (
this.ciphers.filter((cipher) => cipher.login?.totpField && cipher.login?.totp).length > 1
);
}
/**
* Builds a TOTP element for a given TOTP code.
*
* @param totp - The TOTP code to display.
*/
private buildTotpElement(totpCode: string): HTMLDivElement | null {
private buildTotpElement(totpCode: string, username?: string): HTMLDivElement | null {
if (!totpCode) {
return null;
}
@@ -1196,12 +1207,17 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
containerElement.appendChild(totpHeading);
const subtitleElement = document.createElement("span");
subtitleElement.classList.add("cipher-subtitle");
subtitleElement.textContent = formattedTotpCode;
subtitleElement.setAttribute("aria-label", this.getTranslation("totpCodeAria"));
subtitleElement.setAttribute("data-testid", "totp-code");
containerElement.appendChild(subtitleElement);
if (this.multipleTotpElements() && username) {
const usernameSubtitle = this.buildCipherSubtitleElement(username);
containerElement.appendChild(usernameSubtitle);
}
const totpCodeSpan = document.createElement("span");
totpCodeSpan.classList.add("cipher-subtitle");
totpCodeSpan.textContent = formattedTotpCode;
totpCodeSpan.setAttribute("aria-label", this.getTranslation("totpCodeAria"));
totpCodeSpan.setAttribute("data-testid", "totp-code");
containerElement.appendChild(totpCodeSpan);
return containerElement;
}

View File

@@ -18,14 +18,14 @@
"license": "SEE LICENSE IN LICENSE.txt",
"scripts": {
"clean": "rimraf dist",
"build:oss": "webpack",
"build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
"build:oss:debug": "npm run build:oss && node --inspect ./build/bw.js",
"build:oss:watch": "webpack --watch",
"build:oss:prod": "cross-env NODE_ENV=production webpack",
"build:oss:prod:watch": "cross-env NODE_ENV=production webpack --watch",
"debug": "node --inspect ./build/bw.js",
"publish:npm": "npm run build:oss:prod && npm publish --access public",
"build:bit": "webpack -c ../../bitwarden_license/bit-cli/webpack.config.js",
"build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-cli/webpack.config.js",
"build:bit:debug": "npm run build:bit && node --inspect ./build/bw.js",
"build:bit:watch": "webpack --watch -c ../../bitwarden_license/bit-cli/webpack.config.js",
"build:bit:prod": "cross-env NODE_ENV=production npm run build:bit",

View File

@@ -19,7 +19,7 @@
"postinstall": "electron-rebuild",
"start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build",
"build-native": "cd desktop_native && node build.js",
"build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"",
"build": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"",
"build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"",
"build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js",
"build:preload:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch",

View File

@@ -2,8 +2,8 @@
"name": "@bitwarden/web-vault",
"version": "2024.12.1",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
"build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
"build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
"build:oss:watch": "webpack serve",
"build:bit:watch": "webpack serve -c ../../bitwarden_license/bit-web/webpack.config.js",
"build:bit:dev": "cross-env ENV=development npm run build:bit",

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
@@ -31,5 +29,5 @@ import {
],
})
export class AdditionalOptionsComponent {
@Input() notes: string;
@Input() notes: string = "";
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Input, OnChanges, OnDestroy } from "@angular/core";
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
@@ -48,19 +46,19 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide
],
})
export class CipherViewComponent implements OnChanges, OnDestroy {
@Input({ required: true }) cipher: CipherView;
@Input({ required: true }) cipher: CipherView | null = null;
/**
* Optional list of collections the cipher is assigned to. If none are provided, they will be fetched using the
* `CipherService` and the `collectionIds` property of the cipher.
*/
@Input() collections: CollectionView[];
@Input() collections?: CollectionView[];
/** Should be set to true when the component is used within the Admin Console */
@Input() isAdminConsole?: boolean = false;
organization$: Observable<Organization>;
folder$: Observable<FolderView>;
organization$: Observable<Organization | undefined> | undefined;
folder$: Observable<FolderView | undefined> | undefined;
private destroyed$: Subject<void> = new Subject();
cardIsExpired: boolean = false;
@@ -86,24 +84,38 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
}
get hasCard() {
if (!this.cipher) {
return false;
}
const { cardholderName, code, expMonth, expYear, number } = this.cipher.card;
return cardholderName || code || expMonth || expYear || number;
}
get hasLogin() {
if (!this.cipher) {
return false;
}
const { username, password, totp } = this.cipher.login;
return username || password || totp;
}
get hasAutofill() {
return this.cipher.login?.uris.length > 0;
const uris = this.cipher?.login?.uris.length ?? 0;
return uris > 0;
}
get hasSshKey() {
return this.cipher.sshKey?.privateKey;
return !!this.cipher?.sshKey?.privateKey;
}
async loadCipherData() {
if (!this.cipher) {
return;
}
// Load collections if not provided and the cipher has collectionIds
if (
this.cipher.collectionIds &&

View File

@@ -11,7 +11,6 @@ import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
CardComponent,
FormFieldModule,
SectionComponent,
SectionHeaderComponent,
@@ -37,7 +36,6 @@ type TotpCodeValues = {
imports: [
CommonModule,
JslibModule,
CardComponent,
SectionComponent,
SectionHeaderComponent,
TypographyModule,

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AfterViewInit, Component, ContentChildren, QueryList } from "@angular/core";
import { CardComponent, BitFormFieldComponent } from "@bitwarden/components";
@@ -14,14 +12,16 @@ import { CardComponent, BitFormFieldComponent } from "@bitwarden/components";
* A thin wrapper around the `bit-card` component that disables the bottom border for the last form field.
*/
export class ReadOnlyCipherCardComponent implements AfterViewInit {
@ContentChildren(BitFormFieldComponent) formFields: QueryList<BitFormFieldComponent>;
@ContentChildren(BitFormFieldComponent) formFields?: QueryList<BitFormFieldComponent>;
ngAfterViewInit(): void {
// Disable the bottom border for the last form field
if (this.formFields.last) {
if (this.formFields?.last) {
// Delay model update until next change detection cycle
setTimeout(() => {
this.formFields.last.disableReadOnlyBorder = true;
if (this.formFields) {
this.formFields.last.disableReadOnlyBorder = true;
}
});
}
}

View File

@@ -1,12 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgIf } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
CardComponent,
FormFieldModule,
IconButtonModule,
SectionComponent,
@@ -23,7 +20,6 @@ import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-
imports: [
NgIf,
JslibModule,
CardComponent,
SectionComponent,
SectionHeaderComponent,
TypographyModule,
@@ -33,11 +29,11 @@ import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-
],
})
export class ViewIdentitySectionsComponent implements OnInit {
@Input() cipher: CipherView;
@Input({ required: true }) cipher: CipherView | null = null;
showPersonalDetails: boolean;
showIdentificationDetails: boolean;
showContactDetails: boolean;
showPersonalDetails: boolean = false;
showIdentificationDetails: boolean = false;
showContactDetails: boolean = false;
ngOnInit(): void {
this.showPersonalDetails = this.hasPersonalDetails();
@@ -47,6 +43,10 @@ export class ViewIdentitySectionsComponent implements OnInit {
/** Returns all populated address fields */
get addressFields(): string {
if (!this.cipher) {
return "";
}
const { address1, address2, address3, fullAddressPart2, country } = this.cipher.identity;
return [address1, address2, address3, fullAddressPart2, country].filter(Boolean).join("\n");
}
@@ -58,18 +58,30 @@ export class ViewIdentitySectionsComponent implements OnInit {
/** Returns true when any of the "personal detail" attributes are populated */
private hasPersonalDetails(): boolean {
if (!this.cipher) {
return false;
}
const { username, company, fullName } = this.cipher.identity;
return Boolean(fullName || username || company);
}
/** Returns true when any of the "identification detail" attributes are populated */
private hasIdentificationDetails(): boolean {
if (!this.cipher) {
return false;
}
const { ssn, passportNumber, licenseNumber } = this.cipher.identity;
return Boolean(ssn || passportNumber || licenseNumber);
}
/** Returns true when any of the "contact detail" attributes are populated */
private hasContactDetails(): boolean {
if (!this.cipher) {
return false;
}
const { email, phone } = this.cipher.identity;
return Boolean(email || phone || this.addressFields);