Compare commits
2
Commits
99daeb014c
...
4e6341c5d5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e6341c5d5
|
||
|
|
b5a3b278ba
|
@@ -1,3 +1,4 @@
|
|||||||
|
import {headers} from 'next/headers'
|
||||||
import {notFound} from 'next/navigation'
|
import {notFound} from 'next/navigation'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
|
|
||||||
@@ -67,6 +68,8 @@ export default async function AwtisPajSlug(props) {
|
|||||||
const params = await props.params
|
const params = await props.params
|
||||||
const {slug} = params
|
const {slug} = params
|
||||||
const anAwtis = await jwennAwtis(slug)
|
const anAwtis = await jwennAwtis(slug)
|
||||||
|
const requestHeaders = await headers()
|
||||||
|
const nonce = requestHeaders.get('x-nonce') || undefined
|
||||||
|
|
||||||
const {photo} = anAwtis
|
const {photo} = anAwtis
|
||||||
const kuvetiFormat = formatKuveti(photo)
|
const kuvetiFormat = formatKuveti(photo)
|
||||||
@@ -91,6 +94,7 @@ export default async function AwtisPajSlug(props) {
|
|||||||
<section>
|
<section>
|
||||||
<script
|
<script
|
||||||
type='application/ld+json'
|
type='application/ld+json'
|
||||||
|
nonce={nonce}
|
||||||
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import {useServerInsertedHTML} from 'next/navigation'
|
|||||||
import {CacheProvider as DefaultCacheProvider} from '@emotion/react'
|
import {CacheProvider as DefaultCacheProvider} from '@emotion/react'
|
||||||
|
|
||||||
export default function NextAppDirEmotionCacheProvider(props) {
|
export default function NextAppDirEmotionCacheProvider(props) {
|
||||||
const {options, CacheProvider = DefaultCacheProvider, children} = props
|
const {options, CacheProvider = DefaultCacheProvider, children, nonce} = props
|
||||||
|
|
||||||
const [registry] = React.useState(() => {
|
const [registry] = React.useState(() => {
|
||||||
const cache = createCache(options)
|
const cache = createCache({...options, nonce})
|
||||||
cache.compat = true
|
cache.compat = true
|
||||||
const prevInsert = cache.insert
|
const prevInsert = cache.insert
|
||||||
let inserted = []
|
let inserted = []
|
||||||
@@ -65,12 +65,14 @@ export default function NextAppDirEmotionCacheProvider(props) {
|
|||||||
<style
|
<style
|
||||||
key={name}
|
key={name}
|
||||||
dangerouslySetInnerHTML={{__html: style}}
|
dangerouslySetInnerHTML={{__html: style}}
|
||||||
|
nonce={nonce}
|
||||||
data-emotion={`${registry.cache.key}-global ${name}`}
|
data-emotion={`${registry.cache.key}-global ${name}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{styles !== '' && (
|
{styles !== '' && (
|
||||||
<style
|
<style
|
||||||
dangerouslySetInnerHTML={{__html: styles}}
|
dangerouslySetInnerHTML={{__html: styles}}
|
||||||
|
nonce={nonce}
|
||||||
data-emotion={dataEmotionAttribute}
|
data-emotion={dataEmotionAttribute}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -84,5 +86,6 @@ export default function NextAppDirEmotionCacheProvider(props) {
|
|||||||
NextAppDirEmotionCacheProvider.propTypes = {
|
NextAppDirEmotionCacheProvider.propTypes = {
|
||||||
options: PropTypes.object.isRequired,
|
options: PropTypes.object.isRequired,
|
||||||
CacheProvider: PropTypes.func,
|
CacheProvider: PropTypes.func,
|
||||||
children: PropTypes.node.isRequired
|
children: PropTypes.node.isRequired,
|
||||||
|
nonce: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-3
@@ -1,3 +1,4 @@
|
|||||||
|
import {headers} from 'next/headers'
|
||||||
import PlausibleProvider from 'next-plausible'
|
import PlausibleProvider from 'next-plausible'
|
||||||
import TopLoader from '../components/top-loader'
|
import TopLoader from '../components/top-loader'
|
||||||
import Navigasyon from '../components/navigasyon'
|
import Navigasyon from '../components/navigasyon'
|
||||||
@@ -61,16 +62,20 @@ const jsonLd = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function RootLayout({children}) {
|
export default async function RootLayout({children}) {
|
||||||
|
const requestHeaders = await headers()
|
||||||
|
const nonce = requestHeaders.get('x-nonce') || undefined
|
||||||
|
|
||||||
const inner = (
|
const inner = (
|
||||||
<>
|
<>
|
||||||
<TopLoader color='#ffeb3b' />
|
<TopLoader color='#ffeb3b' nonce={nonce} />
|
||||||
<ThemeRegistry>
|
<ThemeRegistry nonce={nonce}>
|
||||||
<Navigasyon />
|
<Navigasyon />
|
||||||
{children}
|
{children}
|
||||||
</ThemeRegistry>
|
</ThemeRegistry>
|
||||||
<section>
|
<section>
|
||||||
<script
|
<script
|
||||||
type='application/ld+json'
|
type='application/ld+json'
|
||||||
|
nonce={nonce}
|
||||||
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@@ -81,7 +86,7 @@ export default async function RootLayout({children}) {
|
|||||||
<html suppressHydrationWarning lang='fr'>
|
<html suppressHydrationWarning lang='fr'>
|
||||||
<body>
|
<body>
|
||||||
{plausibleUrl
|
{plausibleUrl
|
||||||
? <PlausibleProvider src={plausibleUrl}>{inner}</PlausibleProvider>
|
? <PlausibleProvider src={plausibleUrl} scriptProps={{nonce}}>{inner}</PlausibleProvider>
|
||||||
: inner}
|
: inner}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {headers} from 'next/headers'
|
||||||
import {notFound} from 'next/navigation'
|
import {notFound} from 'next/navigation'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
|
|
||||||
@@ -71,6 +72,8 @@ export default async function AnPawolPaj(props) {
|
|||||||
const {slug} = params
|
const {slug} = params
|
||||||
|
|
||||||
const anTeks = await jwennAnTeks(slug)
|
const anTeks = await jwennAnTeks(slug)
|
||||||
|
const requestHeaders = await headers()
|
||||||
|
const nonce = requestHeaders.get('x-nonce') || undefined
|
||||||
const {couverture} = anTeks
|
const {couverture} = anTeks
|
||||||
const teksKuvetiFormat = formatKuveti(couverture)
|
const teksKuvetiFormat = formatKuveti(couverture)
|
||||||
|
|
||||||
@@ -126,6 +129,7 @@ export default async function AnPawolPaj(props) {
|
|||||||
<section>
|
<section>
|
||||||
<script
|
<script
|
||||||
type='application/ld+json'
|
type='application/ld+json'
|
||||||
|
nonce={nonce}
|
||||||
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
dangerouslySetInnerHTML={{__html: JSON.stringify(jsonLd)}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ const theme = createTheme({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default function ThemeRegistry(props) {
|
export default function ThemeRegistry(props) {
|
||||||
const {children} = props
|
const {children, nonce} = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InitColorSchemeScript attribute='class' />
|
<InitColorSchemeScript attribute='class' nonce={nonce} />
|
||||||
<NextAppDirEmotionCacheProvider options={{key: 'mui'}}>
|
<NextAppDirEmotionCacheProvider options={{key: 'mui'}} nonce={nonce}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline enableColorScheme />
|
<CssBaseline enableColorScheme />
|
||||||
<ChanjeTem />
|
<ChanjeTem />
|
||||||
@@ -81,5 +81,6 @@ export default function ThemeRegistry(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ThemeRegistry.propTypes = {
|
ThemeRegistry.propTypes = {
|
||||||
children: PropTypes.node.isRequired
|
children: PropTypes.node.isRequired,
|
||||||
|
nonce: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import NextTopLoader from 'nextjs-toploader'
|
import NextTopLoader from 'nextjs-toploader'
|
||||||
|
|
||||||
export default function TopLoader({color}) {
|
export default function TopLoader({color, nonce}) {
|
||||||
return <NextTopLoader color={color} />
|
return <NextTopLoader color={color} nonce={nonce} />
|
||||||
|
}
|
||||||
|
|
||||||
|
TopLoader.propTypes = {
|
||||||
|
color: PropTypes.string.isRequired,
|
||||||
|
nonce: PropTypes.string
|
||||||
}
|
}
|
||||||
|
|
||||||
TopLoader.propTypes = {
|
TopLoader.propTypes = {
|
||||||
|
|||||||
+41
-1
@@ -28,9 +28,49 @@ function buildRemotePatterns() {
|
|||||||
return patterns
|
return patterns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/:path*',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'Strict-Transport-Security',
|
||||||
|
value: 'max-age=63072000; includeSubDomains; preload'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'X-Content-Type-Options',
|
||||||
|
value: 'nosniff'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'X-Frame-Options',
|
||||||
|
value: 'SAMEORIGIN'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Referrer-Policy',
|
||||||
|
value: 'strict-origin-when-cross-origin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Cross-Origin-Resource-Policy',
|
||||||
|
value: 'same-site'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Cross-Origin-Opener-Policy',
|
||||||
|
value: 'same-origin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Permissions-Policy',
|
||||||
|
value: 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = (withPWA({
|
module.exports = (withPWA({
|
||||||
turbopack: {},
|
turbopack: {},
|
||||||
|
poweredByHeader: false,
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: buildRemotePatterns()
|
remotePatterns: buildRemotePatterns()
|
||||||
}
|
},
|
||||||
|
headers
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/* eslint-disable @next/next/no-server-import-in-page */
|
||||||
|
import {NextResponse} from 'next/server'
|
||||||
|
|
||||||
|
const nonceCharset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||||
|
|
||||||
|
function generateNonce() {
|
||||||
|
const array = new Uint8Array(16)
|
||||||
|
crypto.getRandomValues(array)
|
||||||
|
let nonce = ''
|
||||||
|
for (const byte of array) {
|
||||||
|
nonce += nonceCharset[byte % 64]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractOrigin(url) {
|
||||||
|
if (!url) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(url).origin
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHostSource(raw) {
|
||||||
|
if (!raw) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const [hostname, port] = raw.split(':')
|
||||||
|
if (!hostname) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||||
|
return `http://${hostname}${port ? `:${port}` : ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `https://${hostname}${port ? `:${port}` : ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCsp(nonce) {
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL || process.env.NEXT_PUBLIC_API_URL_ROOT || ''
|
||||||
|
const plausibleUrl = process.env.NEXT_PUBLIC_PLAUSIBLE_URL || ''
|
||||||
|
const mizikUrl = process.env.NEXT_PUBLIC_OKI_MIZIK_URL || ''
|
||||||
|
const imageDomainsRaw = process.env.NEXT_PUBLIC_DOMAINS_IMAGE || ''
|
||||||
|
|
||||||
|
const apiOrigin = extractOrigin(apiUrl)
|
||||||
|
const plausibleOrigin = extractOrigin(plausibleUrl)
|
||||||
|
const mizikOrigin = extractOrigin(mizikUrl)
|
||||||
|
const imageOrigins = imageDomainsRaw
|
||||||
|
.split(' ')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(entry => extractHostSource(entry))
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const imageOriginsSet = new Set([apiOrigin, ...imageOrigins])
|
||||||
|
const imageOriginsUnique = [...imageOriginsSet].filter(Boolean)
|
||||||
|
|
||||||
|
const connectSrc = ['\'self\'', apiOrigin, plausibleOrigin, mizikOrigin].filter(Boolean)
|
||||||
|
const imgSrc = ['\'self\'', 'data:', 'blob:', ...imageOriginsUnique].filter(Boolean)
|
||||||
|
const mediaSrc = ['\'self\'', apiOrigin, mizikOrigin].filter(Boolean)
|
||||||
|
|
||||||
|
// Frame-src is intentionally broad: the application embeds user-supplied
|
||||||
|
// PeerTube instances in addition to known platforms (Tidal, Deezer, Spotify,
|
||||||
|
// Soundcloud, Apple Music). Restricting this to a fixed list would break
|
||||||
|
// user-generated content.
|
||||||
|
const frameSrc = ['\'self\'', 'https:']
|
||||||
|
|
||||||
|
const directives = [
|
||||||
|
'default-src \'self\'',
|
||||||
|
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
|
||||||
|
`style-src-elem 'self' 'nonce-${nonce}'`,
|
||||||
|
'style-src-attr \'unsafe-inline\'',
|
||||||
|
`img-src ${imgSrc.join(' ')}`,
|
||||||
|
'font-src \'self\'',
|
||||||
|
`connect-src ${connectSrc.join(' ')}`,
|
||||||
|
`frame-src ${frameSrc.join(' ')}`,
|
||||||
|
`media-src ${mediaSrc.join(' ')}`,
|
||||||
|
'manifest-src \'self\'',
|
||||||
|
'object-src \'none\'',
|
||||||
|
'base-uri \'self\'',
|
||||||
|
'form-action \'self\'',
|
||||||
|
'frame-ancestors \'self\'',
|
||||||
|
'upgrade-insecure-requests'
|
||||||
|
]
|
||||||
|
|
||||||
|
return directives.join('; ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function proxy(request) {
|
||||||
|
const nonce = generateNonce()
|
||||||
|
const requestHeaders = new Headers(request.headers)
|
||||||
|
requestHeaders.set('x-nonce', nonce)
|
||||||
|
|
||||||
|
const response = NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: requestHeaders
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
response.headers.set('Content-Security-Policy', buildCsp(nonce))
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
{
|
||||||
|
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
|
||||||
|
missing: [
|
||||||
|
{type: 'header', key: 'next-router-prefetch'},
|
||||||
|
{type: 'header', key: 'purpose', value: 'prefetch'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user