mirror of
https://github.com/traefik/traefik.git
synced 2025-11-28 14:11:17 +01:00
Restore remote Upgrade to Hub button web component
This commit is contained in:
parent
77b1282570
commit
a01c73d506
@ -49,7 +49,7 @@
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^14.2.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@traefiklabs/faency": "11.1.4",
|
||||
"@traefiklabs/faency": "12.0.4",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/react": "^18.2.0",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,6 +1,4 @@
|
||||
import { Box, Button, Flex, TextField } from '@traefiklabs/faency'
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { InputHandle } from '@traefiklabs/faency/dist/components/Input'
|
||||
import { Box, Button, Flex, TextField, InputHandle } from '@traefiklabs/faency'
|
||||
import { isUndefined, omitBy } from 'lodash'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { FiSearch, FiXCircle } from 'react-icons/fi'
|
||||
|
||||
131
webui/src/hooks/use-hub-upgrade-button.spec.tsx
Normal file
131
webui/src/hooks/use-hub-upgrade-button.spec.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import useHubUpgradeButton from './use-hub-upgrade-button'
|
||||
|
||||
import { VersionContext } from 'contexts/version'
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
vi.mock('utils/workers/scriptVerification')
|
||||
|
||||
const mockVerifySignature = vi.mocked(verifySignature)
|
||||
|
||||
const createWrapper = (showHubButton: boolean) => {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<VersionContext.Provider value={{ showHubButton, version: '1.0.0' }}>{children}</VersionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useHubUpgradeButton Hook', () => {
|
||||
let originalCreateObjectURL: typeof URL.createObjectURL
|
||||
let originalRevokeObjectURL: typeof URL.revokeObjectURL
|
||||
const mockBlobUrl = 'blob:http://localhost:3000/mock-blob-url'
|
||||
|
||||
beforeEach(() => {
|
||||
originalCreateObjectURL = URL.createObjectURL
|
||||
originalRevokeObjectURL = URL.revokeObjectURL
|
||||
URL.createObjectURL = vi.fn(() => mockBlobUrl)
|
||||
URL.revokeObjectURL = vi.fn()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
URL.createObjectURL = originalCreateObjectURL
|
||||
URL.revokeObjectURL = originalRevokeObjectURL
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should not verify script when showHubButton is false', async () => {
|
||||
renderHook(() => useHubUpgradeButton(), {
|
||||
wrapper: createWrapper(false),
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVerifySignature).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should verify script and create blob URL when showHubButton is true and verification succeeds', async () => {
|
||||
const mockScriptContent = new ArrayBuffer(8)
|
||||
mockVerifySignature.mockResolvedValue({
|
||||
verified: true,
|
||||
scriptContent: mockScriptContent,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useHubUpgradeButton(), {
|
||||
wrapper: createWrapper(true),
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVerifySignature).toHaveBeenCalledWith(
|
||||
'https://traefik.github.io/traefiklabs-hub-button-app/main-v1.js',
|
||||
'https://traefik.github.io/traefiklabs-hub-button-app/main-v1.js.sig',
|
||||
'MCowBQYDK2VwAyEAY0OZFFE5kSuqYK6/UprTL5RmvQ+8dpPTGMCw1MiO/Gs=',
|
||||
)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.signatureVerified).toBe(true)
|
||||
})
|
||||
|
||||
expect(result.current.scriptBlobUrl).toBe(mockBlobUrl)
|
||||
expect(URL.createObjectURL).toHaveBeenCalledWith(expect.any(Blob))
|
||||
})
|
||||
|
||||
it('should set signatureVerified to false when verification fails', async () => {
|
||||
mockVerifySignature.mockResolvedValue({
|
||||
verified: false,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useHubUpgradeButton(), {
|
||||
wrapper: createWrapper(true),
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVerifySignature).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.signatureVerified).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.scriptBlobUrl).toBeNull()
|
||||
expect(URL.createObjectURL).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle verification errors gracefully', async () => {
|
||||
mockVerifySignature.mockRejectedValue(new Error('Verification failed'))
|
||||
|
||||
const { result } = renderHook(() => useHubUpgradeButton(), {
|
||||
wrapper: createWrapper(true),
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockVerifySignature).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.signatureVerified).toBe(false)
|
||||
})
|
||||
|
||||
expect(result.current.scriptBlobUrl).toBeNull()
|
||||
})
|
||||
|
||||
it('should create blob with correct MIME type', async () => {
|
||||
const mockScriptContent = new ArrayBuffer(8)
|
||||
mockVerifySignature.mockResolvedValue({
|
||||
verified: true,
|
||||
scriptContent: mockScriptContent,
|
||||
})
|
||||
|
||||
renderHook(() => useHubUpgradeButton(), {
|
||||
wrapper: createWrapper(true),
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(URL.createObjectURL).toHaveBeenCalled()
|
||||
})
|
||||
const blobCall = vi.mocked(URL.createObjectURL).mock.calls[0][0] as Blob
|
||||
expect(blobCall).toBeInstanceOf(Blob)
|
||||
expect(blobCall.type).toBe('application/javascript')
|
||||
})
|
||||
})
|
||||
61
webui/src/hooks/use-hub-upgrade-button.tsx
Normal file
61
webui/src/hooks/use-hub-upgrade-button.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
|
||||
import { VersionContext } from 'contexts/version'
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
const HUB_BUTTON_URL = 'https://traefik.github.io/traefiklabs-hub-button-app/main-v1.js'
|
||||
const PUBLIC_KEY = 'MCowBQYDK2VwAyEAY0OZFFE5kSuqYK6/UprTL5RmvQ+8dpPTGMCw1MiO/Gs='
|
||||
|
||||
const useHubUpgradeButton = () => {
|
||||
const [signatureVerified, setSignatureVerified] = useState(false)
|
||||
const [scriptBlobUrl, setScriptBlobUrl] = useState<string | null>(null)
|
||||
const [isCustomElementDefined, setIsCustomElementDefined] = useState(false)
|
||||
|
||||
const { showHubButton } = useContext(VersionContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (showHubButton) {
|
||||
if (customElements.get('hub-button-app')) {
|
||||
setSignatureVerified(true)
|
||||
setIsCustomElementDefined(true)
|
||||
return
|
||||
}
|
||||
|
||||
const verifyAndLoadScript = async () => {
|
||||
try {
|
||||
const { verified, scriptContent: content } = await verifySignature(
|
||||
HUB_BUTTON_URL,
|
||||
`${HUB_BUTTON_URL}.sig`,
|
||||
PUBLIC_KEY,
|
||||
)
|
||||
if (!verified || !content) {
|
||||
setSignatureVerified(false)
|
||||
} else {
|
||||
const blob = new Blob([content], { type: 'application/javascript' })
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
|
||||
setScriptBlobUrl(blobUrl)
|
||||
setSignatureVerified(true)
|
||||
}
|
||||
} catch {
|
||||
setSignatureVerified(false)
|
||||
}
|
||||
}
|
||||
|
||||
verifyAndLoadScript()
|
||||
|
||||
return () => {
|
||||
setScriptBlobUrl((currentUrl) => {
|
||||
if (currentUrl) {
|
||||
URL.revokeObjectURL(currentUrl)
|
||||
}
|
||||
return null
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [showHubButton])
|
||||
|
||||
return { signatureVerified, scriptBlobUrl, isCustomElementDefined }
|
||||
}
|
||||
|
||||
export default useHubUpgradeButton
|
||||
@ -1,8 +1,27 @@
|
||||
import { waitFor } from '@testing-library/react'
|
||||
|
||||
import { SideNav, TopNav } from './Navigation'
|
||||
|
||||
import useHubUpgradeButton from 'hooks/use-hub-upgrade-button'
|
||||
import { renderWithProviders } from 'utils/test'
|
||||
|
||||
vi.mock('hooks/use-hub-upgrade-button')
|
||||
|
||||
const mockUseHubUpgradeButton = vi.mocked(useHubUpgradeButton)
|
||||
|
||||
describe('Navigation', () => {
|
||||
beforeEach(() => {
|
||||
mockUseHubUpgradeButton.mockReturnValue({
|
||||
signatureVerified: false,
|
||||
scriptBlobUrl: null,
|
||||
isCustomElementDefined: false,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render the side navigation bar', async () => {
|
||||
const { container } = renderWithProviders(<SideNav isExpanded={false} onSidePanelToggle={() => {}} />)
|
||||
|
||||
@ -18,4 +37,60 @@ describe('Navigation', () => {
|
||||
expect(container.innerHTML).toContain('theme-switcher')
|
||||
expect(container.innerHTML).toContain('help-menu')
|
||||
})
|
||||
|
||||
describe('hub-button-app rendering', () => {
|
||||
it('should NOT render hub-button-app when signatureVerified is false', async () => {
|
||||
mockUseHubUpgradeButton.mockReturnValue({
|
||||
signatureVerified: false,
|
||||
scriptBlobUrl: null,
|
||||
isCustomElementDefined: false,
|
||||
})
|
||||
|
||||
const { container } = renderWithProviders(<TopNav />)
|
||||
|
||||
const hubButtonApp = container.querySelector('hub-button-app')
|
||||
expect(hubButtonApp).toBeNull()
|
||||
})
|
||||
|
||||
it('should NOT render hub-button-app when scriptBlobUrl is null', async () => {
|
||||
mockUseHubUpgradeButton.mockReturnValue({
|
||||
signatureVerified: true,
|
||||
scriptBlobUrl: null,
|
||||
isCustomElementDefined: false,
|
||||
})
|
||||
|
||||
const { container } = renderWithProviders(<TopNav />)
|
||||
|
||||
const hubButtonApp = container.querySelector('hub-button-app')
|
||||
expect(hubButtonApp).toBeNull()
|
||||
})
|
||||
|
||||
it('should render hub-button-app when signatureVerified is true and scriptBlobUrl exists', async () => {
|
||||
mockUseHubUpgradeButton.mockReturnValue({
|
||||
signatureVerified: true,
|
||||
scriptBlobUrl: 'blob:http://localhost:3000/mock-blob-url',
|
||||
isCustomElementDefined: false,
|
||||
})
|
||||
|
||||
const { container } = renderWithProviders(<TopNav />)
|
||||
|
||||
await waitFor(() => {
|
||||
const hubButtonApp = container.querySelector('hub-button-app')
|
||||
expect(hubButtonApp).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('should NOT render hub-button-app when noHubButton prop is true', async () => {
|
||||
mockUseHubUpgradeButton.mockReturnValue({
|
||||
signatureVerified: true,
|
||||
scriptBlobUrl: 'blob:http://localhost:3000/mock-blob-url',
|
||||
isCustomElementDefined: false,
|
||||
})
|
||||
|
||||
const { container } = renderWithProviders(<TopNav noHubButton={true} />)
|
||||
|
||||
const hubButtonApp = container.querySelector('hub-button-app')
|
||||
expect(hubButtonApp).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -21,6 +21,7 @@ import {
|
||||
VisuallyHidden,
|
||||
} from '@traefiklabs/faency'
|
||||
import { useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { BsChevronDoubleRight, BsChevronDoubleLeft } from 'react-icons/bs'
|
||||
import { FiBookOpen, FiGithub, FiHelpCircle } from 'react-icons/fi'
|
||||
import { matchPath, useHref } from 'react-router'
|
||||
@ -36,6 +37,7 @@ import { PluginsIcon } from 'components/icons/PluginsIcon'
|
||||
import ThemeSwitcher from 'components/ThemeSwitcher'
|
||||
import TooltipText from 'components/TooltipText'
|
||||
import { VersionContext } from 'contexts/version'
|
||||
import useHubUpgradeButton from 'hooks/use-hub-upgrade-button'
|
||||
import useTotals from 'hooks/use-overview-totals'
|
||||
import { useIsDarkMode } from 'hooks/use-theme'
|
||||
import ApimDemoNavMenu from 'pages/hub-demo/HubDemoNav'
|
||||
@ -293,8 +295,7 @@ export const SideNav = ({
|
||||
}
|
||||
|
||||
export const TopNav = ({ css, noHubButton = false }: { css?: CSS; noHubButton?: boolean }) => {
|
||||
const [hasHubButtonComponent, setHasHubButtonComponent] = useState(false)
|
||||
const { showHubButton, version } = useContext(VersionContext)
|
||||
const { version } = useContext(VersionContext)
|
||||
const isDarkMode = useIsDarkMode()
|
||||
|
||||
const parsedVersion = useMemo(() => {
|
||||
@ -308,91 +309,73 @@ export const TopNav = ({ css, noHubButton = false }: { css?: CSS; noHubButton?:
|
||||
return matches ? 'v' + matches[1] : 'master'
|
||||
}, [version])
|
||||
|
||||
useEffect(() => {
|
||||
if (!showHubButton) {
|
||||
setHasHubButtonComponent(false)
|
||||
return
|
||||
}
|
||||
const { signatureVerified, scriptBlobUrl, isCustomElementDefined } = useHubUpgradeButton()
|
||||
|
||||
if (customElements.get('hub-button-app')) {
|
||||
setHasHubButtonComponent(true)
|
||||
return
|
||||
}
|
||||
|
||||
const scripts: HTMLScriptElement[] = []
|
||||
const createScript = (scriptSrc: string): HTMLScriptElement => {
|
||||
const script = document.createElement('script')
|
||||
script.src = scriptSrc
|
||||
script.async = true
|
||||
script.onload = () => {
|
||||
setHasHubButtonComponent(customElements.get('hub-button-app') !== undefined)
|
||||
}
|
||||
scripts.push(script)
|
||||
return script
|
||||
}
|
||||
|
||||
// Source: https://github.com/traefik/traefiklabs-hub-button-app
|
||||
document.head.appendChild(createScript('traefiklabs-hub-button-app/main-v1.js'))
|
||||
|
||||
return () => {
|
||||
// Remove the scripts on unmount.
|
||||
scripts.forEach((script) => {
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [showHubButton])
|
||||
const displayUpgradeToHubButton = useMemo(
|
||||
() => !noHubButton && signatureVerified && (!!scriptBlobUrl || isCustomElementDefined),
|
||||
[isCustomElementDefined, noHubButton, scriptBlobUrl, signatureVerified],
|
||||
)
|
||||
|
||||
return (
|
||||
<Flex as="nav" role="navigation" justify="end" align="center" css={{ gap: '$2', mb: '$6', ...css }}>
|
||||
{!noHubButton && hasHubButtonComponent && (
|
||||
<Box css={{ fontFamily: '$rubik', fontWeight: '500 !important' }}>
|
||||
<hub-button-app
|
||||
key={`dark-mode-${isDarkMode}`}
|
||||
style={{ backgroundColor: isDarkMode ? DARK_PRIMARY_COLOR : LIGHT_PRIMARY_COLOR, fontWeight: 'inherit' }}
|
||||
<>
|
||||
{displayUpgradeToHubButton && (
|
||||
<Helmet>
|
||||
<meta
|
||||
httpEquiv="Content-Security-Policy"
|
||||
content="script-src 'self' blob: 'unsafe-inline'; object-src 'none'; base-uri 'self';"
|
||||
/>
|
||||
</Box>
|
||||
<script src={scriptBlobUrl as string} type="module"></script>
|
||||
</Helmet>
|
||||
)}
|
||||
<ThemeSwitcher />
|
||||
<Flex as="nav" role="navigation" justify="end" align="center" css={{ gap: '$2', mb: '$6', ...css }}>
|
||||
{displayUpgradeToHubButton && (
|
||||
<Box css={{ fontFamily: '$rubik', fontWeight: '500 !important' }}>
|
||||
<hub-button-app
|
||||
key={`dark-mode-${isDarkMode}`}
|
||||
style={{ backgroundColor: isDarkMode ? DARK_PRIMARY_COLOR : LIGHT_PRIMARY_COLOR, fontWeight: 'inherit' }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<ThemeSwitcher />
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button ghost variant="secondary" css={{ px: '$2', boxShadow: 'none' }} data-testid="help-menu">
|
||||
<FiHelpCircle size={20} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent align="end" css={{ zIndex: 9999 }}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem css={{ height: '$6', cursor: 'pointer' }}>
|
||||
<Link
|
||||
href={`https://doc.traefik.io/traefik/${parsedVersion}`}
|
||||
target="_blank"
|
||||
css={{ textDecoration: 'none', '&:hover': { textDecoration: 'none' } }}
|
||||
>
|
||||
<Flex align="center" gap={2}>
|
||||
<FiBookOpen size={20} />
|
||||
<Text>Documentation</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem css={{ height: '$6', cursor: 'pointer' }}>
|
||||
<Link
|
||||
href="https://github.com/traefik/traefik/"
|
||||
target="_blank"
|
||||
css={{ textDecoration: 'none', '&:hover': { textDecoration: 'none' } }}
|
||||
>
|
||||
<Flex align="center" gap={2}>
|
||||
<FiGithub size={20} />
|
||||
<Text>Github Repository</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
</Flex>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button ghost variant="secondary" css={{ px: '$2', boxShadow: 'none' }} data-testid="help-menu">
|
||||
<FiHelpCircle size={20} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent align="end" css={{ zIndex: 9999 }}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem css={{ height: '$6', cursor: 'pointer' }}>
|
||||
<Link
|
||||
href={`https://doc.traefik.io/traefik/${parsedVersion}`}
|
||||
target="_blank"
|
||||
css={{ textDecoration: 'none', '&:hover': { textDecoration: 'none' } }}
|
||||
>
|
||||
<Flex align="center" gap={2}>
|
||||
<FiBookOpen size={20} />
|
||||
<Text>Documentation</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem css={{ height: '$6', cursor: 'pointer' }}>
|
||||
<Link
|
||||
href="https://github.com/traefik/traefik/"
|
||||
target="_blank"
|
||||
css={{ textDecoration: 'none', '&:hover': { textDecoration: 'none' } }}
|
||||
>
|
||||
<Flex align="center" gap={2}>
|
||||
<FiGithub size={20} />
|
||||
<Text>Github Repository</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenu>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { waitFor } from '@testing-library/react'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import { PUBLIC_KEY } from './constants'
|
||||
import HubDashboard, { resetCache } from './HubDashboard'
|
||||
import verifySignature from './workers/scriptVerification'
|
||||
|
||||
import { renderWithProviders } from 'utils/test'
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
vi.mock('./workers/scriptVerification', () => ({
|
||||
vi.mock('utils/workers/scriptVerification', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
@ -34,7 +35,6 @@ describe('HubDashboard demo', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock URL.createObjectURL
|
||||
mockCreateObjectURL = vi.fn(() => 'blob:mock-url')
|
||||
globalThis.URL.createObjectURL = mockCreateObjectURL
|
||||
})
|
||||
@ -45,7 +45,6 @@ describe('HubDashboard demo', () => {
|
||||
|
||||
describe('without cache', () => {
|
||||
beforeEach(() => {
|
||||
// Reset cache before each test suites
|
||||
resetCache()
|
||||
})
|
||||
|
||||
@ -130,6 +129,7 @@ describe('HubDashboard demo', () => {
|
||||
expect(mockVerifyScriptSignature).toHaveBeenCalledWith(
|
||||
'https://assets.traefik.io/hub-ui-demo.js',
|
||||
'https://assets.traefik.io/hub-ui-demo.js.sig',
|
||||
PUBLIC_KEY,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,7 +3,9 @@ import { useMemo, useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import verifySignature from './workers/scriptVerification'
|
||||
import verifySignature from '../../utils/workers/scriptVerification'
|
||||
|
||||
import { PUBLIC_KEY } from './constants'
|
||||
|
||||
import { SpinnerLoader } from 'components/SpinnerLoader'
|
||||
import { useIsDarkMode } from 'hooks/use-theme'
|
||||
@ -42,7 +44,7 @@ const HubDashboard = ({ path }: { path: string }) => {
|
||||
setVerificationInProgress(true)
|
||||
|
||||
try {
|
||||
const { verified, scriptContent: content } = await verifySignature(SCRIPT_URL, `${SCRIPT_URL}.sig`)
|
||||
const { verified, scriptContent: content } = await verifySignature(SCRIPT_URL, `${SCRIPT_URL}.sig`, PUBLIC_KEY)
|
||||
|
||||
if (!verified || !content) {
|
||||
setScriptError(true)
|
||||
|
||||
1
webui/src/pages/hub-demo/constants.ts
Normal file
1
webui/src/pages/hub-demo/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const PUBLIC_KEY = 'MCowBQYDK2VwAyEAWMBZ0pMBaL/s8gNXxpAPCIQ8bxjnuz6bQFwGYvjXDfg='
|
||||
@ -3,9 +3,10 @@ import { ReactNode } from 'react'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import { useHubDemo } from './use-hub-demo'
|
||||
import verifySignature from './workers/scriptVerification'
|
||||
|
||||
vi.mock('./workers/scriptVerification', () => ({
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
vi.mock('utils/workers/scriptVerification', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import { RouteObject } from 'react-router-dom'
|
||||
|
||||
import { PUBLIC_KEY } from './constants'
|
||||
|
||||
import HubDashboard from 'pages/hub-demo/HubDashboard'
|
||||
import { ApiIcon, DashboardIcon, GatewayIcon, PortalIcon } from 'pages/hub-demo/icons'
|
||||
import verifySignature from 'pages/hub-demo/workers/scriptVerification'
|
||||
import verifySignature from 'utils/workers/scriptVerification'
|
||||
|
||||
const ROUTES_MANIFEST_URL = 'https://traefik.github.io/hub-ui-demo-app/config/routes.json'
|
||||
|
||||
@ -20,7 +22,11 @@ const useHubDemoRoutesManifest = (): HubDemo.Manifest | null => {
|
||||
useEffect(() => {
|
||||
const fetchManifest = async () => {
|
||||
try {
|
||||
const { verified, scriptContent } = await verifySignature(ROUTES_MANIFEST_URL, `${ROUTES_MANIFEST_URL}.sig`)
|
||||
const { verified, scriptContent } = await verifySignature(
|
||||
ROUTES_MANIFEST_URL,
|
||||
`${ROUTES_MANIFEST_URL}.sig`,
|
||||
PUBLIC_KEY,
|
||||
)
|
||||
|
||||
if (!verified || !scriptContent) {
|
||||
setManifest(null)
|
||||
|
||||
@ -2,6 +2,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import verifySignature from './scriptVerification'
|
||||
|
||||
const SCRIPT_PATH = 'https://example.com/script.js'
|
||||
const MOCK_PUBLIC_KEY = 'MCowBQYDK2VwAyEAWH71OHphISjNK3mizCR/BawiDxc6IXT1vFHpBcxSIA0='
|
||||
class MockWorker {
|
||||
onmessage: ((event: MessageEvent) => void) | null = null
|
||||
onerror: ((error: ErrorEvent) => void) | null = null
|
||||
@ -46,17 +48,14 @@ describe('verifySignature', () => {
|
||||
})
|
||||
|
||||
it('should return true when verification succeeds', async () => {
|
||||
const scriptPath = 'https://example.com/script.js'
|
||||
const signaturePath = 'https://example.com/script.js.sig'
|
||||
|
||||
const promise = verifySignature(scriptPath, signaturePath)
|
||||
const promise = verifySignature(SCRIPT_PATH, `${SCRIPT_PATH}.sig`, MOCK_PUBLIC_KEY)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
expect(mockWorkerInstance.postMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
scriptUrl: scriptPath,
|
||||
signatureUrl: signaturePath,
|
||||
scriptUrl: SCRIPT_PATH,
|
||||
signatureUrl: `${SCRIPT_PATH}.sig`,
|
||||
requestId: expect.any(String),
|
||||
}),
|
||||
)
|
||||
@ -76,12 +75,9 @@ describe('verifySignature', () => {
|
||||
})
|
||||
|
||||
it('should return false when verification fails', async () => {
|
||||
const scriptPath = 'https://example.com/script.js'
|
||||
const signaturePath = 'https://example.com/script.js.sig'
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const promise = verifySignature(scriptPath, signaturePath)
|
||||
const promise = verifySignature(SCRIPT_PATH, `${SCRIPT_PATH}.sig`, MOCK_PUBLIC_KEY)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
@ -101,16 +97,12 @@ describe('verifySignature', () => {
|
||||
})
|
||||
|
||||
it('should return false when worker throws an error', async () => {
|
||||
const scriptPath = 'https://example.com/script.js'
|
||||
const signaturePath = 'https://example.com/script.js.sig'
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const promise = verifySignature(scriptPath, signaturePath)
|
||||
const promise = verifySignature(SCRIPT_PATH, `${SCRIPT_PATH}.sig`, MOCK_PUBLIC_KEY)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
// Simulate worker onerror event
|
||||
const error = new Error('Worker crashed')
|
||||
mockWorkerInstance.simulateError(error)
|
||||
|
||||
@ -3,12 +3,10 @@ export interface VerificationResult {
|
||||
scriptContent?: ArrayBuffer
|
||||
}
|
||||
|
||||
const PUBLIC_KEY = 'MCowBQYDK2VwAyEAWMBZ0pMBaL/s8gNXxpAPCIQ8bxjnuz6bQFwGYvjXDfg='
|
||||
|
||||
async function verifySignature(
|
||||
contentPath: string,
|
||||
signaturePath: string,
|
||||
publicKey: string = PUBLIC_KEY,
|
||||
publicKey: string,
|
||||
): Promise<VerificationResult> {
|
||||
return new Promise((resolve) => {
|
||||
const requestId = Math.random().toString(36).substring(2)
|
||||
1445
webui/yarn.lock
1445
webui/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user