Buchungskalender: Buchungsseite für Gäste

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 14:10:50 +00:00
1 parent bc9b4d9576
commit 0b0d8da335
5 files changed
+598 -2

No files matched your search

+58 -2
View File
@@ -1,11 +1,67 @@
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { Router } from 'express'
import { SITE_KEY_RE } from '../services/unterkunft/kontext.js'
/**
* Oeffentliche Standalone-Seiten des Buchungskalenders: /unterkunft/:siteKey und /unterkunft/:siteKey/buchung/:id
* (Vorbild: shopSeite.js). Geruest (Task 5): Task 7 und Strang 3 fuellen die Seiten.
* Oeffentliche Standalone-Seiten des Buchungskalenders (Vorbild: shopSeite.js):
* GET /unterkunft/:siteKey Widget public/embed/unterkunft.js auf neutraler Seite
* GET /unterkunft/:siteKey/buchung/:id Buchungsseite fuer Gaeste (public/unterkunft-buchung.js, Token ?t= liest nur das Skript)
* Die Seite verraet nicht, ob Schluessel oder Buchung existieren; das Skript fragt die oeffentliche Schnittstelle.
* Ungueltige Schluessel (z. B. "ical" der Export-Route /unterkunft/ical/:token.ics) und Ids: next(), am Ende 404.
*/
const ID_RE = /^[A-Za-z0-9_-]{1,36}$/
const DATEI = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'public', 'unterkunft-seite.html')
const CSP = [
"default-src 'self'",
"img-src 'self' data:",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'", // das Widget legt ein <style>-Element an
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'none'",
"form-action 'self'",
].join('; ')
let vorlage = null
export function resetUnterkunftSeiteForTests() {
vorlage = null
}
function escapeAttr(s) {
return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
function seite(res, next, siteKey, ansicht) {
try {
if (vorlage === null) vorlage = readFileSync(DATEI, 'utf8')
} catch (err) {
return next(err)
}
const html = vorlage.replaceAll('__SITEKEY__', escapeAttr(siteKey)).replaceAll('__ANSICHT__', escapeAttr(ansicht))
res.set({
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Content-Security-Policy': CSP,
'X-Robots-Tag': 'noindex',
})
return res.status(200).send(html)
}
export function unterkunftSeiteRouter() {
const router = Router()
router.get('/unterkunft/:siteKey', (req, res, next) => {
if (!SITE_KEY_RE.test(req.params.siteKey)) return next()
return seite(res, next, req.params.siteKey, 'widget')
})
router.get('/unterkunft/:siteKey/buchung/:id', (req, res, next) => {
if (!SITE_KEY_RE.test(req.params.siteKey) || !ID_RE.test(req.params.id)) return next()
return seite(res, next, req.params.siteKey, 'buchung')
})
return router
}