Various security fixes
This commit is contained in:
parent
49f2c87fed
commit
ddedbf5a19
13
index.html
13
index.html
@ -18,7 +18,9 @@
|
||||
<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 -->
|
||||
<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 -->
|
||||
<meta name="theme-color" content="#51b13e" />
|
||||
@ -37,15 +39,6 @@
|
||||
<!-- Preconnect for YouTube thumbnails -->
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
# (Cloudflare Pages, Netlify). A CSP meta tag in index.html provides
|
||||
# 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-Content-Type-Options: nosniff
|
||||
Referrer-Policy: strict-origin-when-cross-origin
|
||||
|
||||
14
public/theme-init.js
Normal file
14
public/theme-init.js
Normal 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')
|
||||
}
|
||||
}
|
||||
})()
|
||||
15
src/App.tsx
15
src/App.tsx
@ -52,12 +52,12 @@ export default function App() {
|
||||
}
|
||||
|
||||
const { heading, sub } = ROLE_SECTION_TITLE[selectedRole]
|
||||
|
||||
function getIssues() {
|
||||
if (selectedRole === 'tester') return testerFiltered
|
||||
if (selectedRole === 'writer') return writerFiltered
|
||||
return filtered
|
||||
}
|
||||
const issues =
|
||||
selectedRole === 'tester'
|
||||
? testerFiltered
|
||||
: selectedRole === 'writer'
|
||||
? writerFiltered
|
||||
: filtered
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -89,7 +89,8 @@ export default function App() {
|
||||
</div>
|
||||
|
||||
<IssueGrid
|
||||
issues={getIssues()}
|
||||
key={`${selectedRole}:${filters.query}`}
|
||||
issues={issues}
|
||||
loading={status === 'loading'}
|
||||
onIssueClick={handleIssueClick}
|
||||
onIssueHover={preloadIssueModal}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import type React from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@ -17,8 +17,6 @@ interface IssueGridProps {
|
||||
export default function IssueGrid({ issues, loading, onIssueClick, onIssueHover }: IssueGridProps) {
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
useEffect(() => { setPage(1) }, [issues])
|
||||
|
||||
const visible = issues.slice(0, page * PAGE_SIZE)
|
||||
const hasMore = visible.length < issues.length
|
||||
|
||||
|
||||
@ -379,7 +379,7 @@ function StepRow({ step, index, role }: { step: StepDef; index: number; role: Ro
|
||||
</div>
|
||||
</div>
|
||||
<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 />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,17 +6,26 @@ type Status = 'idle' | 'loading' | 'success' | 'error'
|
||||
|
||||
export function useIssues(filters: FilterState) {
|
||||
const [data, setData] = useState<IssuesData | null>(null)
|
||||
const [status, setStatus] = useState<Status>('idle')
|
||||
const [status, setStatus] = useState<Status>('loading')
|
||||
|
||||
useEffect(() => {
|
||||
setStatus('loading')
|
||||
fetch('/data/issues.json')
|
||||
const controller = new AbortController()
|
||||
|
||||
fetch('/data/issues.json', { signal: controller.signal })
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
||||
return r.json() as Promise<IssuesData>
|
||||
})
|
||||
.then((d) => { setData(d); setStatus('success') })
|
||||
.catch((err) => { console.error('[useIssues] failed to load issues.json:', err); setStatus('error') })
|
||||
.then((d) => {
|
||||
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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user