import { stat } from 'node:fs/promises' import path from 'node:path' import express from 'express' import { config } from '../config.js' import { termsDocFor } from '../services/products.js' import { neueAnsichtModus } from '../services/kundenansicht/schalter.js' /** * Umschalt-Weiche der neuen Kundenansicht (P10): erst bei KUNDENANSICHT_NEU=on liefert sie die neuen oeffentlichen Seiten, * das Manifest und die Fassung-6-Adresse der Abo-Bedingungen, auch Besuchern ohne Sitzung. In off und pilot (auch mit '*') * ruft jede Zeile next(); die alten Dateien kommen dann unveraendert von express.static. server/index.js haengt den Router * hinter die Weichen von P0 (dashboardWeiche, oeffentlicheSeiteWeiche) und vor express.static ein. */ /** Adresse -> Datei unter public/, die bei on an ihre Stelle tritt. */ export const UMSCHALT_SEITEN = Object.freeze({ '/dashboard.html': 'neu/dashboard.html', // greift nur, wo P0s Weiche nicht gegriffen hat, also ohne Sitzung '/login.html': 'neu/oeffentlich/login.html', '/kuendigen.html': 'neu/oeffentlich/kuendigen.html', '/widerrufen.html': 'neu/oeffentlich/widerrufen.html', '/auftrag.html': 'neu/oeffentlich/auftrag.html', // Link der Auftragsbestaetigung (/auftrag.html#t=), Uebersicht N40 '/offline.html': 'neu/oeffentlich/offline.html', '/manifest.webmanifest': 'neu/manifest.webmanifest', }) const ABO_SEITE = '/abo-bedingungen.html' const ABO_PDF = '/WEBklar-Abo-Vertragsunterlagen.pdf' /** { url, pdfFile } der Fassung 6 aus config.abo.termsVersionNeu; ohne gesetzte Neu-Fassung null. */ export function fassung6Aus() { return config.abo.termsVersionNeu ? termsDocFor(config.abo.termsVersionNeu) : null } /** * Absoluter Pfad zu rel (Adresse oder Dateiname, mit oder ohne fuehrenden Schraegstrich) unter publicDir. * Null, wenn rel kein Text ist, aus public/ herausfuehren wuerde oder der aufgeloeste Pfad nicht darunter liegt. */ function unterPublic(publicDir, rel) { if (typeof rel !== 'string' || rel === '' || rel.includes('\0')) return null const relativ = path.posix.normalize(rel.replace(/^\/+/, '')) if (relativ === '..' || relativ.startsWith('../')) return null const wurzel = path.resolve(publicDir) const ziel = path.resolve(wurzel, '.' + path.posix.normalize('/' + rel)) return ziel.startsWith(wurzel + path.sep) ? ziel : null } /** * Router der Weiche. modus() und fassung6() werden je Anfrage gelesen (Schalter wirkt ohne Neustart der Weiche); * fassung6() liefert { url, pdfFile } oder null (dann bleibt die Datei von heute). */ export function umschaltSeitenRouter({ publicDir, modus = neueAnsichtModus, fassung6 = fassung6Aus }) { // Wie express.static: Gross-/Kleinschreibung und Schraegstrich am Ende zaehlen const router = express.Router({ caseSensitive: true, strict: true }) /** Zieldatei senden (no-cache); fehlt sie, laeuft die Anfrage unveraendert zu express.static weiter. */ const sende = (ziel, res, next) => stat(ziel).then( (info) => { if (!info.isFile()) return next() res.set('Cache-Control', 'no-cache') res.sendFile(ziel, { dotfiles: 'deny' }, (err) => { // Abgebrochene Verbindung (am Handy haeufig): kein Fehler fuers Log, das die Checkliste auf "Unbehandelter Fehler" zaehlt if (!err || err.code === 'ECONNABORTED') return if (res.headersSent) return res.removeHeader('Cache-Control') next(err.code === 'ENOENT' ? undefined : err) }) }, () => next() ) for (const [adresse, rel] of Object.entries(UMSCHALT_SEITEN)) { router.get(adresse, (_req, res, next) => { if (modus() !== 'on') return next() const ziel = unterPublic(publicDir, rel) return ziel ? sende(ziel, res, next) : next() }) } // Stripe-Checkout und die neuen Seiten verlinken diese zwei Adressen; im Fassung-6-Weg muessen sie Fassung 6 zeigen for (const [adresse, feld] of [[ABO_SEITE, 'url'], [ABO_PDF, 'pdfFile']]) { router.get(adresse, (_req, res, next) => { if (modus() !== 'on') return next() const ziel = unterPublic(publicDir, fassung6()?.[feld]) return ziel ? sende(ziel, res, next) : next() }) } return router }