diff --git a/public/app.js b/public/app.js index 6c90a48..d11bb70 100644 --- a/public/app.js +++ b/public/app.js @@ -1,4 +1,4 @@ -const C24 = u => u; // check24 proxy urls already absolute +const IMG = u => '/img?u=' + encodeURIComponent(u); // route images through same-origin proxy const REG = { kroatien:{flag:'🇭🇷',name:'Kroatien',color:'var(--kro)',mk:'#e8643c'}, kanaren:{flag:'🇮🇨',name:'Kanaren',color:'var(--kan)',mk:'#f1a23b'}, @@ -119,7 +119,7 @@ let STATE = {votes:{}}; /* ---------- hero ---------- */ document.getElementById('herobg').style.backgroundImage = - "url('"+byId('bluesun').imgs[0]+"')"; + "url('"+IMG(byId('bluesun').imgs[0])+"')"; /* ---------- voter selector ---------- */ const whoEl = document.getElementById('who'); @@ -166,7 +166,7 @@ function avgFor(id){ return {avg:n?sum/n:0,n,per}; } function cardHTML(o){ - const imgs=o.imgs.map((s,i)=>`${o.name}`).join(''); + const imgs=o.imgs.map((s,i)=>`${o.name}`).join(''); const dots=o.imgs.length>1?`
${o.imgs.map((_,i)=>``).join('')}
`:''; const nav=o.imgs.length>1?'':''; const rate=o.rate?`${o.rate} ${o.rlabel}${o.bew?(' · '+o.bew+' Bew.'):''}`:(o.bew?`${o.bew} Bewertungen`:''); @@ -256,10 +256,11 @@ async function sendVote(option,stars){ try{const r=await fetch('/api/vote',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({voter:me_,option,stars})});STATE=await r.json();renderVotes();}catch(e){} } -function flashSaved(){const s=document.getElementById('saved');s.classList.add('on');setTimeout(()=>s.classList.remove('on'),1400);} +function flashSaved(msg){const s=document.getElementById('saved');s.textContent=msg||'✓ gespeichert';s.classList.add('on');setTimeout(()=>s.classList.remove('on'),1800);} document.getElementById('resetBtn').onclick=async()=>{ if(!confirm('Wirklich ALLE Bewertungen von Till, Lea und Astrid zurücksetzen?'))return; - try{const r=await fetch('/api/reset',{method:'POST'});STATE=await r.json();renderVotes();flashSaved();}catch(e){} + try{const r=await fetch('/api/reset',{method:'POST'});STATE=await r.json();renderVotes();flashSaved('✓ Alle Bewertungen gelöscht');} + catch(e){alert('Zurücksetzen fehlgeschlagen – bitte nochmal versuchen.');} }; function renderAll(){renderWho();renderRecos();renderRegions();} diff --git a/server.js b/server.js index bb1f97c..6d4c7e6 100644 --- a/server.js +++ b/server.js @@ -30,6 +30,12 @@ function saveState(state) { } let state = loadState(); +// In-memory image cache for the proxy +const imgCache = new Map(); +function hostAllowed(host) { + return /(^|\.)urlaub\.check24\.de$/.test(host) || host === 'files.ahoi-schiff.de'; +} + const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.ico': 'image/x-icon', '.webmanifest': 'application/manifest+json' }; @@ -72,6 +78,34 @@ const server = http.createServer(async (req, res) => { } if (p === '/health') { return sendJSON(res, 200, { ok: true }); } + // ---- Image proxy (defeats hotlink/referrer protection, same-origin = always loads) ---- + if (p === '/img' && req.method === 'GET') { + const u = url.searchParams.get('u'); + let host; + try { host = new URL(u).host; } catch (e) { res.writeHead(400); return res.end('bad url'); } + if (!hostAllowed(host)) { res.writeHead(403); return res.end('host not allowed'); } + if (imgCache.has(u)) { + const c = imgCache.get(u); + res.writeHead(200, { 'Content-Type': c.type, 'Cache-Control': 'public, max-age=604800' }); + return res.end(c.buf); + } + try { + const referer = host.endsWith('ahoi-schiff.de') ? 'https://www.ahoi-schiff.de/' : 'https://urlaub.check24.de/'; + const ac = new AbortController(); + const t = setTimeout(() => ac.abort(), 8000); + const r = await fetch(u, { signal: ac.signal, headers: { + 'Referer': referer, 'User-Agent': 'Mozilla/5.0 (compatible; HeidrichReise/1.0)', 'Accept': 'image/*' + }}); + clearTimeout(t); + if (!r.ok) { res.writeHead(502); return res.end('upstream ' + r.status); } + const type = r.headers.get('content-type') || 'image/jpeg'; + const buf = Buffer.from(await r.arrayBuffer()); + if (imgCache.size < 300) imgCache.set(u, { buf, type }); + res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'public, max-age=604800' }); + return res.end(buf); + } catch (e) { res.writeHead(502); return res.end('proxy error'); } + } + // ---- Static ---- let file = p === '/' ? '/index.html' : decodeURIComponent(p); const full = path.join(PUBLIC, path.normalize(file).replace(/^(\.\.[\/\\])+/, ''));