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." />
|
<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>
|
||||||
|
|||||||
@ -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
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]
|
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}
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user