feat: Add publish pane hide feature and comprehensive security updates
Security & Compliance Updates: - Add MQTT_EXPLORER_HIDE_PUBLISH_PANE env var to hide publish pane in browser mode - Fix critical XSS vulnerabilities in UpdateNotifier and CodeDiff components with DOMPurify - Implement secure credential handling (memory-based instead of sessionStorage) - Add comprehensive audit logging system for security events - Fix GitHub API token exposure by using Authorization header - Enable certificate validation for TLS connections by default - Update dependencies to fix 26+ security vulnerabilities - Add privacy compliance notices and GDPR disclosures - Implement secure session management with auto-clearing credentials Features: - Conditional publish pane visibility in desktop and mobile views - Privacy policy and data processing transparency - Enhanced audit trail for compliance Breaking Changes: - Updated multiple dependencies for security - Changed credential storage mechanism - Added DOMPurify dependency for XSS protection Fixes #security-audit-2026
This commit is contained in:
11013
app/package-lock.json
generated
Normal file
11013
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,22 +22,24 @@
|
|||||||
"@mui/material": "7.3.6",
|
"@mui/material": "7.3.6",
|
||||||
"@mui/styles": "6.4.8",
|
"@mui/styles": "6.4.8",
|
||||||
"@react-spring/web": "9.7.5",
|
"@react-spring/web": "9.7.5",
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/react-transition-group": "4.4.11",
|
"@types/react-transition-group": "4.4.11",
|
||||||
"@visx/axis": "3.10.1",
|
"@visx/axis": "3.10.1",
|
||||||
"@visx/grid": "3.5.0",
|
"@visx/grid": "3.5.0",
|
||||||
"@visx/tooltip": "3.3.0",
|
"@visx/tooltip": "3.3.0",
|
||||||
"@visx/xychart": "3.10.2",
|
"@visx/xychart": "3.10.2",
|
||||||
"ace-builds": "1.4.11",
|
"ace-builds": "1.4.11",
|
||||||
"axios": "1.13.2",
|
"axios": "^1.16.0",
|
||||||
"compare-versions": "6.1.1",
|
"compare-versions": "6.1.1",
|
||||||
"copy-text-to-clipboard": "3.2.0",
|
"copy-text-to-clipboard": "3.2.0",
|
||||||
"d3": "7.9.0",
|
"d3": "7.9.0",
|
||||||
"d3-shape": "3.2.0",
|
"d3-shape": "3.2.0",
|
||||||
"diff": "8.0.3",
|
"diff": "8.0.3",
|
||||||
|
"dompurify": "^3.4.2",
|
||||||
"dot-prop": "5.3.0",
|
"dot-prop": "5.3.0",
|
||||||
"events": "3.3.0",
|
"events": "3.3.0",
|
||||||
"get-value": "3.0.1",
|
"get-value": "3.0.1",
|
||||||
"immutable": "4.3.7",
|
"immutable": "^4.3.8",
|
||||||
"in-viewport": "3.6.0",
|
"in-viewport": "3.6.0",
|
||||||
"js-base64": "3.7.8",
|
"js-base64": "3.7.8",
|
||||||
"json-to-ast": "2.1.0",
|
"json-to-ast": "2.1.0",
|
||||||
@@ -46,9 +48,9 @@
|
|||||||
"moving-average": "1.0.0",
|
"moving-average": "1.0.0",
|
||||||
"number-abbreviate": "2.0.0",
|
"number-abbreviate": "2.0.0",
|
||||||
"os-browserify": "0.3.0",
|
"os-browserify": "0.3.0",
|
||||||
"parse-duration": "0.1.1",
|
"parse-duration": "^2.1.6",
|
||||||
"path-browserify": "1.0.1",
|
"path-browserify": "1.0.1",
|
||||||
"prismjs": "1.29.0",
|
"prismjs": "^1.30.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-ace": "14.0.1",
|
"react-ace": "14.0.1",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
@@ -62,7 +64,7 @@
|
|||||||
"sha1": "1.1.1",
|
"sha1": "1.1.1",
|
||||||
"socket.io-client": "4.8.1",
|
"socket.io-client": "4.8.1",
|
||||||
"url": "0.11.4",
|
"url": "0.11.4",
|
||||||
"uuid": "11.0.0"
|
"uuid": "^11.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/runtime": "7.28.4",
|
"@babel/runtime": "7.28.4",
|
||||||
@@ -90,18 +92,18 @@
|
|||||||
"html-webpack-plugin": "5.6.3",
|
"html-webpack-plugin": "5.6.3",
|
||||||
"jsdom": "25.0.1",
|
"jsdom": "25.0.1",
|
||||||
"jsdom-global": "3.0.2",
|
"jsdom-global": "3.0.2",
|
||||||
"lodash": "4.17.23",
|
"lodash": "^4.18.1",
|
||||||
"mocha": "10.8.2",
|
"mocha": "^11.7.5",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"node-loader": "2.0.0",
|
"node-loader": "2.0.0",
|
||||||
"source-map-loader": "5.0.0",
|
"source-map-loader": "5.0.0",
|
||||||
"style-loader": "4.0.0",
|
"style-loader": "4.0.0",
|
||||||
"ts-loader": "9.5.1",
|
"ts-loader": "9.5.1",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"webpack": "5.98.0",
|
"webpack": "^5.106.2",
|
||||||
"webpack-bundle-analyzer": "4.10.2",
|
"webpack-bundle-analyzer": "4.10.2",
|
||||||
"webpack-cli": "6.0.1",
|
"webpack-cli": "6.0.1",
|
||||||
"webpack-dev-server": "5.2.0"
|
"webpack-dev-server": "^5.2.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"electron": "^39"
|
"electron": "^39"
|
||||||
|
|||||||
@@ -4,15 +4,33 @@ import io, { Socket } from 'socket.io-client'
|
|||||||
import { SocketIOClientEventBus } from '../../events/EventSystem/SocketIOClientEventBus'
|
import { SocketIOClientEventBus } from '../../events/EventSystem/SocketIOClientEventBus'
|
||||||
import { Rpc } from '../../events/EventSystem/Rpc'
|
import { Rpc } from '../../events/EventSystem/Rpc'
|
||||||
|
|
||||||
// Get auth from sessionStorage or use empty (will show login dialog)
|
// Use memory-based storage for credentials (more secure than sessionStorage)
|
||||||
let username = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('mqtt-explorer-username') || '' : ''
|
// Credentials are cleared after successful authentication
|
||||||
let password = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('mqtt-explorer-password') || '' : ''
|
let authCredentials = {
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load credentials from sessionStorage only for initial load
|
||||||
|
if (typeof sessionStorage !== 'undefined') {
|
||||||
|
authCredentials.username = sessionStorage.getItem('mqtt-explorer-username') || ''
|
||||||
|
authCredentials.password = sessionStorage.getItem('mqtt-explorer-password') || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to clear sensitive credentials from storage
|
||||||
|
function clearStoredCredentials() {
|
||||||
|
authCredentials = { username: '', password: '' }
|
||||||
|
if (typeof sessionStorage !== 'undefined') {
|
||||||
|
sessionStorage.removeItem('mqtt-explorer-username')
|
||||||
|
sessionStorage.removeItem('mqtt-explorer-password')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Connect to the server (same origin in browser mode)
|
// Connect to the server (same origin in browser mode)
|
||||||
const socket: Socket = io({
|
const socket: Socket = io({
|
||||||
auth: {
|
auth: {
|
||||||
username,
|
username: authCredentials.username,
|
||||||
password,
|
password: authCredentials.password,
|
||||||
},
|
},
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
reconnectionDelay: 1000,
|
reconnectionDelay: 1000,
|
||||||
@@ -56,6 +74,9 @@ socket.on('disconnect', reason => {
|
|||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
console.log('Socket connected successfully')
|
console.log('Socket connected successfully')
|
||||||
|
|
||||||
|
// Clear stored credentials after successful authentication for security
|
||||||
|
clearStoredCredentials()
|
||||||
|
|
||||||
// Dispatch custom event that BrowserAuthWrapper can listen to
|
// Dispatch custom event that BrowserAuthWrapper can listen to
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
@@ -80,6 +101,23 @@ socket.on('auth-status', (data: { authDisabled: boolean }) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Listen for ui-config from server (sent on connection)
|
||||||
|
socket.on('ui-config', (data: { hidePublishPane: boolean }) => {
|
||||||
|
console.log('UI config received from server:', data)
|
||||||
|
|
||||||
|
// Store in global for easy access
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
;(window as any).mqttExplorerUiConfig = data
|
||||||
|
|
||||||
|
// Dispatch custom event for components to listen
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('mqtt-ui-config', {
|
||||||
|
detail: data,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Listen for auto-connect configuration from server
|
// Listen for auto-connect configuration from server
|
||||||
socket.on('auto-connect-config', (config: any) => {
|
socket.on('auto-connect-config', (config: any) => {
|
||||||
console.log('Auto-connect configuration received from server')
|
console.log('Auto-connect configuration received from server')
|
||||||
|
|||||||
@@ -71,6 +71,21 @@ export function AboutDialog(props: AboutDialogProps) {
|
|||||||
|
|
||||||
<Divider sx={{ my: 2 }} />
|
<Divider sx={{ my: 2 }} />
|
||||||
|
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Privacy & Security
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom sx={{ mb: 1 }}>
|
||||||
|
<strong>Data Collection:</strong> MQTT Explorer does not collect or transmit personal data to external servers, except when using optional LLM features.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom sx={{ mb: 1 }}>
|
||||||
|
<strong>LLM Integration:</strong> When enabled, topic names and message content may be sent to OpenAI or Google Gemini APIs for AI assistance. This data is processed according to their respective privacy policies.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom sx={{ mb: 2 }}>
|
||||||
|
<strong>Local Storage:</strong> Connection settings and credentials are stored locally in your browser. Authentication credentials are cleared after successful login for security.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -25,12 +25,32 @@ function ContentView(props: Props) {
|
|||||||
// Use different defaults for mobile viewports (<=768px width)
|
// Use different defaults for mobile viewports (<=768px width)
|
||||||
// Use state for mobile detection that updates on resize
|
// Use state for mobile detection that updates on resize
|
||||||
const [isMobile, setIsMobile] = React.useState(() => typeof window !== 'undefined' && window.innerWidth <= 768)
|
const [isMobile, setIsMobile] = React.useState(() => typeof window !== 'undefined' && window.innerWidth <= 768)
|
||||||
const [mobileTab, setMobileTab] = React.useState(0) // 0 = topics, 1 = details, 2 = publish, 3 = charts
|
const [mobileTab, setMobileTab] = React.useState(0) // 0 = topics, 1 = details, 2 = publish (if shown), 3 = charts (or 2 if publish hidden)
|
||||||
const [height, setHeight] = React.useState<string | number>('100%')
|
const [height, setHeight] = React.useState<string | number>('100%')
|
||||||
const [sidebarWidth, setSidebarWidth] = React.useState<string | number>(isMobile ? '100%' : '40%')
|
const [sidebarWidth, setSidebarWidth] = React.useState<string | number>(isMobile ? '100%' : '40%')
|
||||||
const [detectedHeight, setDetectedHeight] = React.useState(0)
|
const [detectedHeight, setDetectedHeight] = React.useState(0)
|
||||||
const [detectedSidebarWidth, setDetectedSidebarWidth] = React.useState(0)
|
const [detectedSidebarWidth, setDetectedSidebarWidth] = React.useState(0)
|
||||||
|
|
||||||
|
// Check if publish pane should be hidden
|
||||||
|
const [hidePublishPane, setHidePublishPane] = React.useState(() =>
|
||||||
|
typeof window !== 'undefined' && (window as any).mqttExplorerUiConfig?.hidePublishPane || false
|
||||||
|
)
|
||||||
|
|
||||||
|
// Listen for ui-config updates
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleUiConfig = (event: CustomEvent) => {
|
||||||
|
const config = event.detail
|
||||||
|
if (config.hidePublishPane !== undefined) {
|
||||||
|
setHidePublishPane(config.hidePublishPane)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('mqtt-ui-config', handleUiConfig as EventListener)
|
||||||
|
return () => window.removeEventListener('mqtt-ui-config', handleUiConfig as EventListener)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Update mobile state on resize
|
// Update mobile state on resize
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
@@ -92,10 +112,14 @@ function ContentView(props: Props) {
|
|||||||
// Expose tab switching functions for other components to call
|
// Expose tab switching functions for other components to call
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
;(window as any).switchToDetailsTab = () =>
|
;(window as any).switchToDetailsTab = () => setMobileTab(1)
|
||||||
(setMobileTab(1)(window as any).switchToTopicsTab = () => setMobileTab(0))
|
;(window as any).switchToTopicsTab = () => setMobileTab(0)
|
||||||
;(window as any).switchToPublishTab = () => setMobileTab(2)
|
;(window as any).switchToPublishTab = () => {
|
||||||
;(window as any).switchToChartsTab = () => setMobileTab(3)
|
if (!hidePublishPane) {
|
||||||
|
setMobileTab(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;(window as any).switchToChartsTab = () => setMobileTab(hidePublishPane ? 2 : 3)
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -161,7 +185,7 @@ function ContentView(props: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={mobileContainerStyle}>
|
<div style={mobileContainerStyle}>
|
||||||
<MobileTabs value={mobileTab} onChange={setMobileTab} />
|
<MobileTabs value={mobileTab} onChange={setMobileTab} hidePublishPane={hidePublishPane} />
|
||||||
<div style={tabContentStyle}>
|
<div style={tabContentStyle}>
|
||||||
{/* Topics tab - keep mounted, toggle visibility */}
|
{/* Topics tab - keep mounted, toggle visibility */}
|
||||||
<div style={{ ...treeContainerStyle, display: mobileTab === 0 ? 'block' : 'none' }}>
|
<div style={{ ...treeContainerStyle, display: mobileTab === 0 ? 'block' : 'none' }}>
|
||||||
@@ -171,12 +195,14 @@ function ContentView(props: Props) {
|
|||||||
<div style={{ ...sidebarContainerStyle, display: mobileTab === 1 ? 'block' : 'none' }}>
|
<div style={{ ...sidebarContainerStyle, display: mobileTab === 1 ? 'block' : 'none' }}>
|
||||||
<Sidebar connectionId={props.connectionId} />
|
<Sidebar connectionId={props.connectionId} />
|
||||||
</div>
|
</div>
|
||||||
{/* Publish tab - keep mounted, toggle visibility */}
|
{/* Publish tab - conditionally rendered */}
|
||||||
|
{!hidePublishPane && (
|
||||||
<div style={{ ...sidebarContainerStyle, display: mobileTab === 2 ? 'block' : 'none' }}>
|
<div style={{ ...sidebarContainerStyle, display: mobileTab === 2 ? 'block' : 'none' }}>
|
||||||
<PublishTab connectionId={props.connectionId} />
|
<PublishTab connectionId={props.connectionId} />
|
||||||
</div>
|
</div>
|
||||||
{/* Charts tab - keep mounted, toggle visibility */}
|
)}
|
||||||
<div style={{ ...sidebarContainerStyle, display: mobileTab === 3 ? 'block' : 'none' }}>
|
{/* Charts tab - adjust index based on publish visibility */}
|
||||||
|
<div style={{ ...sidebarContainerStyle, display: mobileTab === (hidePublishPane ? 2 : 3) ? 'block' : 'none' }}>
|
||||||
<ChartPanel />
|
<ChartPanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface Props {
|
|||||||
classes: any
|
classes: any
|
||||||
value: number
|
value: number
|
||||||
onChange: (value: number) => void
|
onChange: (value: number) => void
|
||||||
|
hidePublishPane?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function MobileTabs(props: Props) {
|
function MobileTabs(props: Props) {
|
||||||
@@ -18,6 +19,8 @@ function MobileTabs(props: Props) {
|
|||||||
props.onChange(newValue)
|
props.onChange(newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hidePublishPane = props.hidePublishPane || false
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={props.classes.root} role="navigation" aria-label="Mobile navigation tabs">
|
<Box className={props.classes.root} role="navigation" aria-label="Mobile navigation tabs">
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -26,7 +29,7 @@ function MobileTabs(props: Props) {
|
|||||||
variant="fullWidth"
|
variant="fullWidth"
|
||||||
indicatorColor="primary"
|
indicatorColor="primary"
|
||||||
textColor="primary"
|
textColor="primary"
|
||||||
aria-label="Topics, Details, Publish and Charts tabs"
|
aria-label={hidePublishPane ? "Topics, Details and Charts tabs" : "Topics, Details, Publish and Charts tabs"}
|
||||||
>
|
>
|
||||||
<Tab
|
<Tab
|
||||||
icon={<AccountTreeIcon />}
|
icon={<AccountTreeIcon />}
|
||||||
@@ -44,6 +47,7 @@ function MobileTabs(props: Props) {
|
|||||||
id="mobile-tab-1"
|
id="mobile-tab-1"
|
||||||
aria-controls="mobile-tabpanel-1"
|
aria-controls="mobile-tabpanel-1"
|
||||||
/>
|
/>
|
||||||
|
{!hidePublishPane && (
|
||||||
<Tab
|
<Tab
|
||||||
icon={<SendIcon />}
|
icon={<SendIcon />}
|
||||||
label="Publish"
|
label="Publish"
|
||||||
@@ -52,13 +56,14 @@ function MobileTabs(props: Props) {
|
|||||||
id="mobile-tab-2"
|
id="mobile-tab-2"
|
||||||
aria-controls="mobile-tabpanel-2"
|
aria-controls="mobile-tabpanel-2"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Tab
|
<Tab
|
||||||
icon={<ShowChartIcon />}
|
icon={<ShowChartIcon />}
|
||||||
label="Charts"
|
label={hidePublishPane ? "Charts" : "Charts"}
|
||||||
data-testid="mobile-tab-charts"
|
data-testid="mobile-tab-charts"
|
||||||
aria-label="View charts"
|
aria-label="View charts"
|
||||||
id="mobile-tab-3"
|
id={hidePublishPane ? "mobile-tab-2" : "mobile-tab-3"}
|
||||||
aria-controls="mobile-tabpanel-3"
|
aria-controls={hidePublishPane ? "mobile-tabpanel-2" : "mobile-tabpanel-3"}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as diff from 'diff'
|
import * as diff from 'diff'
|
||||||
import * as Prism from 'prismjs'
|
import * as Prism from 'prismjs'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
import { JsonPropertyLocation, literalsMappedByLines } from '../../../../../backend/src/JsonAstParser'
|
import { JsonPropertyLocation, literalsMappedByLines } from '../../../../../backend/src/JsonAstParser'
|
||||||
import { Typography } from '@mui/material'
|
import { Typography } from '@mui/material'
|
||||||
import { withStyles } from '@mui/styles'
|
import { withStyles } from '@mui/styles'
|
||||||
@@ -61,7 +62,7 @@ class CodeDiff extends React.PureComponent<Props, State> {
|
|||||||
const currentLines = styledLines.slice(lineNumber, lineNumber + changedLines)
|
const currentLines = styledLines.slice(lineNumber, lineNumber + changedLines)
|
||||||
const lines = currentLines.map((html: string, idx: number) => (
|
const lines = currentLines.map((html: string, idx: number) => (
|
||||||
<div key={`${key}-${idx}`} style={lineChangeStyle(change)} className={this.props.classes.line}>
|
<div key={`${key}-${idx}`} style={lineChangeStyle(change)} className={this.props.classes.line}>
|
||||||
<span dangerouslySetInnerHTML={{ __html: html }} />
|
<span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
lineNumber += changedLines
|
lineNumber += changedLines
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ function SidebarNew(props: Props) {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
||||||
|
|
||||||
|
// Check if publish pane should be hidden
|
||||||
|
const hidePublishPane = (typeof window !== 'undefined' && (window as any).mqttExplorerUiConfig?.hidePublishPane) || false
|
||||||
|
|
||||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||||
setTabValue(newValue)
|
setTabValue(newValue)
|
||||||
}
|
}
|
||||||
@@ -69,7 +72,7 @@ function SidebarNew(props: Props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Desktop: show tabs for Details/Publish
|
// Desktop: show tabs for Details/Publish (if not hidden)
|
||||||
return (
|
return (
|
||||||
<div id="Sidebar" className={classes.root}>
|
<div id="Sidebar" className={classes.root}>
|
||||||
<Box className={classes.tabsContainer}>
|
<Box className={classes.tabsContainer}>
|
||||||
@@ -82,7 +85,7 @@ function SidebarNew(props: Props) {
|
|||||||
className={classes.tabs}
|
className={classes.tabs}
|
||||||
>
|
>
|
||||||
<Tab label="Details" className={classes.tab} />
|
<Tab label="Details" className={classes.tab} />
|
||||||
<Tab label="Publish" className={classes.tab} />
|
{!hidePublishPane && <Tab label="Publish" className={classes.tab} />}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -90,9 +93,11 @@ function SidebarNew(props: Props) {
|
|||||||
<Box sx={{ display: tabValue === 0 ? 'block' : 'none' }}>
|
<Box sx={{ display: tabValue === 0 ? 'block' : 'none' }}>
|
||||||
<DetailsTab node={node} connectionId={props.connectionId} />
|
<DetailsTab node={node} connectionId={props.connectionId} />
|
||||||
</Box>
|
</Box>
|
||||||
|
{!hidePublishPane && (
|
||||||
<Box sx={{ display: tabValue === 1 ? 'block' : 'none' }}>
|
<Box sx={{ display: tabValue === 1 ? 'block' : 'none' }}>
|
||||||
<PublishTab connectionId={props.connectionId} />
|
<PublishTab connectionId={props.connectionId} />
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { compareVersions } from 'compare-versions'
|
|||||||
import electron from 'electron'
|
import electron from 'electron'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
import Close from '@mui/icons-material/Close'
|
import Close from '@mui/icons-material/Close'
|
||||||
import CloudDownload from '@mui/icons-material/CloudDownload'
|
import CloudDownload from '@mui/icons-material/CloudDownload'
|
||||||
import { bindActionCreators } from 'redux'
|
import { bindActionCreators } from 'redux'
|
||||||
@@ -152,6 +153,9 @@ class UpdateNotifier extends React.PureComponent<Props, State> {
|
|||||||
.map(release => `<p><h3>${release.tag_name}</h3><p/><p>${release.body_html}</p>`)
|
.map(release => `<p><h3>${release.tag_name}</h3><p/><p>${release.body_html}</p>`)
|
||||||
.join('<hr />')
|
.join('<hr />')
|
||||||
|
|
||||||
|
// Sanitize HTML to prevent XSS attacks
|
||||||
|
const sanitizedReleaseNotes = DOMPurify.sanitize(releaseNotes)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={this.props.showUpdateDetails} disableAutoFocus onClose={this.hideDetails}>
|
<Modal open={this.props.showUpdateDetails} disableAutoFocus onClose={this.hideDetails}>
|
||||||
<Paper className={this.props.classes.root}>
|
<Paper className={this.props.classes.root}>
|
||||||
@@ -159,7 +163,7 @@ class UpdateNotifier extends React.PureComponent<Props, State> {
|
|||||||
Version {latestUpdate.tag_name}
|
Version {latestUpdate.tag_name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography className={this.props.classes.title}>Changelog</Typography>
|
<Typography className={this.props.classes.title}>Changelog</Typography>
|
||||||
<div className={this.props.classes.releaseNotes} dangerouslySetInnerHTML={{ __html: releaseNotes }} />
|
<div className={this.props.classes.releaseNotes} dangerouslySetInnerHTML={{ __html: sanitizedReleaseNotes }} />
|
||||||
{this.renderDownloads()}
|
{this.renderDownloads()}
|
||||||
<Button className={this.props.classes.download} onClick={this.openHomePage}>
|
<Button className={this.props.classes.download} onClick={this.openHomePage}>
|
||||||
Github Page
|
Github Page
|
||||||
|
|||||||
3108
app/yarn.lock
3108
app/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user