feat: update security headers and improve content security policy; enhance hero image component and loading states in public news
This commit is contained in:
@@ -32,7 +32,7 @@
|
|||||||
Header always set Content-Security-Policy "frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de"
|
Header always set Content-Security-Policy "frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de"
|
||||||
|
|
||||||
# Optional: Vollständige Content Security Policy (zusätzlich zu frame-ancestors)
|
# Optional: Vollständige Content Security Policy (zusätzlich zu frame-ancestors)
|
||||||
# Header always set Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de; font-src 'self' https://fonts.gstatic.com data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: blob:; connect-src 'self'"
|
# Header always set Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data: blob:; connect-src 'self'"
|
||||||
|
|
||||||
# Proxy alle Anfragen an Nuxt Server (Port 3100)
|
# Proxy alle Anfragen an Nuxt Server (Port 3100)
|
||||||
ProxyPreserveHost On
|
ProxyPreserveHost On
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
Header always set Content-Security-Policy "frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de"
|
Header always set Content-Security-Policy "frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de"
|
||||||
|
|
||||||
# Optional: Vollständige Content Security Policy (zusätzlich zu frame-ancestors)
|
# Optional: Vollständige Content Security Policy (zusätzlich zu frame-ancestors)
|
||||||
# Header always set Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de; font-src 'self' https://fonts.gstatic.com data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: blob:; connect-src 'self'"
|
# Header always set Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self' https://harheimertc.de https://www.harheimertc.de; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data: blob:; connect-src 'self'"
|
||||||
|
|
||||||
# SPA Fallback für Nuxt.js
|
# SPA Fallback für Nuxt.js
|
||||||
<Directory "/var/www/harheimertc/dist">
|
<Directory "/var/www/harheimertc/dist">
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html {
|
html {
|
||||||
font-family: 'Inter', system-ui, sans-serif;
|
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: 'Montserrat', system-ui, sans-serif;
|
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
alt=""
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="w-full h-full object-cover"
|
class="w-full h-full object-cover"
|
||||||
|
width="1600"
|
||||||
|
height="900"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
fetchpriority="high"
|
fetchpriority="high"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
v-if="news.length > 0"
|
class="py-16 sm:py-20 bg-white min-h-[32rem]"
|
||||||
class="py-16 sm:py-20 bg-white"
|
|
||||||
>
|
>
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="text-center mb-16">
|
<div class="text-center mb-16">
|
||||||
@@ -14,7 +13,29 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center">
|
<div
|
||||||
|
v-if="isLoading"
|
||||||
|
class="grid gap-8 grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="placeholder in 3"
|
||||||
|
:key="`news-placeholder-${placeholder}`"
|
||||||
|
class="bg-gray-50 rounded-xl p-6 border border-gray-200"
|
||||||
|
>
|
||||||
|
<div class="h-4 w-32 bg-gray-200 rounded animate-pulse mb-4" />
|
||||||
|
<div class="h-7 w-3/4 bg-gray-200 rounded animate-pulse mb-4" />
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="h-4 w-full bg-gray-200 rounded animate-pulse" />
|
||||||
|
<div class="h-4 w-5/6 bg-gray-200 rounded animate-pulse" />
|
||||||
|
<div class="h-4 w-2/3 bg-gray-200 rounded animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="news.length > 0"
|
||||||
|
class="flex justify-center"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="grid gap-8"
|
class="grid gap-8"
|
||||||
:class="getGridClass()"
|
:class="getGridClass()"
|
||||||
@@ -43,6 +64,18 @@
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="max-w-xl mx-auto text-center bg-gray-50 border border-gray-200 rounded-xl p-8"
|
||||||
|
>
|
||||||
|
<p class="text-gray-700 font-semibold mb-2">
|
||||||
|
Aktuell keine News
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-600 text-sm">
|
||||||
|
Neue Vereinsnachrichten erscheinen hier automatisch.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- News Modal -->
|
<!-- News Modal -->
|
||||||
@@ -91,13 +124,16 @@ import { Calendar, X } from 'lucide-vue-next'
|
|||||||
|
|
||||||
const news = ref([])
|
const news = ref([])
|
||||||
const selectedNews = ref(null)
|
const selectedNews = ref(null)
|
||||||
|
const isLoading = ref(true)
|
||||||
|
|
||||||
const loadNews = async () => {
|
const loadNews = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await $fetch('/api/news-public')
|
const response = await $fetch('/api/news-public')
|
||||||
news.value = response.news
|
news.value = Array.isArray(response?.news) ? response.news : []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Laden der öffentlichen News:', error)
|
console.error('Fehler beim Laden der öffentlichen News:', error)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,13 +76,7 @@ export default defineNuxtConfig({
|
|||||||
{ property: 'twitter:description', content: 'Offizielle Website des Harheimer Tischtennis-Club 1954 e.V.' }
|
{ property: 'twitter:description', content: 'Offizielle Website des Harheimer Tischtennis-Club 1954 e.V.' }
|
||||||
],
|
],
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'canonical', href: 'https://www.harheimertc.de/' },
|
{ rel: 'canonical', href: 'https://www.harheimertc.de/' }
|
||||||
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
|
||||||
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
|
|
||||||
{
|
|
||||||
rel: 'stylesheet',
|
|
||||||
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@700;800;900&display=swap'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -226,17 +226,39 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineAsyncComponent, ref } from 'vue'
|
import { computed, defineAsyncComponent, h, ref } from 'vue'
|
||||||
import { SlidersHorizontal, X } from 'lucide-vue-next'
|
import { SlidersHorizontal, X } from 'lucide-vue-next'
|
||||||
import Hero from '~/components/Hero.vue'
|
import Hero from '~/components/Hero.vue'
|
||||||
const HomeTermine = defineAsyncComponent(() => import('~/components/HomeTermine.vue'))
|
|
||||||
const Spielplan = defineAsyncComponent(() => import('~/components/Spielplan.vue'))
|
const SectionLoadingPlaceholder = {
|
||||||
const PublicNews = defineAsyncComponent(() => import('~/components/PublicNews.vue'))
|
name: 'SectionLoadingPlaceholder',
|
||||||
const HomeActions = defineAsyncComponent(() => import('~/components/HomeActions.vue'))
|
render() {
|
||||||
const HomeTrainingTeaser = defineAsyncComponent(() => import('~/components/HomeTrainingTeaser.vue'))
|
return h('section', { class: 'py-16 sm:py-20 bg-white' }, [
|
||||||
const HomeLinksTeaser = defineAsyncComponent(() => import('~/components/HomeLinksTeaser.vue'))
|
h('div', { class: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8' }, [
|
||||||
const HomeVereinsmeisterschaftenTeaser = defineAsyncComponent(() => import('~/components/HomeVereinsmeisterschaftenTeaser.vue'))
|
h('div', { class: 'h-10 max-w-xs mx-auto rounded bg-gray-200 animate-pulse mb-8' }),
|
||||||
const HomeSpielplanTeamWidget = defineAsyncComponent(() => import('~/components/HomeSpielplanTeamWidget.vue'))
|
h('div', { class: 'h-56 rounded-2xl bg-gray-100 animate-pulse' })
|
||||||
|
])
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHomeSection(loader) {
|
||||||
|
return defineAsyncComponent({
|
||||||
|
loader,
|
||||||
|
loadingComponent: SectionLoadingPlaceholder,
|
||||||
|
delay: 0,
|
||||||
|
suspensible: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const HomeTermine = loadHomeSection(() => import('~/components/HomeTermine.vue'))
|
||||||
|
const Spielplan = loadHomeSection(() => import('~/components/Spielplan.vue'))
|
||||||
|
const PublicNews = loadHomeSection(() => import('~/components/PublicNews.vue'))
|
||||||
|
const HomeActions = loadHomeSection(() => import('~/components/HomeActions.vue'))
|
||||||
|
const HomeTrainingTeaser = loadHomeSection(() => import('~/components/HomeTrainingTeaser.vue'))
|
||||||
|
const HomeLinksTeaser = loadHomeSection(() => import('~/components/HomeLinksTeaser.vue'))
|
||||||
|
const HomeVereinsmeisterschaftenTeaser = loadHomeSection(() => import('~/components/HomeVereinsmeisterschaftenTeaser.vue'))
|
||||||
|
const HomeSpielplanTeamWidget = loadHomeSection(() => import('~/components/HomeSpielplanTeamWidget.vue'))
|
||||||
|
|
||||||
const { data: config } = await useFetch('/api/config')
|
const { data: config } = await useFetch('/api/config')
|
||||||
const { data: authStatus } = await useFetch('/api/auth/status')
|
const { data: authStatus } = await useFetch('/api/auth/status')
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ async function fileExists(filePath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAllowedVariantKey(key) {
|
||||||
|
return /^[A-Za-z0-9_-]+$/.test(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendPathSegment(rootDir, segment) {
|
||||||
|
if (!isAllowedVariantKey(segment)) return null
|
||||||
|
return `${rootDir}${path.sep}${segment}`
|
||||||
|
}
|
||||||
|
|
||||||
async function listHeroVariants(heroRoot) {
|
async function listHeroVariants(heroRoot) {
|
||||||
const dirEntries = await fs.readdir(heroRoot, { withFileTypes: true })
|
const dirEntries = await fs.readdir(heroRoot, { withFileTypes: true })
|
||||||
const variants = []
|
const variants = []
|
||||||
@@ -44,12 +53,17 @@ async function listHeroVariants(heroRoot) {
|
|||||||
if (!entry.isDirectory()) continue
|
if (!entry.isDirectory()) continue
|
||||||
|
|
||||||
const key = entry.name
|
const key = entry.name
|
||||||
const variantDir = path.join(heroRoot, key)
|
if (!isAllowedVariantKey(key)) continue
|
||||||
|
|
||||||
|
const variantDir = appendPathSegment(heroRoot, key)
|
||||||
|
if (!variantDir) continue
|
||||||
|
|
||||||
const mobileFile = 'hero_960.webp'
|
const mobileFile = 'hero_960.webp'
|
||||||
const desktopFile = 'hero_1600.webp'
|
const desktopFile = 'hero_1600.webp'
|
||||||
|
|
||||||
const mobilePath = path.join(variantDir, mobileFile)
|
const mobilePath = `${variantDir}${path.sep}${mobileFile}`
|
||||||
const desktopPath = path.join(variantDir, desktopFile)
|
const desktopPath = `${variantDir}${path.sep}${desktopFile}`
|
||||||
|
if (!mobilePath || !desktopPath) continue
|
||||||
|
|
||||||
if (!(await fileExists(mobilePath)) || !(await fileExists(desktopPath))) {
|
if (!(await fileExists(mobilePath)) || !(await fileExists(desktopPath))) {
|
||||||
continue
|
continue
|
||||||
@@ -57,7 +71,8 @@ async function listHeroVariants(heroRoot) {
|
|||||||
|
|
||||||
let fallbackFile = desktopFile
|
let fallbackFile = desktopFile
|
||||||
for (const candidate of FALLBACK_FILE_CANDIDATES) {
|
for (const candidate of FALLBACK_FILE_CANDIDATES) {
|
||||||
if (await fileExists(path.join(variantDir, candidate))) {
|
const fallbackPath = `${variantDir}${path.sep}${candidate}`
|
||||||
|
if (fallbackPath && await fileExists(fallbackPath)) {
|
||||||
fallbackFile = candidate
|
fallbackFile = candidate
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,8 @@ export default defineEventHandler((event) => {
|
|||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
`frame-ancestors ${allowedFrameAncestors}`,
|
`frame-ancestors ${allowedFrameAncestors}`,
|
||||||
// Nuxt lädt Fonts ggf. von Google (siehe nuxt.config.js)
|
"font-src 'self' data:",
|
||||||
"font-src 'self' https://fonts.gstatic.com data:",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
|
||||||
// Script: Nuxt kann in Dev eval nutzen; diese CSP ist primär für Produktion gedacht.
|
// Script: Nuxt kann in Dev eval nutzen; diese CSP ist primär für Produktion gedacht.
|
||||||
"script-src 'self'",
|
"script-src 'self'",
|
||||||
"img-src 'self' data: blob:",
|
"img-src 'self' data: blob:",
|
||||||
|
|||||||
Reference in New Issue
Block a user