Kundenansicht neu: Dashboard-Weiche vor den statischen Dateien, Platzhalter-Seite
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c4a4a567d
commit
f2c9eb107a
4 files changed
+221
No files matched your search
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="kn-seite" content="neu">
|
||||
<title>Kundenbereich – WEBklar</title>
|
||||
<style>
|
||||
body.kn { margin: 0; background: #fff; color: #1a1f36; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; }
|
||||
body.kn main { max-width: 640px; margin: 0 auto; padding: 48px 16px; }
|
||||
body.kn h1 { margin: 0 0 8px; font-size: 22px; font-weight: 600; }
|
||||
body.kn p { margin: 0 0 8px; color: #5e6678; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="kn">
|
||||
<main data-kn-platzhalter>
|
||||
<h1>Kundenansicht neu</h1>
|
||||
<p>Platzhalter bis P1</p>
|
||||
<p>Ansicht: <span id="kn-platzhalter-ansicht"></span></p>
|
||||
</main>
|
||||
<script>
|
||||
(function () {
|
||||
var ziel = document.getElementById('kn-platzhalter-ansicht')
|
||||
function zeige(text) { ziel.textContent = text }
|
||||
fetch('/api/auth/me', { credentials: 'same-origin' })
|
||||
.then(function (r) { return r.json() })
|
||||
.then(function (me) { zeige(me && me.authenticated === false ? 'abgemeldet' : (me && typeof me.ansicht === 'string' ? me.ansicht : '')) })
|
||||
.catch(function () { zeige('Fehler') })
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -60,6 +60,7 @@ import teamRoutes from './routes/team/index.js'
|
||||
import legacyAdminRedirect from './routes/legacyAdminRedirect.js'
|
||||
import { corsMiddleware } from './middleware/cors.js'
|
||||
import { kundenansichtGuard } from './middleware/kundenansichtGuard.js'
|
||||
import { dashboardWeiche, neuDirektWeiche, oeffentlicheSeiteWeiche } from './services/kundenansicht/dashboardWeiche.js'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const publicDir = path.join(__dirname, '..', 'public')
|
||||
@@ -151,6 +152,11 @@ app.use(unterkunftSeiteRouter())
|
||||
// Abgeschaltetes Admin-Panel und Rechnungsanalyse: Links aus alten Team-Mails ins Ticketsystem
|
||||
app.use('/admin.html', legacyAdminRedirect.admin)
|
||||
app.use('/kalkulation.html', legacyAdminRedirect.kalkulation)
|
||||
// Kundenansicht neu: je Sitzung die neue oder die alte Seite (Schalter KUNDENANSICHT_NEU), vor den statischen Dateien
|
||||
app.get('/dashboard.html', dashboardWeiche({ publicDir }))
|
||||
app.get('/neu/dashboard.html', neuDirektWeiche())
|
||||
// Kuendigen und Widerrufen: im Pilot die neuen Seiten aus public/neu/oeffentlich/, sobald die Dateien dort liegen
|
||||
app.get(['/kuendigen.html', '/widerrufen.html'], oeffentlicheSeiteWeiche({ publicDir }))
|
||||
app.use(express.static(publicDir))
|
||||
|
||||
app.get('/dashboard.html', (req, res, next) => {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { stat } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { neueAnsicht } from './schalter.js'
|
||||
|
||||
/**
|
||||
* Weichen der neuen Kundenansicht (Schalter KUNDENANSICHT_NEU, siehe schalter.js). Handler-Fabriken, die server/index.js
|
||||
* vor express.static einhaengt: je Sitzung die neue Seite aus public/neu/ oder next() fuer die alte (bytegleich, von
|
||||
* express.static ausgeliefert). Die neue Seite wird nie gecacht, weil dieselbe Adresse je Sitzung etwas anderes liefert.
|
||||
*/
|
||||
|
||||
const NEUES_DASHBOARD = 'neu/dashboard.html'
|
||||
|
||||
// Nachtrag N39: Seiten, die schon im Pilot aus public/neu/oeffentlich/ kommen, sobald die Datei dort liegt
|
||||
const OEFFENTLICHE_SEITEN = new Map([
|
||||
['/kuendigen.html', 'neu/oeffentlich/kuendigen.html'],
|
||||
['/widerrufen.html', 'neu/oeffentlich/widerrufen.html'],
|
||||
])
|
||||
|
||||
/** Neue Seite senden; no-cache steht vor sendFile, damit Express den Header nicht ueberschreibt. Fehler gehen an next(err). */
|
||||
function sendeNeu(res, publicDir, datei, next) {
|
||||
res.set('Cache-Control', 'no-cache')
|
||||
res.sendFile(datei, { root: publicDir }, (err) => {
|
||||
if (err) next(err)
|
||||
})
|
||||
}
|
||||
|
||||
/** GET /dashboard.html: neue Ansicht -> public/neu/dashboard.html, sonst next(). */
|
||||
export function dashboardWeiche({ publicDir }) {
|
||||
return (req, res, next) => {
|
||||
if (!neueAnsicht(req)) return next()
|
||||
sendeNeu(res, publicDir, NEUES_DASHBOARD, next)
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /neu/dashboard.html (Direktaufruf): neue Ansicht -> next() (express.static liefert die Datei), sonst 302 auf /dashboard.html. */
|
||||
export function neuDirektWeiche() {
|
||||
return (req, res, next) => {
|
||||
if (neueAnsicht(req)) return next()
|
||||
res.redirect(302, '/dashboard.html')
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /kuendigen.html und /widerrufen.html (N39): angemeldete Pilot-Sitzung und Datei vorhanden -> neue Seite, sonst next(). */
|
||||
export function oeffentlicheSeiteWeiche({ publicDir }) {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
const datei = OEFFENTLICHE_SEITEN.get(req.path)
|
||||
if (!datei || !neueAnsicht(req)) return next()
|
||||
const info = await stat(path.join(publicDir, datei)).catch(() => null)
|
||||
if (!info?.isFile()) return next()
|
||||
sendeNeu(res, publicDir, datei, next)
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { test, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import express from 'express'
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
|
||||
// Dashboard-Weiche: je Sitzung die neue Platzhalter-Seite oder bytegleich die alte (Schalter KUNDENANSICHT_NEU)
|
||||
const aw = await startAppwriteMock({ schema: { customerViewLog: schemaKeys('customerViewLog') } })
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp, rawGet } = await import('./support/testApp.js')
|
||||
const { customerCookie, kundenansichtCookie, adminCookie, viewAsCookie } = await import('./support/fixtures.js')
|
||||
const { dashboardWeiche, neuDirektWeiche, oeffentlicheSeiteWeiche } = await import('../server/services/kundenansicht/dashboardWeiche.js')
|
||||
const publicDir = fileURLToPath(new URL('../public/', import.meta.url))
|
||||
const router = express.Router() // gleiche Reihenfolge wie server/index.js
|
||||
router.get('/dashboard.html', dashboardWeiche({ publicDir }))
|
||||
router.get('/neu/dashboard.html', neuDirektWeiche())
|
||||
router.get(['/kuendigen.html', '/widerrufen.html'], oeffentlicheSeiteWeiche({ publicDir }))
|
||||
router.use(express.static(publicDir))
|
||||
// Nachtrag N39: dieselbe Weiche mit einem Nachbau von public/ (Datei fuer Datei erfunden), weil public/neu/oeffentlich/ erst mit P2 entsteht
|
||||
const nachbau = mkdtempSync(path.join(os.tmpdir(), 'kn-weiche-'))
|
||||
mkdirSync(path.join(nachbau, 'neu', 'oeffentlich'), { recursive: true })
|
||||
writeFileSync(path.join(nachbau, 'neu', 'oeffentlich', 'kuendigen.html'), '<!doctype html><meta name="kn-seite" content="neu-oeffentlich"><title>Nachbau</title>')
|
||||
const nachbauRouter = express.Router()
|
||||
nachbauRouter.get(['/kuendigen.html', '/widerrufen.html'], oeffentlicheSeiteWeiche({ publicDir: nachbau }))
|
||||
nachbauRouter.use(express.static(publicDir))
|
||||
const app = await startTestApp({ '/nachbau': nachbauRouter, '/': router })
|
||||
after(() => {
|
||||
if (nachbau.startsWith(path.join(os.tmpdir(), 'kn-weiche-'))) rmSync(nachbau, { recursive: true, force: true })
|
||||
return Promise.all([app.close(), aw.close(), st.close()])
|
||||
})
|
||||
const stelle = (mode, pilotEmails = []) => Object.assign(config.kundenansichtNeu, { mode, pilotEmails })
|
||||
const ALT = readFileSync(path.join(publicDir, 'dashboard.html'), 'utf8')
|
||||
const istNeu = (r) => r.status === 200 && String(r.body).includes('<meta name="kn-seite" content="neu">') // N5: gepinnt wird nur die Meta-Zeile
|
||||
const istAlt = (r) => r.status === 200 && r.body === ALT
|
||||
const MAIL = 'k@example.invalid' // E-Mail von c1; c2 hat k2@example.invalid
|
||||
const UNSIGNIERT = `${config.cookieName}=${encodeURIComponent(JSON.stringify({ customerId: 'c1', appwriteUserId: 'u1', email: MAIL }))}`
|
||||
|
||||
const FAELLE = [ // [Name, Modus, Liste, Cookie, Ergebnis]
|
||||
['Pilot, Kunde auf der Liste', 'pilot', [MAIL], customerCookie('c1'), 'neu'],
|
||||
['Pilot, Kunde daneben', 'pilot', [MAIL], customerCookie('c2'), 'alt'],
|
||||
['Pilot, Mitarbeiter-Kundenansicht', 'pilot', [], kundenansichtCookie(), 'neu'],
|
||||
['off mit Stern, Kunde', 'off', ['*'], customerCookie('c1'), 'alt'],
|
||||
['off mit Stern, Kundenansicht', 'off', ['*'], kundenansichtCookie(), 'alt'],
|
||||
['on, Kunde', 'on', [], customerCookie('c1'), 'neu'],
|
||||
['on, alte Mitarbeiter-Sitzung', 'on', [], adminCookie(), 'alt'],
|
||||
['on, Kundenansicht ohne viewId', 'on', [], viewAsCookie('c1'), 'alt'],
|
||||
['Pilot mit Stern, ohne Sitzung (200 wie heute, kein Redirect)', 'pilot', ['*'], undefined, 'alt'],
|
||||
['Pilot mit Stern, unsigniertes Cookie', 'pilot', ['*'], UNSIGNIERT, 'alt'],
|
||||
]
|
||||
for (const [name, modus, liste, cookie, soll] of FAELLE) test(name, async () => {
|
||||
stelle(modus, liste)
|
||||
const r = await rawGet('/dashboard.html', { cookie })
|
||||
assert.ok(soll === 'neu' ? istNeu(r) : istAlt(r), `${r.status} ${String(r.body).slice(0, 80)}`)
|
||||
})
|
||||
|
||||
test('neue Seite: HTML und Cache-Control no-cache; Query bleibt erhalten', async () => {
|
||||
stelle('pilot', [MAIL])
|
||||
const r = await rawGet('/dashboard.html?abo=success', { cookie: customerCookie('c1') })
|
||||
assert.ok(istNeu(r)); assert.equal(r.headers.get('cache-control'), 'no-cache'); assert.match(r.headers.get('content-type'), /^text\/html/)
|
||||
})
|
||||
test('Platzhalter trägt beide Marker (N5) und die Hülle des Briefs', () => {
|
||||
const html = readFileSync(path.join(publicDir, 'neu', 'dashboard.html'), 'utf8')
|
||||
for (const teil of ['<meta name="kn-seite" content="neu">', '<body class="kn">', 'data-kn-platzhalter', 'id="kn-platzhalter-ansicht"', 'Kundenansicht neu', 'Platzhalter bis P1', '/api/auth/me']) assert.ok(html.includes(teil), teil)
|
||||
assert.ok(!/<script[^>]*\btype=["']module/.test(html), 'klassisches Skript, kein Modul')
|
||||
})
|
||||
test('Direktaufruf /neu/dashboard.html: Pilot ja, sonst 302 auf /dashboard.html', async () => {
|
||||
stelle('pilot', [MAIL])
|
||||
assert.ok(istNeu(await rawGet('/neu/dashboard.html', { cookie: customerCookie('c1') })))
|
||||
for (const cookie of [customerCookie('c2'), undefined]) {
|
||||
const r = await rawGet('/neu/dashboard.html', { cookie })
|
||||
assert.deepEqual([r.status, r.headers.get('location')], [302, '/dashboard.html'])
|
||||
}
|
||||
})
|
||||
test('andere Seiten bleiben unberührt', async () => {
|
||||
stelle('pilot', ['*'])
|
||||
for (const p of ['/login.html', '/manifest.webmanifest']) assert.equal((await rawGet(p, { cookie: customerCookie('c1') })).status, 200, p)
|
||||
})
|
||||
test('server/index.js hängt die Weiche zwischen /kalkulation.html und express.static ein', () => {
|
||||
const index = readFileSync(new URL('../server/index.js', import.meta.url), 'utf8')
|
||||
const stelle_ = (s) => index.indexOf(s)
|
||||
const reihe = ["app.use('/kalkulation.html'", "app.get('/dashboard.html', dashboardWeiche({ publicDir }))", "app.get('/neu/dashboard.html', neuDirektWeiche())", "app.get(['/kuendigen.html', '/widerrufen.html'], oeffentlicheSeiteWeiche({ publicDir }))", 'app.use(express.static(publicDir))'].map(stelle_)
|
||||
assert.ok(reihe[0] > -1 && reihe.every((n, i) => i === 0 || n > reihe[i - 1]), reihe.join(','))
|
||||
assert.match(index, /import \{[^}]*\bdashboardWeiche\b[^}]*\bneuDirektWeiche\b[^}]*\boeffentlicheSeiteWeiche\b[^}]*\} from '\.\/services\/kundenansicht\/dashboardWeiche\.js'/)
|
||||
})
|
||||
|
||||
// Nachtrag N39: /kuendigen.html und /widerrufen.html aus public/neu/oeffentlich/, aber nur für Pilot-Sitzungen und nur wenn die Datei da ist
|
||||
const bytes = (datei) => readFileSync(path.join(publicDir, datei), 'utf8')
|
||||
test('N39: ohne Datei in public/neu/oeffentlich/ liefern alle Modi bytegleich die alten Seiten', async () => {
|
||||
for (const [modus, liste] of [['off', ['*']], ['pilot', ['*']], ['on', []]]) {
|
||||
stelle(modus, liste)
|
||||
for (const seite of ['kuendigen.html', 'widerrufen.html']) {
|
||||
const r = await rawGet(`/${seite}`, { cookie: customerCookie('c1') })
|
||||
assert.equal(r.status, 200, `${modus} ${seite}`); assert.equal(r.body, bytes(seite), `${modus} ${seite}`)
|
||||
assert.notEqual(r.headers.get('cache-control'), 'no-cache', `${modus} ${seite}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
test('N39: mit Datei bekommen nur angemeldete Pilot-Sitzungen die neue Seite, alle anderen bytegleich die alte', async () => {
|
||||
const neu = async (cookie) => { const r = await rawGet('/nachbau/kuendigen.html', { cookie }); return r.status === 200 && String(r.body).includes('content="neu-oeffentlich"') ? r : null }
|
||||
stelle('pilot', [MAIL])
|
||||
const r = await neu(customerCookie('c1'))
|
||||
assert.ok(r, 'Pilot-Kunde'); assert.equal(r.headers.get('cache-control'), 'no-cache'); assert.match(r.headers.get('content-type'), /^text\/html/)
|
||||
assert.ok(await neu(kundenansichtCookie()), 'Mitarbeiter-Kundenansicht')
|
||||
for (const [name, cookie] of [['Kunde daneben', customerCookie('c2')], ['ohne Sitzung', undefined], ['unsigniertes Cookie', UNSIGNIERT], ['alte Mitarbeiter-Sitzung', adminCookie()]]) {
|
||||
const alt = await rawGet('/nachbau/kuendigen.html', { cookie })
|
||||
assert.equal(alt.status, 200, name); assert.equal(alt.body, bytes('kuendigen.html'), name); assert.notEqual(alt.headers.get('cache-control'), 'no-cache', name)
|
||||
}
|
||||
stelle('off', ['*']); assert.equal(await neu(customerCookie('c1')), null, 'off schlaegt die Liste')
|
||||
stelle('on', []); assert.ok(await neu(customerCookie('c1')), 'on, Kunde'); assert.equal(await neu(undefined), null, 'on, ohne Sitzung bleibt alt (N39)')
|
||||
})
|
||||
test('N39: nur Seiten mit eigener Datei wechseln; die andere, der Login und Offline bleiben alt', async () => {
|
||||
stelle('pilot', ['*'])
|
||||
const wid = await rawGet('/nachbau/widerrufen.html', { cookie: customerCookie('c1') })
|
||||
assert.deepEqual([wid.status, wid.body], [200, bytes('widerrufen.html')])
|
||||
for (const seite of ['login.html', 'offline.html']) {
|
||||
const r = await rawGet(`/${seite}`, { cookie: customerCookie('c1') })
|
||||
assert.deepEqual([r.status, r.body], [200, bytes(seite)], seite)
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user