Merge branch 'main' into ps/extension-refresh
This commit is contained in:
commit
04180482c5
|
@ -128,6 +128,21 @@
|
|||
"copyLicenseNumber": {
|
||||
"message": "Copy license number"
|
||||
},
|
||||
"copyCustomField": {
|
||||
"message": "Copy $FIELD$",
|
||||
"placeholders": {
|
||||
"field": {
|
||||
"content": "$1",
|
||||
"example": "Custom field label"
|
||||
}
|
||||
}
|
||||
},
|
||||
"copyWebsite": {
|
||||
"message": "Copy website"
|
||||
},
|
||||
"copyNotes": {
|
||||
"message": "Copy notes"
|
||||
},
|
||||
"autoFill": {
|
||||
"message": "Autofill"
|
||||
},
|
||||
|
@ -2253,6 +2268,10 @@
|
|||
"message": "Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendDetails": {
|
||||
"message": "Send details",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"searchSends": {
|
||||
"message": "Search Sends",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
|
@ -2264,6 +2283,9 @@
|
|||
"sendTypeText": {
|
||||
"message": "Text"
|
||||
},
|
||||
"sendTypeTextToShare": {
|
||||
"message": "Text to share"
|
||||
},
|
||||
"sendTypeFile": {
|
||||
"message": "File"
|
||||
},
|
||||
|
@ -2271,6 +2293,9 @@
|
|||
"message": "All Sends",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"hideTextByDefault": {
|
||||
"message": "Hide text by default"
|
||||
},
|
||||
"maxAccessCountReached": {
|
||||
"message": "Max access count reached",
|
||||
"description": "This text will be displayed after a Send has been accessed the maximum amount of times."
|
||||
|
@ -2344,6 +2369,10 @@
|
|||
"message": "The Send will be permanently deleted on the specified date and time.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletionDateDescV2": {
|
||||
"message": "The Send will be permanently deleted on this date.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"expirationDate": {
|
||||
"message": "Expiration date"
|
||||
},
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { CommonModule, Location } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, map, of, startWith, switchMap, takeUntil } from "rxjs";
|
||||
import { Subject, firstValueFrom, map, of, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { LockService } from "@bitwarden/auth/common";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
|
@ -70,6 +71,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private authService: AuthService,
|
||||
private configService: ConfigService,
|
||||
private lockService: LockService,
|
||||
) {}
|
||||
|
||||
get accountLimit() {
|
||||
|
@ -131,26 +133,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
|||
|
||||
async lockAll() {
|
||||
this.loading = true;
|
||||
this.availableAccounts$
|
||||
.pipe(
|
||||
map((accounts) =>
|
||||
accounts
|
||||
.filter((account) => account.id !== this.specialAddAccountId)
|
||||
.sort((a, b) => (a.isActive ? -1 : b.isActive ? 1 : 0)) // Log out of the active account first
|
||||
.map((account) => account.id),
|
||||
),
|
||||
switchMap(async (accountIds) => {
|
||||
if (accountIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must lock active (first) account first, then order doesn't matter
|
||||
await this.vaultTimeoutService.lock(accountIds.shift());
|
||||
await Promise.all(accountIds.map((id) => this.vaultTimeoutService.lock(id)));
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(() => this.router.navigate(["lock"]));
|
||||
await this.lockService.lockAll();
|
||||
await this.router.navigate(["lock"]);
|
||||
}
|
||||
|
||||
async logOut(userId: UserId) {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { filter, firstValueFrom } from "rxjs";
|
||||
|
||||
import { LockService } from "@bitwarden/auth/common";
|
||||
import {
|
||||
CommandDefinition,
|
||||
MessageListener,
|
||||
MessageSender,
|
||||
} from "@bitwarden/common/platform/messaging";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
const LOCK_ALL_FINISHED = new CommandDefinition<{ requestId: string }>("lockAllFinished");
|
||||
const LOCK_ALL = new CommandDefinition<{ requestId: string }>("lockAll");
|
||||
|
||||
export class ForegroundLockService implements LockService {
|
||||
constructor(
|
||||
private readonly messageSender: MessageSender,
|
||||
private readonly messageListener: MessageListener,
|
||||
) {}
|
||||
|
||||
async lockAll(): Promise<void> {
|
||||
const requestId = Utils.newGuid();
|
||||
const finishMessage = firstValueFrom(
|
||||
this.messageListener
|
||||
.messages$(LOCK_ALL_FINISHED)
|
||||
.pipe(filter((m) => m.requestId === requestId)),
|
||||
);
|
||||
|
||||
this.messageSender.send(LOCK_ALL, { requestId });
|
||||
|
||||
await finishMessage;
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import {
|
|||
AuthRequestService,
|
||||
LoginEmailServiceAbstraction,
|
||||
LogoutReason,
|
||||
DefaultLockService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||
|
@ -1037,6 +1038,14 @@ export default class MainBackground {
|
|||
|
||||
const systemUtilsServiceReloadCallback = async () => {
|
||||
await this.taskSchedulerService.clearAllScheduledTasks();
|
||||
if (this.platformUtilsService.isSafari()) {
|
||||
// If we do `chrome.runtime.reload` on safari they will send an onInstalled reason of install
|
||||
// and that prompts us to show a new tab, this apparently doesn't happen on sideloaded
|
||||
// extensions and only shows itself production scenarios. See: https://bitwarden.atlassian.net/browse/PM-12298
|
||||
self.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
BrowserApi.reloadExtension();
|
||||
};
|
||||
|
||||
|
@ -1065,6 +1074,9 @@ export default class MainBackground {
|
|||
this.scriptInjectorService,
|
||||
this.configService,
|
||||
);
|
||||
|
||||
const lockService = new DefaultLockService(this.accountService, this.vaultTimeoutService);
|
||||
|
||||
this.runtimeBackground = new RuntimeBackground(
|
||||
this,
|
||||
this.autofillService,
|
||||
|
@ -1079,6 +1091,7 @@ export default class MainBackground {
|
|||
this.fido2Background,
|
||||
messageListener,
|
||||
this.accountService,
|
||||
lockService,
|
||||
);
|
||||
this.nativeMessagingBackground = new NativeMessagingBackground(
|
||||
this.cryptoService,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { firstValueFrom, map, mergeMap, of, switchMap } from "rxjs";
|
||||
|
||||
import { LockService } from "@bitwarden/auth/common";
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AutofillOverlayVisibility, ExtensionCommand } from "@bitwarden/common/autofill/constants";
|
||||
|
@ -48,6 +49,7 @@ export default class RuntimeBackground {
|
|||
private fido2Background: Fido2Background,
|
||||
private messageListener: MessageListener,
|
||||
private accountService: AccountService,
|
||||
private readonly lockService: LockService,
|
||||
) {
|
||||
// onInstalled listener must be wired up before anything else, so we do it in the ctor
|
||||
chrome.runtime.onInstalled.addListener((details: any) => {
|
||||
|
@ -245,6 +247,12 @@ export default class RuntimeBackground {
|
|||
case "lockVault":
|
||||
await this.main.vaultTimeoutService.lock(msg.userId);
|
||||
break;
|
||||
case "lockAll":
|
||||
{
|
||||
await this.lockService.lockAll();
|
||||
this.messagingService.send("lockAllFinished", { requestId: msg.requestId });
|
||||
}
|
||||
break;
|
||||
case "logout":
|
||||
await this.main.logout(msg.expired, msg.userId);
|
||||
break;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Observable } from "rxjs";
|
||||
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { isBrowserSafariApi } from "@bitwarden/platform";
|
||||
|
||||
import { TabMessage } from "../../types/tab-messages";
|
||||
import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service";
|
||||
|
@ -9,10 +10,7 @@ import { registerContentScriptsPolyfill } from "./browser-api.register-content-s
|
|||
|
||||
export class BrowserApi {
|
||||
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
|
||||
static isSafariApi: boolean =
|
||||
navigator.userAgent.indexOf(" Safari/") !== -1 &&
|
||||
navigator.userAgent.indexOf(" Chrome/") === -1 &&
|
||||
navigator.userAgent.indexOf(" Chromium/") === -1;
|
||||
static isSafariApi: boolean = isBrowserSafariApi();
|
||||
static isChromeApi: boolean = !BrowserApi.isSafariApi && typeof chrome !== "undefined";
|
||||
static isFirefoxOnAndroid: boolean =
|
||||
navigator.userAgent.indexOf("Firefox/") !== -1 && navigator.userAgent.indexOf("Android") !== -1;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { switchMap, merge, delay, filter, concatMap, map } from "rxjs";
|
||||
import { switchMap, merge, delay, filter, concatMap, map, first, of } from "rxjs";
|
||||
|
||||
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import {
|
||||
|
@ -61,7 +61,18 @@ export class PopupViewCacheBackgroundService {
|
|||
merge(
|
||||
// on tab changed, excluding extension tabs
|
||||
fromChromeEvent(chrome.tabs.onActivated).pipe(
|
||||
switchMap(([tabInfo]) => BrowserApi.getTab(tabInfo.tabId)),
|
||||
switchMap((tabs) => BrowserApi.getTab(tabs[0].tabId)),
|
||||
switchMap((tab) => {
|
||||
// FireFox sets the `url` to "about:blank" and won't populate the `url` until the `onUpdated` event
|
||||
if (tab.url !== "about:blank") {
|
||||
return of(tab);
|
||||
}
|
||||
|
||||
return fromChromeEvent(chrome.tabs.onUpdated).pipe(
|
||||
first(),
|
||||
switchMap(([tabId]) => BrowserApi.getTab(tabId)),
|
||||
);
|
||||
}),
|
||||
map((tab) => tab.url || tab.pendingUrl),
|
||||
filter((url) => !url.startsWith(chrome.runtime.getURL(""))),
|
||||
),
|
||||
|
|
|
@ -127,6 +127,12 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
this.showNativeMessagingFingerprintDialog(msg);
|
||||
} else if (msg.command === "showToast") {
|
||||
this.toastService._showToast(msg);
|
||||
} else if (msg.command === "reloadProcess") {
|
||||
if (this.platformUtilsService.isSafari()) {
|
||||
window.setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
}
|
||||
} else if (msg.command === "reloadPopup") {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { LockService, PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
|
@ -91,6 +91,7 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
|||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
|
||||
import AutofillService from "../../autofill/services/autofill.service";
|
||||
|
@ -560,6 +561,11 @@ const safeProviders: SafeProvider[] = [
|
|||
useClass: ExtensionAnonLayoutWrapperDataService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LockService,
|
||||
useClass: ForegroundLockService,
|
||||
deps: [MessageSender, MessageListener],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -20,17 +20,15 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
|
|||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
SearchModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
DialogService,
|
||||
IconButtonModule,
|
||||
SearchModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { TotpCaptureService } from "@bitwarden/vault";
|
||||
|
||||
import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view";
|
||||
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
|
||||
import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service";
|
||||
|
||||
import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component";
|
||||
|
@ -41,7 +39,6 @@ import { VaultPopupAutofillService } from "./../../../services/vault-popup-autof
|
|||
selector: "app-view-v2",
|
||||
templateUrl: "view-v2.component.html",
|
||||
standalone: true,
|
||||
providers: [{ provide: TotpCaptureService, useClass: BrowserTotpCaptureService }],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SearchModule,
|
||||
|
|
|
@ -13,15 +13,10 @@ describe("BrowserTotpCaptureService", () => {
|
|||
let testBed: TestBed;
|
||||
let service: BrowserTotpCaptureService;
|
||||
let mockCaptureVisibleTab: jest.SpyInstance;
|
||||
let createNewTabSpy: jest.SpyInstance;
|
||||
|
||||
const validTotpUrl = "otpauth://totp/label?secret=123";
|
||||
|
||||
beforeEach(() => {
|
||||
const tabReturn = new Promise<chrome.tabs.Tab>((resolve) =>
|
||||
resolve({ url: "google.com", active: true } as chrome.tabs.Tab),
|
||||
);
|
||||
createNewTabSpy = jest.spyOn(BrowserApi, "createNewTab").mockReturnValue(tabReturn);
|
||||
mockCaptureVisibleTab = jest.spyOn(BrowserApi, "captureVisibleTab");
|
||||
mockCaptureVisibleTab.mockResolvedValue("screenshot");
|
||||
|
||||
|
@ -71,10 +66,4 @@ describe("BrowserTotpCaptureService", () => {
|
|||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should call BrowserApi.createNewTab with a given loginURI", async () => {
|
||||
await service.openAutofillNewTab("www.google.com");
|
||||
|
||||
expect(createNewTabSpy).toHaveBeenCalledWith("www.google.com");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,8 +20,4 @@ export class BrowserTotpCaptureService implements TotpCaptureService {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async openAutofillNewTab(loginUri: string) {
|
||||
await BrowserApi.createNewTab(loginUri);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -419,6 +419,23 @@
|
|||
"enableHardwareAccelerationDesc" | i18n
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="!isLinux">
|
||||
<div class="checkbox">
|
||||
<label for="allowScreenshots">
|
||||
<input
|
||||
id="allowScreenshots"
|
||||
type="checkbox"
|
||||
aria-describedby="allowScreenshotsHelp"
|
||||
formControlName="allowScreenshots"
|
||||
(change)="saveAllowScreenshots()"
|
||||
/>
|
||||
{{ "allowScreenshots" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<small id="allowScreenshotsHelp" class="help-block">{{
|
||||
"allowScreenshotsDesc" | i18n
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="showDuckDuckGoIntegrationOption">
|
||||
<div class="checkbox">
|
||||
<label for="enableDuckDuckGoBrowserIntegration">
|
||||
|
|
|
@ -109,6 +109,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|||
disabled: true,
|
||||
}),
|
||||
enableHardwareAcceleration: true,
|
||||
allowScreenshots: false,
|
||||
enableDuckDuckGoBrowserIntegration: false,
|
||||
theme: [null as ThemeType | null],
|
||||
locale: [null as string | null],
|
||||
|
@ -277,6 +278,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|||
enableHardwareAcceleration: await firstValueFrom(
|
||||
this.desktopSettingsService.hardwareAcceleration$,
|
||||
),
|
||||
allowScreenshots: await firstValueFrom(this.desktopSettingsService.allowScreenshots$),
|
||||
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
||||
locale: await firstValueFrom(this.i18nService.userSetLocale$),
|
||||
};
|
||||
|
@ -727,6 +729,10 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
async saveAllowScreenshots() {
|
||||
await this.desktopSettingsService.setAllowScreenshots(this.form.value.allowScreenshots);
|
||||
}
|
||||
|
||||
private async generateVaultTimeoutOptions(): Promise<VaultTimeoutOption[]> {
|
||||
let vaultTimeoutOptions: VaultTimeoutOption[] = [
|
||||
{ name: this.i18nService.t("oneMinute"), value: 1 },
|
||||
|
|
|
@ -3077,6 +3077,12 @@
|
|||
"textSends": {
|
||||
"message": "Text Sends"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Allow screen capture"
|
||||
},
|
||||
"allowScreenshotsDesc": {
|
||||
"message": "Allow screen capture of the Bitwarden desktop application."
|
||||
},
|
||||
"ssoError": {
|
||||
"message": "No free ports could be found for the sso login."
|
||||
},
|
||||
|
|
|
@ -69,6 +69,13 @@ export class WindowMain {
|
|||
this.logService.info("Render process reloaded");
|
||||
});
|
||||
|
||||
this.desktopSettingsService.allowScreenshots$.subscribe((allowed) => {
|
||||
if (this.win == null) {
|
||||
return;
|
||||
}
|
||||
this.win.setContentProtection(!allowed);
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
if (!isMacAppStore() && !isSnapStore()) {
|
||||
|
@ -270,6 +277,14 @@ export class WindowMain {
|
|||
});
|
||||
});
|
||||
|
||||
firstValueFrom(this.desktopSettingsService.allowScreenshots$)
|
||||
.then((allowScreenshots) => {
|
||||
this.win.setContentProtection(!allowScreenshots);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logService.error(e);
|
||||
});
|
||||
|
||||
if (this.createWindowCallback) {
|
||||
this.createWindowCallback(this.win);
|
||||
}
|
||||
|
|
|
@ -71,6 +71,10 @@ const MINIMIZE_ON_COPY = new UserKeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "
|
|||
clearOn: [], // User setting, no need to clear
|
||||
});
|
||||
|
||||
const ALLOW_SCREENSHOTS = new KeyDefinition<boolean>(DESKTOP_SETTINGS_DISK, "allowScreenshots", {
|
||||
deserializer: (b) => b,
|
||||
});
|
||||
|
||||
/**
|
||||
* Various settings for controlling application behavior specific to the desktop client.
|
||||
*/
|
||||
|
@ -139,6 +143,13 @@ export class DesktopSettingsService {
|
|||
browserIntegrationFingerprintEnabled$ =
|
||||
this.browserIntegrationFingerprintEnabledState.state$.pipe(map(Boolean));
|
||||
|
||||
private readonly allowScreenshotState = this.stateProvider.getGlobal(ALLOW_SCREENSHOTS);
|
||||
|
||||
/**
|
||||
* The application setting for whether or not to allow screenshots of the app.
|
||||
*/
|
||||
allowScreenshots$ = this.allowScreenshotState.state$.pipe(map(Boolean));
|
||||
|
||||
private readonly minimizeOnCopyState = this.stateProvider.getActive(MINIMIZE_ON_COPY);
|
||||
|
||||
/**
|
||||
|
@ -255,4 +266,12 @@ export class DesktopSettingsService {
|
|||
async setMinimizeOnCopy(value: boolean, userId: UserId) {
|
||||
await this.stateProvider.getUser(userId, MINIMIZE_ON_COPY).update(() => value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the setting for whether or not the screenshot protection is enabled.
|
||||
* @param value `true` if the screenshot protection is enabled, `false` if it is not.
|
||||
*/
|
||||
async setAllowScreenshots(value: boolean) {
|
||||
await this.allowScreenshotState.update(() => value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2024.9.0",
|
||||
"version": "2024.9.1",
|
||||
"scripts": {
|
||||
"build:oss": "webpack",
|
||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
|
|
@ -118,6 +118,7 @@ export class ViewComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.dialogRef.close({ action: ViewCipherDialogResult.deleted });
|
||||
await this.router.navigate(["/vault"]);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -81,7 +81,11 @@
|
|||
(click)="addCipher()"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned"
|
||||
*ngIf="
|
||||
filter.type !== 'trash' &&
|
||||
filter.collectionId !== Unassigned &&
|
||||
selectedCollection?.node?.canEditItems(organization)
|
||||
"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus"></i> {{ "newItem" | i18n }}
|
||||
</button>
|
||||
|
|
|
@ -592,6 +592,45 @@
|
|||
"message": "Copy URI",
|
||||
"description": "Copy URI to clipboard"
|
||||
},
|
||||
"copyCustomField": {
|
||||
"message": "Copy $FIELD$",
|
||||
"placeholders": {
|
||||
"field": {
|
||||
"content": "$1",
|
||||
"example": "Custom field label"
|
||||
}
|
||||
}
|
||||
},
|
||||
"copyWebsite": {
|
||||
"message": "Copy website"
|
||||
},
|
||||
"copyNotes": {
|
||||
"message": "Copy notes"
|
||||
},
|
||||
"copyAddress": {
|
||||
"message": "Copy address"
|
||||
},
|
||||
"copyPhone": {
|
||||
"message": "Copy phone"
|
||||
},
|
||||
"copyEmail": {
|
||||
"message": "Copy email"
|
||||
},
|
||||
"copyCompany": {
|
||||
"message": "Copy company"
|
||||
},
|
||||
"copySSN": {
|
||||
"message": "Copy Social Security number"
|
||||
},
|
||||
"copyPassportNumber": {
|
||||
"message": "Copy passport number"
|
||||
},
|
||||
"copyLicenseNumber": {
|
||||
"message": "Copy license number"
|
||||
},
|
||||
"copyName": {
|
||||
"message": "Copy name"
|
||||
},
|
||||
"me": {
|
||||
"message": "Me"
|
||||
},
|
||||
|
@ -4861,12 +4900,6 @@
|
|||
"deleteAnyCollection": {
|
||||
"message": "Delete any collection"
|
||||
},
|
||||
"editAssignedCollections": {
|
||||
"message": "Edit assigned collections"
|
||||
},
|
||||
"deleteAssignedCollections": {
|
||||
"message": "Delete assigned collections"
|
||||
},
|
||||
"manageGroups": {
|
||||
"message": "Manage groups"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { combineLatest, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export abstract class LockService {
|
||||
/**
|
||||
* Locks all accounts.
|
||||
*/
|
||||
abstract lockAll(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DefaultLockService implements LockService {
|
||||
constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly vaultTimeoutService: VaultTimeoutService,
|
||||
) {}
|
||||
|
||||
async lockAll() {
|
||||
const accounts = await firstValueFrom(
|
||||
combineLatest([this.accountService.activeAccount$, this.accountService.accounts$]).pipe(
|
||||
map(([activeAccount, accounts]) => {
|
||||
const otherAccounts = Object.keys(accounts) as UserId[];
|
||||
|
||||
if (activeAccount == null) {
|
||||
return { activeAccount: null, otherAccounts: otherAccounts };
|
||||
}
|
||||
|
||||
return {
|
||||
activeAccount: activeAccount.id,
|
||||
otherAccounts: otherAccounts.filter((accountId) => accountId !== activeAccount.id),
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
for (const otherAccount of accounts.otherAccounts) {
|
||||
await this.vaultTimeoutService.lock(otherAccount);
|
||||
}
|
||||
|
||||
// Do the active account last in case we ever try to route the user on lock
|
||||
// that way this whole operation will be complete before that routing
|
||||
// could take place.
|
||||
if (accounts.activeAccount != null) {
|
||||
await this.vaultTimeoutService.lock(accounts.activeAccount);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DefaultLockService } from "./lock.service";
|
||||
|
||||
describe("DefaultLockService", () => {
|
||||
const mockUser1 = "user1" as UserId;
|
||||
const mockUser2 = "user2" as UserId;
|
||||
const mockUser3 = "user3" as UserId;
|
||||
|
||||
const accountService = mockAccountServiceWith(mockUser1);
|
||||
const vaultTimeoutService = mock<VaultTimeoutService>();
|
||||
|
||||
const sut = new DefaultLockService(accountService, vaultTimeoutService);
|
||||
describe("lockAll", () => {
|
||||
it("locks the active account last", async () => {
|
||||
await accountService.addAccount(mockUser2, {
|
||||
name: "name2",
|
||||
email: "email2@example.com",
|
||||
emailVerified: false,
|
||||
});
|
||||
|
||||
await accountService.addAccount(mockUser3, {
|
||||
name: "name3",
|
||||
email: "email3@example.com",
|
||||
emailVerified: false,
|
||||
});
|
||||
|
||||
await sut.lockAll();
|
||||
|
||||
expect(vaultTimeoutService.lock).toHaveBeenCalledTimes(3);
|
||||
// Non-Active users should be called first
|
||||
expect(vaultTimeoutService.lock).toHaveBeenNthCalledWith(1, mockUser2);
|
||||
expect(vaultTimeoutService.lock).toHaveBeenNthCalledWith(2, mockUser3);
|
||||
|
||||
// Active user should be called last
|
||||
expect(vaultTimeoutService.lock).toHaveBeenNthCalledWith(3, mockUser1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,3 +4,4 @@ export * from "./login-strategies/login-strategy.service";
|
|||
export * from "./user-decryption-options/user-decryption-options.service";
|
||||
export * from "./auth-request/auth-request.service";
|
||||
export * from "./register-route.service";
|
||||
export * from "./accounts/lock.service";
|
||||
|
|
|
@ -7,8 +7,6 @@ export class PermissionsApi extends BaseResponse {
|
|||
createNewCollections: boolean;
|
||||
editAnyCollection: boolean;
|
||||
deleteAnyCollection: boolean;
|
||||
editAssignedCollections: boolean;
|
||||
deleteAssignedCollections: boolean;
|
||||
manageCiphers: boolean;
|
||||
manageGroups: boolean;
|
||||
manageSso: boolean;
|
||||
|
@ -29,8 +27,6 @@ export class PermissionsApi extends BaseResponse {
|
|||
this.createNewCollections = this.getResponseProperty("CreateNewCollections");
|
||||
this.editAnyCollection = this.getResponseProperty("EditAnyCollection");
|
||||
this.deleteAnyCollection = this.getResponseProperty("DeleteAnyCollection");
|
||||
this.editAssignedCollections = this.getResponseProperty("EditAssignedCollections");
|
||||
this.deleteAssignedCollections = this.getResponseProperty("DeleteAssignedCollections");
|
||||
|
||||
this.manageCiphers = this.getResponseProperty("ManageCiphers");
|
||||
this.manageGroups = this.getResponseProperty("ManageGroups");
|
||||
|
|
|
@ -340,8 +340,6 @@ describe("KeyConnectorService", () => {
|
|||
createNewCollections: false,
|
||||
editAnyCollection: false,
|
||||
deleteAnyCollection: false,
|
||||
editAssignedCollections: false,
|
||||
deleteAssignedCollections: false,
|
||||
manageGroups: false,
|
||||
managePolicies: false,
|
||||
manageSso: false,
|
||||
|
|
|
@ -17,8 +17,10 @@ export class Fido2Utils {
|
|||
return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str));
|
||||
}
|
||||
|
||||
static bufferSourceToUint8Array(bufferSource: BufferSource) {
|
||||
if (Fido2Utils.isArrayBuffer(bufferSource)) {
|
||||
static bufferSourceToUint8Array(bufferSource: BufferSource): Uint8Array {
|
||||
if (bufferSource instanceof Uint8Array) {
|
||||
return bufferSource;
|
||||
} else if (Fido2Utils.isArrayBuffer(bufferSource)) {
|
||||
return new Uint8Array(bufferSource);
|
||||
} else {
|
||||
return new Uint8Array(bufferSource.buffer);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { GlobalState } from "./global-state";
|
|||
import { KeyDefinition } from "./key-definition";
|
||||
|
||||
/**
|
||||
* A provider for geting an implementation of global state scoped to the given key.
|
||||
* A provider for getting an implementation of global state scoped to the given key.
|
||||
*/
|
||||
export abstract class GlobalStateProvider {
|
||||
/**
|
||||
|
|
|
@ -57,6 +57,8 @@ export class SendService implements InternalSendServiceAbstraction {
|
|||
send.disabled = model.disabled;
|
||||
send.hideEmail = model.hideEmail;
|
||||
send.maxAccessCount = model.maxAccessCount;
|
||||
send.deletionDate = model.deletionDate;
|
||||
send.expirationDate = model.expirationDate;
|
||||
if (model.key == null) {
|
||||
const key = await this.keyGenerationService.createKeyWithPurpose(
|
||||
128,
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<input
|
||||
#input
|
||||
bitInput
|
||||
type="search"
|
||||
[type]="inputType"
|
||||
[id]="id"
|
||||
[placeholder]="placeholder ?? ('search' | i18n)"
|
||||
class="tw-pl-9"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { Component, ElementRef, Input, ViewChild } from "@angular/core";
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
|
||||
import { isBrowserSafariApi } from "@bitwarden/platform";
|
||||
|
||||
import { FocusableElement } from "../shared/focusable-element";
|
||||
|
||||
let nextId = 0;
|
||||
|
@ -28,6 +30,8 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
|
|||
|
||||
protected id = `search-id-${nextId++}`;
|
||||
protected searchText: string;
|
||||
// Use `type="text"` for Safari to improve rendering performance
|
||||
protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const);
|
||||
|
||||
@Input() disabled: boolean;
|
||||
@Input() placeholder: string;
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
"lib": ["es2020", "dom"],
|
||||
"paths": {
|
||||
"@bitwarden/common/*": ["../common/src/*"],
|
||||
"@bitwarden/angular/*": ["../angular/src/*"]
|
||||
"@bitwarden/angular/*": ["../angular/src/*"],
|
||||
"@bitwarden/platform": ["../platform/src"]
|
||||
}
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from "./services/browser-service";
|
|
@ -0,0 +1,41 @@
|
|||
import { isBrowserSafariApi } from "./browser-service";
|
||||
|
||||
describe("browser-service", () => {
|
||||
describe("isBrowserSafariApi", () => {
|
||||
it("returns true if browser is safari", () => {
|
||||
jest
|
||||
.spyOn(navigator, "userAgent", "get")
|
||||
.mockReturnValue(
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
|
||||
);
|
||||
|
||||
const result = isBrowserSafariApi();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false if browser is chrome", () => {
|
||||
jest
|
||||
.spyOn(navigator, "userAgent", "get")
|
||||
.mockReturnValue(
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
|
||||
);
|
||||
|
||||
const result = isBrowserSafariApi();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false if browser is firefox", () => {
|
||||
jest
|
||||
.spyOn(navigator, "userAgent", "get")
|
||||
.mockReturnValue(
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:130.0) Gecko/20100101 Firefox/130.0",
|
||||
);
|
||||
|
||||
const result = isBrowserSafariApi();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
export function isBrowserSafariApi(): boolean {
|
||||
return (
|
||||
navigator.userAgent.indexOf(" Safari/") !== -1 &&
|
||||
navigator.userAgent.indexOf(" Chrome/") === -1 &&
|
||||
navigator.userAgent.indexOf(" Chromium/") === -1
|
||||
);
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
import { DatePipe } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, FormGroup, FormControl, Validators } from "@angular/forms";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
|
||||
import { SendFormConfig } from "../../abstractions/send-form-config.service";
|
||||
import { SendFormContainer } from "../../send-form-container";
|
||||
|
||||
export type BaseSendDetailsForm = FormGroup<{
|
||||
name: FormControl<string>;
|
||||
selectedDeletionDatePreset: FormControl<string | number>;
|
||||
}>;
|
||||
|
||||
// Value = hours
|
||||
export enum DatePreset {
|
||||
OneHour = 1,
|
||||
OneDay = 24,
|
||||
TwoDays = 48,
|
||||
ThreeDays = 72,
|
||||
SevenDays = 168,
|
||||
FourteenDays = 336,
|
||||
ThirtyDays = 720,
|
||||
}
|
||||
|
||||
export interface DatePresetSelectOption {
|
||||
name: string;
|
||||
value: DatePreset | string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "base-send-details-behavior",
|
||||
template: "",
|
||||
})
|
||||
export class BaseSendDetailsComponent implements OnInit {
|
||||
@Input() config: SendFormConfig;
|
||||
@Input() originalSendView?: SendView;
|
||||
|
||||
sendDetailsForm: BaseSendDetailsForm;
|
||||
customDeletionDateOption: DatePresetSelectOption | null = null;
|
||||
datePresetOptions: DatePresetSelectOption[] = [];
|
||||
|
||||
constructor(
|
||||
protected sendFormContainer: SendFormContainer,
|
||||
protected formBuilder: FormBuilder,
|
||||
protected i18nService: I18nService,
|
||||
protected datePipe: DatePipe,
|
||||
) {
|
||||
this.sendDetailsForm = this.formBuilder.group({
|
||||
name: new FormControl("", Validators.required),
|
||||
selectedDeletionDatePreset: new FormControl(DatePreset.SevenDays || "", Validators.required),
|
||||
});
|
||||
|
||||
this.sendDetailsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
|
||||
this.sendFormContainer.patchSend((send) => {
|
||||
return Object.assign(send, {
|
||||
name: value.name,
|
||||
deletionDate: new Date(this.formattedDeletionDate),
|
||||
expirationDate: new Date(this.formattedDeletionDate),
|
||||
} as SendView);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.setupDeletionDatePresets();
|
||||
|
||||
if (this.originalSendView) {
|
||||
this.sendDetailsForm.patchValue({
|
||||
name: this.originalSendView.name,
|
||||
selectedDeletionDatePreset: this.originalSendView.deletionDate.toString(),
|
||||
});
|
||||
|
||||
if (this.originalSendView.deletionDate) {
|
||||
this.customDeletionDateOption = {
|
||||
name: this.datePipe.transform(this.originalSendView.deletionDate, "MM/dd/yyyy, hh:mm a"),
|
||||
value: this.originalSendView.deletionDate.toString(),
|
||||
};
|
||||
this.datePresetOptions.unshift(this.customDeletionDateOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupDeletionDatePresets() {
|
||||
const defaultSelections: DatePresetSelectOption[] = [
|
||||
{ name: this.i18nService.t("oneHour"), value: DatePreset.OneHour },
|
||||
{ name: this.i18nService.t("oneDay"), value: DatePreset.OneDay },
|
||||
{ name: this.i18nService.t("days", "2"), value: DatePreset.TwoDays },
|
||||
{ name: this.i18nService.t("days", "3"), value: DatePreset.ThreeDays },
|
||||
{ name: this.i18nService.t("days", "7"), value: DatePreset.SevenDays },
|
||||
{ name: this.i18nService.t("days", "14"), value: DatePreset.FourteenDays },
|
||||
{ name: this.i18nService.t("days", "30"), value: DatePreset.ThirtyDays },
|
||||
];
|
||||
|
||||
this.datePresetOptions = defaultSelections;
|
||||
}
|
||||
|
||||
get formattedDeletionDate(): string {
|
||||
const now = new Date();
|
||||
const selectedValue = this.sendDetailsForm.controls.selectedDeletionDatePreset.value;
|
||||
|
||||
if (typeof selectedValue === "string") {
|
||||
return selectedValue;
|
||||
}
|
||||
|
||||
const milliseconds = now.setTime(now.getTime() + (selectedValue as number) * 60 * 60 * 1000);
|
||||
return new Date(milliseconds).toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<bit-section [formGroup]="sendDetailsForm">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h5">{{ "sendDetails" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
|
||||
<bit-card>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="name" />
|
||||
</bit-form-field>
|
||||
|
||||
<tools-send-text-details
|
||||
*ngIf="config.sendType === TextSendType"
|
||||
[config]="config"
|
||||
[originalSendView]="originalSendView"
|
||||
[sendDetailsForm]="sendDetailsForm"
|
||||
></tools-send-text-details>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "deletionDate" | i18n }}</bit-label>
|
||||
<bit-select
|
||||
id="deletionDate"
|
||||
name="SelectedDeletionDatePreset"
|
||||
formControlName="selectedDeletionDatePreset"
|
||||
>
|
||||
<bit-option
|
||||
*ngFor="let o of datePresetOptions"
|
||||
[value]="o.value"
|
||||
[label]="o.name"
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
<bit-hint>{{ "deletionDateDescV2" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</bit-card>
|
||||
</bit-section>
|
|
@ -0,0 +1,59 @@
|
|||
import { CommonModule, DatePipe } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import {
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
CardComponent,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
CheckboxModule,
|
||||
SelectModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { SendFormContainer } from "../../send-form-container";
|
||||
|
||||
import { BaseSendDetailsComponent } from "./base-send-details.component";
|
||||
import { SendTextDetailsComponent } from "./send-text-details.component";
|
||||
|
||||
@Component({
|
||||
selector: "tools-send-details",
|
||||
templateUrl: "./send-details.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
JslibModule,
|
||||
CardComponent,
|
||||
FormFieldModule,
|
||||
ReactiveFormsModule,
|
||||
SendTextDetailsComponent,
|
||||
IconButtonModule,
|
||||
CheckboxModule,
|
||||
CommonModule,
|
||||
SelectModule,
|
||||
],
|
||||
})
|
||||
export class SendDetailsComponent extends BaseSendDetailsComponent implements OnInit {
|
||||
FileSendType = SendType.File;
|
||||
TextSendType = SendType.Text;
|
||||
|
||||
constructor(
|
||||
protected sendFormContainer: SendFormContainer,
|
||||
protected formBuilder: FormBuilder,
|
||||
protected i18nService: I18nService,
|
||||
protected datePipe: DatePipe,
|
||||
) {
|
||||
super(sendFormContainer, formBuilder, i18nService, datePipe);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<bit-section [formGroup]="sendTextDetailsForm">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "sendTypeTextToShare" | i18n }}</bit-label>
|
||||
<textarea bitInput id="text" rows="6" formControlName="text"></textarea>
|
||||
</bit-form-field>
|
||||
<bit-form-control>
|
||||
<input bitCheckbox type="checkbox" formControlName="hidden" />
|
||||
<bit-label>{{ "hideTextByDefault" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</bit-section>
|
|
@ -0,0 +1,82 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import {
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
Validators,
|
||||
ReactiveFormsModule,
|
||||
} from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { CheckboxModule, FormFieldModule, SectionComponent } from "@bitwarden/components";
|
||||
|
||||
import { SendFormConfig } from "../../abstractions/send-form-config.service";
|
||||
import { SendFormContainer } from "../../send-form-container";
|
||||
|
||||
import { BaseSendDetailsForm } from "./base-send-details.component";
|
||||
|
||||
type BaseSendTextDetailsForm = FormGroup<{
|
||||
text: FormControl<string>;
|
||||
hidden: FormControl<boolean>;
|
||||
}>;
|
||||
|
||||
export type SendTextDetailsForm = BaseSendTextDetailsForm & BaseSendDetailsForm;
|
||||
|
||||
@Component({
|
||||
selector: "tools-send-text-details",
|
||||
templateUrl: "./send-text-details.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CheckboxModule,
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
SectionComponent,
|
||||
],
|
||||
})
|
||||
export class SendTextDetailsComponent implements OnInit {
|
||||
@Input() config: SendFormConfig;
|
||||
@Input() originalSendView?: SendView;
|
||||
@Input() sendDetailsForm: BaseSendDetailsForm;
|
||||
|
||||
baseSendTextDetailsForm: BaseSendTextDetailsForm;
|
||||
sendTextDetailsForm: SendTextDetailsForm;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
protected sendFormContainer: SendFormContainer,
|
||||
) {
|
||||
this.baseSendTextDetailsForm = this.formBuilder.group({
|
||||
text: new FormControl("", Validators.required),
|
||||
hidden: new FormControl(false),
|
||||
});
|
||||
|
||||
this.sendTextDetailsForm = Object.assign(this.baseSendTextDetailsForm, this.sendDetailsForm);
|
||||
|
||||
this.sendFormContainer.registerChildForm("sendTextDetailsForm", this.sendTextDetailsForm);
|
||||
|
||||
this.sendTextDetailsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
|
||||
this.sendFormContainer.patchSend((send) => {
|
||||
return Object.assign(send, {
|
||||
text: {
|
||||
text: value.text,
|
||||
hidden: value.hidden,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.originalSendView) {
|
||||
this.baseSendTextDetailsForm.patchValue({
|
||||
text: this.originalSendView.text?.text || "",
|
||||
hidden: this.originalSendView.text?.hidden || false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
<form [id]="formId" [formGroup]="sendForm" [bitSubmit]="submit">
|
||||
<!-- TODO: Should we show a loading spinner here? Or emit a ready event for the container to handle loading state -->
|
||||
<ng-container *ngIf="!loading"> </ng-container>
|
||||
<ng-container *ngIf="!loading">
|
||||
<tools-send-details
|
||||
[config]="config"
|
||||
[originalSendView]="originalSendView"
|
||||
></tools-send-details>
|
||||
</ng-container>
|
||||
</form>
|
||||
|
|
|
@ -35,6 +35,8 @@ import { SendFormConfig } from "../abstractions/send-form-config.service";
|
|||
import { SendFormService } from "../abstractions/send-form.service";
|
||||
import { SendForm, SendFormContainer } from "../send-form-container";
|
||||
|
||||
import { SendDetailsComponent } from "./send-details/send-details.component";
|
||||
|
||||
@Component({
|
||||
selector: "tools-send-form",
|
||||
templateUrl: "./send-form.component.html",
|
||||
|
@ -55,6 +57,7 @@ import { SendForm, SendFormContainer } from "../send-form-container";
|
|||
ReactiveFormsModule,
|
||||
SelectModule,
|
||||
NgIf,
|
||||
SendDetailsComponent,
|
||||
],
|
||||
})
|
||||
export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, SendFormContainer {
|
||||
|
@ -131,12 +134,11 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send
|
|||
}
|
||||
|
||||
/**
|
||||
* Patches the updated send with the provided partial senbd. Used by child components to update the send
|
||||
* as their form values change.
|
||||
* @param send
|
||||
* Method to update the sendView with the new values. This method should be called by the child form components
|
||||
* @param updateFn - A function that takes the current sendView and returns the updated sendView
|
||||
*/
|
||||
patchSend(send: Partial<SendView>): void {
|
||||
this.updatedSendView = Object.assign(this.updatedSendView, send);
|
||||
patchSend(updateFn: (current: SendView) => SendView): void {
|
||||
this.updatedSendView = updateFn(this.updatedSendView);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
|
||||
import { SendFormConfig } from "./abstractions/send-form-config.service";
|
||||
import { SendDetailsComponent } from "./components/send-details/send-details.component";
|
||||
import { SendTextDetailsForm } from "./components/send-details/send-text-details.component";
|
||||
/**
|
||||
* The complete form for a send. Includes all the sub-forms from their respective section components.
|
||||
* TODO: Add additional form sections as they are implemented.
|
||||
*/
|
||||
export type SendForm = object;
|
||||
export type SendForm = {
|
||||
sendDetailsForm?: SendDetailsComponent["sendDetailsForm"];
|
||||
sendTextDetailsForm?: SendTextDetailsForm;
|
||||
};
|
||||
|
||||
/**
|
||||
* A container for the {@link SendForm} that allows for registration of child form groups and patching of the send
|
||||
|
@ -32,5 +37,5 @@ export abstract class SendFormContainer {
|
|||
group: Exclude<SendForm[K], undefined>,
|
||||
): void;
|
||||
|
||||
abstract patchSend(send: Partial<SendView>): void;
|
||||
abstract patchSend(updateFn: (current: SendView) => SendView): void;
|
||||
}
|
||||
|
|
|
@ -17,13 +17,8 @@ export class DefaultSendFormService implements SendFormService {
|
|||
return await send.decrypt();
|
||||
}
|
||||
|
||||
async saveSend(
|
||||
send: SendView,
|
||||
file: File | ArrayBuffer,
|
||||
config: SendFormConfig,
|
||||
): Promise<SendView> {
|
||||
async saveSend(send: SendView, file: File | ArrayBuffer, config: SendFormConfig) {
|
||||
const sendData = await this.sendService.encrypt(send, file, send.password, null);
|
||||
const savedSend = await this.sendApiService.save(sendData);
|
||||
return await savedSend.decrypt();
|
||||
return await this.sendApiService.save(sendData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,27 +30,28 @@
|
|||
<span slot="secondary">
|
||||
{{ "deletionDate" | i18n }}: {{ send.deletionDate | date: "mediumDate" }}
|
||||
</span>
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button
|
||||
type="button"
|
||||
(click)="copySendLink(send)"
|
||||
appA11yTitle="{{ 'copyLink' | i18n }} - {{ send.name }}"
|
||||
>
|
||||
<i class="bwi tw-text-lg bwi-clone"></i>
|
||||
</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button
|
||||
type="button"
|
||||
(click)="deleteSend(send)"
|
||||
appA11yTitle="{{ 'delete' | i18n }} - {{ send.name }}"
|
||||
>
|
||||
<i class="bwi tw-text-lg bwi-trash"></i>
|
||||
</button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</button>
|
||||
<ng-container slot="end">
|
||||
<bit-item-action>
|
||||
<button
|
||||
class="tw-p-1"
|
||||
bitIconButton="bwi-clone"
|
||||
size="small"
|
||||
type="button"
|
||||
(click)="copySendLink(send)"
|
||||
appA11yTitle="{{ 'copyLink' | i18n }} - {{ send.name }}"
|
||||
></button>
|
||||
</bit-item-action>
|
||||
<bit-item-action>
|
||||
<button
|
||||
bitIconButton="bwi-trash"
|
||||
size="small"
|
||||
type="button"
|
||||
(click)="deleteSend(send)"
|
||||
appA11yTitle="{{ 'delete' | i18n }} - {{ send.name }}"
|
||||
></button>
|
||||
</bit-item-action>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
</bit-item-group>
|
||||
</bit-section>
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
/**
|
||||
* TODO: PM-10727 - Rename and Refactor this service
|
||||
* This service is being used in both CipherForm and CipherView. Update this service to reflect that
|
||||
*/
|
||||
|
||||
/**
|
||||
* Service to capture TOTP secret from a client application.
|
||||
*/
|
||||
|
@ -11,5 +6,4 @@ export abstract class TotpCaptureService {
|
|||
* Captures a TOTP secret and returns it as a string. Returns null if no TOTP secret was found.
|
||||
*/
|
||||
abstract captureTotpSecret(): Promise<string | null>;
|
||||
abstract openAutofillNewTab(loginUri: string): void;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
[appCopyClick]="notes"
|
||||
showToast
|
||||
[valueLabel]="'note' | i18n"
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
[appA11yTitle]="'copyNotes' | i18n"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</bit-card>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
[appCopyClick]="login.launchUri"
|
||||
[valueLabel]="'website' | i18n"
|
||||
showToast
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
[appA11yTitle]="'copyWebsite' | i18n"
|
||||
data-testid="copy-website"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
|
|
@ -2,18 +2,17 @@ import { CommonModule } from "@angular/common";
|
|||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import {
|
||||
CardComponent,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
IconButtonModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { TotpCaptureService } from "../../cipher-form";
|
||||
|
||||
@Component({
|
||||
selector: "app-autofill-options-view",
|
||||
templateUrl: "autofill-options-view.component.html",
|
||||
|
@ -32,9 +31,9 @@ import { TotpCaptureService } from "../../cipher-form";
|
|||
export class AutofillOptionsViewComponent {
|
||||
@Input() loginUris: LoginUriView[];
|
||||
|
||||
constructor(private totpCaptureService: TotpCaptureService) {}
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async openWebsite(selectedUri: string) {
|
||||
await this.totpCaptureService.openAutofillNewTab(selectedUri);
|
||||
openWebsite(selectedUri: string) {
|
||||
this.platformUtilsService.launchUri(selectedUri);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
[appCopyClick]="card.number"
|
||||
showToast
|
||||
[valueLabel]="'number' | i18n"
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
[appA11yTitle]="'copyNumber' | i18n"
|
||||
data-testid="copy-number"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
@ -79,7 +79,7 @@
|
|||
[appCopyClick]="card.code"
|
||||
showToast
|
||||
[valueLabel]="'securityCode' | i18n"
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
[appA11yTitle]="'copySecurityCode' | i18n"
|
||||
data-testid="copy-code"
|
||||
(click)="logCardEvent(true, EventType.Cipher_ClientCopiedCardCode)"
|
||||
></button>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
class="tw-border-secondary-300 [&_bit-form-field:last-of-type]:tw-mb-0"
|
||||
*ngFor="let field of cipher.fields; let last = last"
|
||||
[ngClass]="{ 'tw-mb-4': !last }"
|
||||
data-testid="custom-field"
|
||||
>
|
||||
<bit-form-field *ngIf="field.type === fieldType.Text" [disableReadOnlyBorder]="last">
|
||||
<bit-label>{{ field.name }}</bit-label>
|
||||
|
@ -18,7 +19,8 @@
|
|||
[appCopyClick]="field.value"
|
||||
showToast
|
||||
[valueLabel]="field.name"
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
[appA11yTitle]="'copyCustomField' | i18n: field.name"
|
||||
data-testid="copy-custom-field"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="field.type === fieldType.Hidden" [disableReadOnlyBorder]="last">
|
||||
|
@ -38,7 +40,7 @@
|
|||
[appCopyClick]="field.value"
|
||||
showToast
|
||||
[valueLabel]="field.name"
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
[appA11yTitle]="'copyCustomField' | i18n: field.name"
|
||||
(click)="logCopyEvent()"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
[appCopyClick]="cipher.login.username"
|
||||
[valueLabel]="'username' | i18n"
|
||||
showToast
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
[appA11yTitle]="'copyUsername' | i18n"
|
||||
data-testid="copy-username"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
@ -64,7 +64,7 @@
|
|||
[appCopyClick]="cipher.login.password"
|
||||
[valueLabel]="'password' | i18n"
|
||||
showToast
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
[appA11yTitle]="'copyPassword' | i18n"
|
||||
data-testid="copy-password"
|
||||
(click)="logCopyEvent()"
|
||||
></button>
|
||||
|
@ -127,7 +127,7 @@
|
|||
[appCopyClick]="totpCodeCopyObj?.totpCode"
|
||||
[valueLabel]="'verificationCodeTotp' | i18n"
|
||||
showToast
|
||||
[appA11yTitle]="'copyValue' | i18n"
|
||||
[appA11yTitle]="'copyVerificationCode' | i18n"
|
||||
data-testid="copy-totp"
|
||||
[disabled]="!(isPremium$ | async)"
|
||||
class="disabled:tw-cursor-default"
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
[appCopyClick]="cipher.identity.fullName"
|
||||
showToast
|
||||
[valueLabel]="'name' | i18n"
|
||||
data-testid="copy-name"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="cipher.identity.username">
|
||||
|
@ -41,6 +42,7 @@
|
|||
[appCopyClick]="cipher.identity.company"
|
||||
showToast
|
||||
[valueLabel]="'company' | i18n"
|
||||
data-testid="copy-company"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</read-only-cipher-card>
|
||||
|
@ -70,6 +72,7 @@
|
|||
[appCopyClick]="cipher.identity.ssn"
|
||||
showToast
|
||||
[valueLabel]="'ssn' | i18n"
|
||||
data-testid="copy-ssn"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="cipher.identity.passportNumber">
|
||||
|
@ -96,6 +99,7 @@
|
|||
[appCopyClick]="cipher.identity.passportNumber"
|
||||
showToast
|
||||
[valueLabel]="'passportNumber' | i18n"
|
||||
data-testid="copy-passport"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="cipher.identity.licenseNumber">
|
||||
|
@ -109,6 +113,7 @@
|
|||
[appCopyClick]="cipher.identity.licenseNumber"
|
||||
showToast
|
||||
[valueLabel]="'licenseNumber' | i18n"
|
||||
data-testid="copy-license"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</read-only-cipher-card>
|
||||
|
@ -131,6 +136,7 @@
|
|||
[appCopyClick]="cipher.identity.email"
|
||||
showToast
|
||||
[valueLabel]="'email' | i18n"
|
||||
data-testid="copy-email"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="cipher.identity.phone">
|
||||
|
@ -144,6 +150,7 @@
|
|||
[appCopyClick]="cipher.identity.phone"
|
||||
showToast
|
||||
[valueLabel]="'phone' | i18n"
|
||||
data-testid="copy-phone"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="addressFields">
|
||||
|
@ -164,6 +171,7 @@
|
|||
[appCopyClick]="addressFields"
|
||||
showToast
|
||||
[valueLabel]="'address' | i18n"
|
||||
data-testid="copy-address"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</read-only-cipher-card>
|
||||
|
|
|
@ -124,8 +124,8 @@
|
|||
"babel-loader": "9.1.3",
|
||||
"base64-loader": "1.0.0",
|
||||
"browserslist": "4.23.2",
|
||||
"chromatic": "11.7.1",
|
||||
"concurrently": "8.2.2",
|
||||
"chromatic": "11.10.2",
|
||||
"concurrently": "9.0.1",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "7.1.2",
|
||||
|
@ -171,7 +171,7 @@
|
|||
"sass-loader": "16.0.1",
|
||||
"storybook": "8.2.9",
|
||||
"style-loader": "3.3.4",
|
||||
"tailwindcss": "3.4.10",
|
||||
"tailwindcss": "3.4.11",
|
||||
"ts-jest": "29.2.2",
|
||||
"ts-loader": "9.5.1",
|
||||
"tsconfig-paths-webpack-plugin": "4.1.0",
|
||||
|
@ -246,7 +246,7 @@
|
|||
},
|
||||
"apps/web": {
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2024.9.0"
|
||||
"version": "2024.9.1"
|
||||
},
|
||||
"libs/admin-console": {
|
||||
"name": "@bitwarden/admin-console",
|
||||
|
@ -13845,9 +13845,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/chromatic": {
|
||||
"version": "11.7.1",
|
||||
"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.7.1.tgz",
|
||||
"integrity": "sha512-LvgPimdQdnQB07ZDxLEC2KtxgYeqTw0X71GA7fi3zhgtKLxZcE+BSZ/5I9rrQp1V8ydmfElfw0ZwnUH4fVgUAQ==",
|
||||
"version": "11.10.2",
|
||||
"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.10.2.tgz",
|
||||
"integrity": "sha512-EbVlhmOLGdx9QRX3RMOTF3UzoyC1aaXNRjlzm1mc++2OI5+6C5Bzwt2ZUYJ3Jnf/pJa23q0y5Y3QEDcfRVqIbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
@ -14537,18 +14537,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
|
||||
"integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.0.1.tgz",
|
||||
"integrity": "sha512-wYKvCd/f54sTXJMSfV6Ln/B8UrfLBKOYa+lzc6CHay3Qek+LorVSBdMVfyewFhRbH0Rbabsk4D+3PL/VjQ5gzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"lodash": "^4.17.21",
|
||||
"rxjs": "^7.8.1",
|
||||
"shell-quote": "^1.8.1",
|
||||
"spawn-command": "0.0.2",
|
||||
"supports-color": "^8.1.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
"yargs": "^17.7.2"
|
||||
|
@ -14558,7 +14556,7 @@
|
|||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.13.0 || >=16.0.0"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
|
@ -15476,23 +15474,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/debounce-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz",
|
||||
|
@ -34665,12 +34646,6 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/spawn-command": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
|
||||
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/spdx-correct": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
||||
|
@ -35516,9 +35491,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
|
||||
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
|
||||
"version": "3.4.11",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz",
|
||||
"integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
@ -86,8 +86,8 @@
|
|||
"babel-loader": "9.1.3",
|
||||
"base64-loader": "1.0.0",
|
||||
"browserslist": "4.23.2",
|
||||
"chromatic": "11.7.1",
|
||||
"concurrently": "8.2.2",
|
||||
"chromatic": "11.10.2",
|
||||
"concurrently": "9.0.1",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "7.1.2",
|
||||
|
@ -133,7 +133,7 @@
|
|||
"sass-loader": "16.0.1",
|
||||
"storybook": "8.2.9",
|
||||
"style-loader": "3.3.4",
|
||||
"tailwindcss": "3.4.10",
|
||||
"tailwindcss": "3.4.11",
|
||||
"ts-jest": "29.2.2",
|
||||
"ts-loader": "9.5.1",
|
||||
"tsconfig-paths-webpack-plugin": "4.1.0",
|
||||
|
|
Loading…
Reference in New Issue