- websiteProjects.shareToken (scripts/vorschau-link-setup.mjs) - POST/DELETE /api/admin/website-projects/:id/vorschau-link fuer Mitarbeiter (neu erzeugen / abschalten) - GET /api/vorschau-link/pruefen fuer die Login-Pruefung der Previews (preview-portal /auth/verify) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
5.4 KiB
JavaScript
125 lines
5.4 KiB
JavaScript
// Geheimer Vorschau-Link: Link erzeugen, einloesen (Cookie + Weiterleitung), pruefen, abschalten
|
|
import { test, before, after } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import express from 'express'
|
|
import { startFakeAppwrite } from './helpers/fakeAppwrite.js'
|
|
|
|
const ready = { subdomain: 'kunde-a', projectType: 'preview', provisioningStatus: 'deployed', customerId: 'c1' }
|
|
|
|
let aw, server, baseUrl, svc
|
|
before(async () => {
|
|
aw = await startFakeAppwrite({
|
|
seed: {
|
|
websiteProjects: [
|
|
{ $id: 'p1', ...ready },
|
|
{ $id: 'p2', ...ready, subdomain: 'baut-noch', provisioningStatus: 'pending', shareToken: 'Pending_Token_123456789' },
|
|
{ $id: 'p3', projectName: 'ohne Subdomain', subdomain: '' },
|
|
{ $id: 'p4', ...ready, subdomain: 'archiv', archived: true, shareToken: 'Archiv_Token_1234567890' },
|
|
],
|
|
},
|
|
})
|
|
Object.assign(process.env, { APPWRITE_ENDPOINT: aw.endpoint, APPWRITE_API_KEY: 'test-key', PREVIEW_SESSION_SECRET: 'x'.repeat(40) })
|
|
svc = await import('../server/services/vorschauLink.js')
|
|
const { vorschauLinkSeite, vorschauLinkPruefung } = await import('../server/routes/vorschauLink.js')
|
|
const app = express()
|
|
app.use('/vorschau', vorschauLinkSeite())
|
|
app.use('/api/vorschau-link', vorschauLinkPruefung())
|
|
server = await new Promise((r) => { const s = app.listen(0, '127.0.0.1', () => r(s)) })
|
|
baseUrl = `http://127.0.0.1:${server.address().port}`
|
|
})
|
|
after(async () => {
|
|
server.closeAllConnections()
|
|
await new Promise((r) => server.close(r))
|
|
await aw.close()
|
|
})
|
|
|
|
const doc = (id) => aw.all('websiteProjects').find((d) => d.$id === id)
|
|
const pruefen = (sub, token) =>
|
|
fetch(`${baseUrl}/api/vorschau-link/pruefen?subdomain=${sub}`, { headers: token ? { 'x-vorschau-token': token } : {} })
|
|
|
|
test('Token: zufaellig, URL-tauglich, lang genug', () => {
|
|
const a = svc.newShareToken()
|
|
assert.match(a, /^[A-Za-z0-9_-]{24}$/)
|
|
assert.notEqual(a, svc.newShareToken())
|
|
assert.equal(svc.cleanToken(a), a)
|
|
assert.equal(svc.cleanToken('kurz'), '')
|
|
assert.equal(svc.cleanToken('../../etc/passwd/xxxxxxxxxxxxxxx'), '')
|
|
assert.equal(svc.cleanSubdomain('Kunde-A'), 'kunde-a')
|
|
assert.equal(svc.cleanSubdomain('-x'), '')
|
|
assert.equal(svc.cleanSubdomain('a.b'), '')
|
|
})
|
|
|
|
test('Link erzeugen speichert den Token und baut die URL', async () => {
|
|
const r = await svc.renewShareLink('p1')
|
|
assert.equal(doc('p1').shareToken, r.shareToken)
|
|
assert.equal(r.url, `https://project.webklar.com/vorschau/kunde-a/${r.shareToken}`)
|
|
})
|
|
|
|
test('Ohne Subdomain gibt es keinen Link (409), unbekanntes Projekt 404', async () => {
|
|
await assert.rejects(svc.renewShareLink('p3'), (e) => e.status === 409)
|
|
await assert.rejects(svc.renewShareLink('gibt-es-nicht'), (e) => e.status === 404)
|
|
})
|
|
|
|
test('Link einloesen: Cookie nur fuer diese Subdomain, Weiterleitung auf die Preview', async () => {
|
|
const { shareToken } = await svc.renewShareLink('p1')
|
|
const res = await fetch(`${baseUrl}/vorschau/kunde-a/${shareToken}`, { redirect: 'manual' })
|
|
assert.equal(res.status, 302)
|
|
assert.equal(res.headers.get('location'), 'https://kunde-a.project.webklar.com/')
|
|
const cookie = res.headers.get('set-cookie')
|
|
assert.match(cookie, new RegExp(`^wk_vorschau_kunde-a=${shareToken};`))
|
|
assert.match(cookie, /Domain=\.project\.webklar\.com/)
|
|
assert.match(cookie, /HttpOnly/)
|
|
assert.match(cookie, /SameSite=Lax/)
|
|
assert.equal(res.headers.get('cache-control'), 'no-store')
|
|
})
|
|
|
|
test('Falscher Token, fremde Subdomain oder Muell: 404-Seite ohne Cookie', async () => {
|
|
const { shareToken } = await svc.renewShareLink('p1')
|
|
for (const path of [
|
|
`/vorschau/kunde-a/${shareToken.slice(0, -1)}X`,
|
|
`/vorschau/archiv/${shareToken}`,
|
|
`/vorschau/kunde-a/kurz`,
|
|
`/vorschau/KUNDE-A.evil/${shareToken}`,
|
|
]) {
|
|
const res = await fetch(baseUrl + path, { redirect: 'manual' })
|
|
assert.equal(res.status, 404, path)
|
|
assert.equal(res.headers.get('set-cookie'), null, path)
|
|
assert.match(await res.text(), /nicht \(mehr\) gültig/)
|
|
}
|
|
})
|
|
|
|
test('Archiviertes Projekt: Link gilt nicht', async () => {
|
|
const res = await pruefen('archiv', 'Archiv_Token_1234567890')
|
|
assert.equal(res.status, 403)
|
|
})
|
|
|
|
test('Preview noch nicht fertig: 503-Hinweis, Pruefung sagt not_ready', async () => {
|
|
const res = await fetch(`${baseUrl}/vorschau/baut-noch/Pending_Token_123456789`, { redirect: 'manual' })
|
|
assert.equal(res.status, 503)
|
|
assert.equal(res.headers.get('set-cookie'), null)
|
|
const p = await pruefen('baut-noch', 'Pending_Token_123456789')
|
|
assert.deepEqual(await p.json(), { allowed: false, reason: 'not_ready' })
|
|
})
|
|
|
|
test('Pruefung: richtiger Token 200, sonst 403', async () => {
|
|
const { shareToken } = await svc.renewShareLink('p1')
|
|
let res = await pruefen('kunde-a', shareToken)
|
|
assert.equal(res.status, 200)
|
|
assert.equal((await res.json()).allowed, true)
|
|
res = await pruefen('kunde-a', svc.newShareToken())
|
|
assert.equal(res.status, 403)
|
|
res = await pruefen('kunde-a', '')
|
|
assert.equal(res.status, 403)
|
|
})
|
|
|
|
test('Neuer Link oder Abschalten macht den alten sofort ungueltig', async () => {
|
|
const alt = (await svc.renewShareLink('p1')).shareToken
|
|
assert.equal((await pruefen('kunde-a', alt)).status, 200)
|
|
const neu = (await svc.renewShareLink('p1')).shareToken
|
|
assert.equal((await pruefen('kunde-a', alt)).status, 403)
|
|
assert.equal((await pruefen('kunde-a', neu)).status, 200)
|
|
await svc.removeShareLink('p1')
|
|
assert.equal(doc('p1').shareToken, '')
|
|
assert.equal((await pruefen('kunde-a', neu)).status, 403)
|
|
})
|