From 30402542476ba7412c1ca96111081e6c3a7fbf07 Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Mon, 9 Nov 2026 16:55:47 +0800 Subject: [PATCH 01/14] fix(web-app): start folder-size scans and key copy on plain HTTP Embedded Console is usually opened at http://:9001, which is not a secure context. There crypto.randomUUID and navigator.clipboard are undefined, so "Calculate folder sizes" threw before sending its request and the decryption-key copy button did nothing, both without feedback. Use the shared makeid() task-ID helper, and copy the key with copy-to-clipboard (already shipped through react-copy-to-clipboard), keeping the field's single overlay button. Add a static guard against secure-context-only APIs and exercise the real Inspect dialog's copy on a plain-HTTP origin. Signed-off-by: Feng Ruohang Co-Authored-By: Claude Opus 5.5 --- web-app/e2e/secure-context.unit.ts | 36 +++++++++++++++++++ web-app/e2e/visual-polish.unit.ts | 17 +++++++++ web-app/package.json | 1 + .../Console/ObjectBrowser/usePrefixUsage.ts | 6 ++-- .../src/screens/Console/Tools/KeyRevealer.tsx | 5 ++- web-app/yarn.lock | 1 + 6 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 web-app/e2e/secure-context.unit.ts diff --git a/web-app/e2e/secure-context.unit.ts b/web-app/e2e/secure-context.unit.ts new file mode 100644 index 0000000000..a2078563db --- /dev/null +++ b/web-app/e2e/secure-context.unit.ts @@ -0,0 +1,36 @@ +// Copyright (c) 2026 Pigsty +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Embedded Console is commonly served over plain HTTP (http://:9001), where +// browsers omit secure-context APIs. A call to one of them throws before the +// surrounding request starts, so features silently do nothing. + +import { expect, test } from "@playwright/test"; +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const roots = ["../src/", "../packages/mds/src/"].map((relative) => + fileURLToPath(new URL(relative, import.meta.url)), +); +const sources = (dir: string): string[] => + readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const path = join(dir, entry.name); + if (entry.isDirectory()) return sources(path); + return /\.(ts|tsx)$/.test(entry.name) ? [path] : []; + }); + +test("source avoids APIs that plain-HTTP pages do not expose", () => { + const offenders = roots + .flatMap(sources) + .filter((path) => + /crypto\.randomUUID|crypto\.subtle|navigator\.clipboard/.test( + readFileSync(path, "utf8"), + ), + ); + expect(offenders).toEqual([]); +}); diff --git a/web-app/e2e/visual-polish.unit.ts b/web-app/e2e/visual-polish.unit.ts index de56c3435b..5e8ea9b02c 100644 --- a/web-app/e2e/visual-polish.unit.ts +++ b/web-app/e2e/visual-polish.unit.ts @@ -192,6 +192,23 @@ test("actual Inspect guards pristine submit and preserves encoded download and d page.getByRole("dialog", { name: "Inspect Decryption Key" }), ).toBeVisible(); await expect(page.locator("#inspect-dec-key")).toHaveValue("test-key"); + // polish.test is plain HTTP, where the async Clipboard API is absent. The + // copy action must still work, and stay the field's only overlay button. + const copyKey = page.locator( + ".textBoxContainer:has(#inspect-dec-key) .overlayAction button", + ); + await expect(copyKey).toHaveCount(1); + const pageErrors: string[] = []; + page.on("pageerror", (error) => pageErrors.push(error.message)); + await copyKey.click(); + expect(pageErrors).toEqual([]); + await expect(page.locator("#inspect-dec-key")).toHaveAttribute( + "type", + "password", + ); + // Leave the button so its tooltip does not take the first Escape. + await page.mouse.move(0, 0); + await page.locator("#inspect-dec-key").focus(); await page.keyboard.press("Escape"); await expect(page.locator("#inspect_volume")).toHaveValue(""); await expect(page.locator('[aria-invalid="true"]')).toHaveCount(0); diff --git a/web-app/package.json b/web-app/package.json index c0c2a35d35..249c3d134d 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -7,6 +7,7 @@ "type": "module", "dependencies": { "@reduxjs/toolkit": "^1.9.7", + "copy-to-clipboard": "^3.3.3", "kbar": "^0.1.0-beta.48", "lodash": "^4.18.1", "luxon": "^3.7.2", diff --git a/web-app/src/screens/Console/ObjectBrowser/usePrefixUsage.ts b/web-app/src/screens/Console/ObjectBrowser/usePrefixUsage.ts index 898f7592c7..cb72b0f89c 100644 --- a/web-app/src/screens/Console/ObjectBrowser/usePrefixUsage.ts +++ b/web-app/src/screens/Console/ObjectBrowser/usePrefixUsage.ts @@ -5,7 +5,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { api } from "api"; import type { PrefixUsageFrame } from "api/consoleApi"; import { readPrefixUsageStream } from "./prefixUsage"; -import { registerTaskControl, removeTrace } from "./transferManager"; +import { makeid, registerTaskControl, removeTrace } from "./transferManager"; import { useTaskSession } from "../Common/ObjectManager/useTaskSession"; type Scan = { @@ -40,7 +40,9 @@ export function usePrefixUsage( const request = ++serial.current; const controller = new AbortController(); active.current = controller; - const task = `prefix-usage-${crypto.randomUUID()}`; + // randomUUID exists only in secure contexts; embedded Console is + // often served over plain HTTP, so use the shared task-ID helper. + const task = `prefix-usage-${makeid(16)}`; registerTaskControl(task, { cancel: () => controller.abort() }); const current = () => serial.current === request && isCurrentSession(); setScan({ owner, prefix: target, running: true }); diff --git a/web-app/src/screens/Console/Tools/KeyRevealer.tsx b/web-app/src/screens/Console/Tools/KeyRevealer.tsx index 8bae2413c3..37bc4f2ae2 100644 --- a/web-app/src/screens/Console/Tools/KeyRevealer.tsx +++ b/web-app/src/screens/Console/Tools/KeyRevealer.tsx @@ -15,6 +15,7 @@ // along with this program. If not, see . import React, { useState } from "react"; +import copy from "copy-to-clipboard"; import { Button, CopyIcon, InputBox, Box, breakPoints } from "mds"; import { useT } from "i18n"; @@ -44,7 +45,9 @@ const KeyRevealer = ({ value }: { value: string }) => { value={value} overlayIcon={} readOnly={true} - overlayAction={() => navigator.clipboard.writeText(value)} + // The async Clipboard API exists only in secure contexts; Console is + // often served over plain HTTP, so use the execCommand fallback. + overlayAction={() => copy(value)} />