Various security fixes

This commit is contained in:
Pavlenex 2026-03-17 10:00:20 +01:00
parent 49f2c87fed
commit ddedbf5a19
7 changed files with 42 additions and 27 deletions

View File

@ -18,7 +18,9 @@
<meta name="twitter:description" content="Pick your role and start contributing to BTCPay Server - developer, tester, designer, or evangelist." /> <meta name="twitter:description" content="Pick your role and start contributing to BTCPay Server - developer, tester, designer, or evangelist." />
<!-- CSP - GitHub Pages ignores _headers, so enforce via meta tag --> <!-- CSP - GitHub Pages ignores _headers, so enforce via meta tag -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://avatars.githubusercontent.com https://img.youtube.com; connect-src 'self'; frame-src https://www.youtube.com" /> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://avatars.githubusercontent.com https://img.youtube.com; connect-src 'self'; frame-src https://www.youtube.com" />
<script src="/theme-init.js"></script>
<!-- Favicon --> <!-- Favicon -->
<meta name="theme-color" content="#51b13e" /> <meta name="theme-color" content="#51b13e" />
@ -37,15 +39,6 @@
<!-- Preconnect for YouTube thumbnails --> <!-- Preconnect for YouTube thumbnails -->
<link rel="preconnect" href="https://img.youtube.com" /> <link rel="preconnect" href="https://img.youtube.com" />
<!-- Theme flash prevention — runs before React hydrates -->
<script>
(function() {
var t = localStorage.getItem('theme');
if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -3,7 +3,7 @@
# (Cloudflare Pages, Netlify). A CSP meta tag in index.html provides # (Cloudflare Pages, Netlify). A CSP meta tag in index.html provides
# baseline protection regardless of hosting platform. # baseline protection regardless of hosting platform.
/* /*
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://avatars.githubusercontent.com https://img.youtube.com; connect-src 'self'; frame-src https://www.youtube.com; frame-ancestors 'none' Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://avatars.githubusercontent.com https://img.youtube.com; connect-src 'self'; frame-src https://www.youtube.com; frame-ancestors 'none'
X-Frame-Options: DENY X-Frame-Options: DENY
X-Content-Type-Options: nosniff X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin Referrer-Policy: strict-origin-when-cross-origin

14
public/theme-init.js Normal file
View File

@ -0,0 +1,14 @@
(function () {
try {
var theme = localStorage.getItem('theme')
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
if (theme === 'dark' || (!theme && prefersDark)) {
document.documentElement.classList.add('dark')
}
} catch {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark')
}
}
})()

View File

@ -52,12 +52,12 @@ export default function App() {
} }
const { heading, sub } = ROLE_SECTION_TITLE[selectedRole] const { heading, sub } = ROLE_SECTION_TITLE[selectedRole]
const issues =
function getIssues() { selectedRole === 'tester'
if (selectedRole === 'tester') return testerFiltered ? testerFiltered
if (selectedRole === 'writer') return writerFiltered : selectedRole === 'writer'
return filtered ? writerFiltered
} : filtered
return ( return (
<> <>
@ -89,7 +89,8 @@ export default function App() {
</div> </div>
<IssueGrid <IssueGrid
issues={getIssues()} key={`${selectedRole}:${filters.query}`}
issues={issues}
loading={status === 'loading'} loading={status === 'loading'}
onIssueClick={handleIssueClick} onIssueClick={handleIssueClick}
onIssueHover={preloadIssueModal} onIssueHover={preloadIssueModal}

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import { useState } from 'react'
import type React from 'react' import type React from 'react'
import { Loader2 } from 'lucide-react' import { Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@ -17,8 +17,6 @@ interface IssueGridProps {
export default function IssueGrid({ issues, loading, onIssueClick, onIssueHover }: IssueGridProps) { export default function IssueGrid({ issues, loading, onIssueClick, onIssueHover }: IssueGridProps) {
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
useEffect(() => { setPage(1) }, [issues])
const visible = issues.slice(0, page * PAGE_SIZE) const visible = issues.slice(0, page * PAGE_SIZE)
const hasMore = visible.length < issues.length const hasMore = visible.length < issues.length

View File

@ -379,7 +379,7 @@ function StepRow({ step, index, role }: { step: StepDef; index: number; role: Ro
</div> </div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 items-center"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 items-center">
<YoutubeThumbnail video={role === 'writer' ? WRITER_VIDEO : DEV_VIDEO} /> <YoutubeThumbnail video={DEV_VIDEO} />
<DevToolRows /> <DevToolRows />
</div> </div>
</div> </div>

View File

@ -6,17 +6,26 @@ type Status = 'idle' | 'loading' | 'success' | 'error'
export function useIssues(filters: FilterState) { export function useIssues(filters: FilterState) {
const [data, setData] = useState<IssuesData | null>(null) const [data, setData] = useState<IssuesData | null>(null)
const [status, setStatus] = useState<Status>('idle') const [status, setStatus] = useState<Status>('loading')
useEffect(() => { useEffect(() => {
setStatus('loading') const controller = new AbortController()
fetch('/data/issues.json')
fetch('/data/issues.json', { signal: controller.signal })
.then((r) => { .then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`) if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.json() as Promise<IssuesData> return r.json() as Promise<IssuesData>
}) })
.then((d) => { setData(d); setStatus('success') }) .then((d) => {
.catch((err) => { console.error('[useIssues] failed to load issues.json:', err); setStatus('error') }) setData(d)
setStatus('success')
})
.catch((err: unknown) => {
if (controller.signal.aborted) return
console.error('[useIssues] failed to load issues.json:', err)
setStatus('error')
})
return () => controller.abort()
}, []) }, [])
const filtered = useMemo( const filtered = useMemo(