63 lines
2.2 KiB
JavaScript
63 lines
2.2 KiB
JavaScript
import { readFileSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { Router } from 'express'
|
|
|
|
/**
|
|
* Oeffentliche Standalone-Seiten des Webshops (Plan 2026-10-05-webshop.md, H2; Spec 5.4):
|
|
* GET /shop/:siteKey Shop mit dem Widget public/embed/shop.js
|
|
* GET /shop/:siteKey/widerruf dieselbe Seite, das Widget oeffnet gleich die Widerrufsfunktion
|
|
* Die Seite verraet nicht, ob der Schluessel existiert oder der Shop aktiv ist: jeder syntaktisch gueltige Schluessel
|
|
* bekommt dieselbe Seite, ob der Shop laeuft entscheidet das Widget ueber die oeffentliche Schnittstelle. Alles andere ist 404.
|
|
*/
|
|
|
|
const SITE_KEY_RE = /^[a-z2-7]{16}$/
|
|
const DATEI = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'public', 'shop-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 resetShopSeiteForTests() {
|
|
vorlage = null
|
|
}
|
|
|
|
function escapeAttr(s) {
|
|
return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>')
|
|
}
|
|
|
|
function seite(res, next, siteKey, ansicht) {
|
|
if (!SITE_KEY_RE.test(siteKey)) return res.status(404).type('text/plain').send('Not found')
|
|
try {
|
|
if (vorlage === null) vorlage = readFileSync(DATEI, 'utf8')
|
|
} catch (err) {
|
|
return next(err)
|
|
}
|
|
const html = vorlage.replace('__SITEKEY__', escapeAttr(siteKey)).replace('__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 shopSeiteRouter() {
|
|
const router = Router()
|
|
router.get('/shop/:siteKey', (req, res, next) => seite(res, next, req.params.siteKey, ''))
|
|
router.get('/shop/:siteKey/widerruf', (req, res, next) => seite(res, next, req.params.siteKey, 'widerruf'))
|
|
return router
|
|
}
|
|
|
|
export default shopSeiteRouter
|