Kundenansicht: Einmal-Link aus dem Ticketsystem, Sitzung und Preview-Zugang fuer 2 Stunden, Protokoll
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fdf594706d
commit
1cd776a2ee
10 files changed
+277
-7
No files matched your search
@@ -0,0 +1,103 @@
|
||||
import { test, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
|
||||
// Kundenansicht aus dem Ticketsystem: Einmal-Link (60 s) -> Portal-Sitzung des Kunden fuer 2 Stunden
|
||||
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, post, rawGet } = await import('./support/testApp.js')
|
||||
const { seedFixtures } = await import('./support/fixtures.js')
|
||||
const { default: authRoutes } = await import('../server/routes/auth.js')
|
||||
const { default: teamRoutes } = await import('../server/routes/team/index.js')
|
||||
const { createStaffPreviewToken } = await import('../server/services/staffPreviewTokens.js')
|
||||
const { verifyPreviewSessionToken } = await import('../server/services/previewSession.js')
|
||||
const app = await startTestApp({ '/api/auth': authRoutes, '/api/team': teamRoutes })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
|
||||
seedFixtures(aw)
|
||||
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
||||
aw.seed('customerPortalAccess', { $id: 'pa1', customerId: 'c1', enabled: true, lastLoginAt: '2026-09-30T10:00:00.000Z' })
|
||||
aw.seed('customerPortalAccess', { $id: 'pa3', customerId: 'c3', enabled: false })
|
||||
|
||||
const bearer = (jwt) => ({ headers: { authorization: `Bearer ${jwt}` } })
|
||||
const link = (customerId, jwt = 'jwt-staff') => post(`/api/team/customers/${customerId}/kundenansicht`, {}, bearer(jwt))
|
||||
const tokenFor = async (customerId) => new URL((await link(customerId)).body.openUrl).searchParams.get('token')
|
||||
const setCookie = (res, name) => res.headers.getSetCookie().find((c) => c.startsWith(`${name}=`)) || null
|
||||
const cookieValue = (res, name) => {
|
||||
const c = setCookie(res, name)
|
||||
return c ? decodeURIComponent(c.split(';')[0].slice(name.length + 1)) : null
|
||||
}
|
||||
const portalSession = (res) => {
|
||||
const raw = cookieValue(res, config.cookieName)
|
||||
return raw ? JSON.parse(raw.slice(2, raw.lastIndexOf('.'))) : null
|
||||
}
|
||||
const logEntries = () => [...aw.docs('customerViewLog').values()]
|
||||
|
||||
test('Link fuer Mitarbeiter: Einmal-Link auf /api/auth/kundenansicht', async () => {
|
||||
const res = await link('c1')
|
||||
assert.equal(res.status, 200)
|
||||
assert.match(res.body.openUrl, /^https:\/\/project\.webklar\.com\/api\/auth\/kundenansicht\?token=[0-9a-f]{64}$/)
|
||||
})
|
||||
|
||||
test('Link ohne JWT 401, fuer Kunden-JWT 403', async () => {
|
||||
assert.equal((await post('/api/team/customers/c1/kundenansicht', {})).status, 401)
|
||||
assert.equal((await link('c1', 'jwt-kunde')).status, 403)
|
||||
})
|
||||
|
||||
test('unbekannter Kunde 404, Kunde ohne Portal-Zugang 409 no_portal_access', async () => {
|
||||
assert.equal((await link('gibtsnicht')).status, 404)
|
||||
for (const id of ['c2', 'c3']) {
|
||||
const res = await link(id)
|
||||
assert.equal(res.status, 409, id)
|
||||
assert.equal(res.body.code, 'no_portal_access', id)
|
||||
}
|
||||
})
|
||||
|
||||
test('Einloesen: Sitzung 2 h, Preview-Cookie nur fuer den Kunden, Protokoll geoeffnet, Weiter zum Dashboard', async () => {
|
||||
const before = logEntries().length
|
||||
const res = await rawGet(`/api/auth/kundenansicht?token=${await tokenFor('c1')}`)
|
||||
assert.equal(res.status, 302)
|
||||
assert.equal(res.headers.get('location'), '/dashboard.html')
|
||||
const s = portalSession(res)
|
||||
assert.equal(s.viewAs, true)
|
||||
assert.equal(s.role, 'admin')
|
||||
assert.match(s.viewId, /^[0-9a-f]{32}$/)
|
||||
assert.ok(Math.abs(s.viewExpiresAt - (Date.now() + 7200000)) < 5000)
|
||||
assert.deepEqual([s.customerId, s.adminName, s.appwriteUserId, s.name], ['c1', 'Andrej', 'u_staff2', 'Kunde Eins'])
|
||||
assert.match(setCookie(res, config.cookieName), /Max-Age=7200/)
|
||||
const preview = await verifyPreviewSessionToken(cookieValue(res, 'preview_session'))
|
||||
assert.deepEqual([preview.role, preview.customerId, preview.userId], ['customer', 'c1', 'u_staff2'])
|
||||
assert.ok(preview.exp - preview.iat <= 7200)
|
||||
assert.match(setCookie(res, 'preview_session'), /Max-Age=7200/)
|
||||
const all = logEntries()
|
||||
assert.equal(all.length, before + 1)
|
||||
const log = all.at(-1)
|
||||
assert.deepEqual([log.customerId, log.action, log.staffName, log.staffUserId], ['c1', 'geoeffnet', 'Andrej', 'u_staff2'])
|
||||
})
|
||||
|
||||
test('Token zweimal -> 410 mit Hinweis, keine Sitzung', async () => {
|
||||
const path = `/api/auth/kundenansicht?token=${await tokenFor('c1')}`
|
||||
assert.equal((await rawGet(path)).status, 302)
|
||||
const again = await rawGet(path)
|
||||
assert.equal(again.status, 410)
|
||||
assert.match(again.body, /im Ticketsystem erneut öffnen/)
|
||||
assert.equal(portalSession(again), null)
|
||||
})
|
||||
|
||||
test('Token nach 61 s -> 410', async (t) => {
|
||||
const token = await tokenFor('c1')
|
||||
const now = Date.now()
|
||||
t.mock.method(Date, 'now', () => now + 61 * 1000)
|
||||
assert.equal((await rawGet(`/api/auth/kundenansicht?token=${token}`)).status, 410)
|
||||
})
|
||||
|
||||
test('Preview-Token gilt hier nicht, Kundenansicht-Token nicht als Preview-Link', async () => {
|
||||
const previewToken = createStaffPreviewToken({ userId: 'u_admin', email: 'kenso@webklar.com', name: 'Kenso', previewUrl: 'https://x.project.webklar.com/' })
|
||||
assert.equal((await rawGet(`/api/auth/kundenansicht?token=${previewToken}`)).status, 410)
|
||||
assert.equal((await rawGet(`/api/auth/staff-preview?token=${await tokenFor('c1')}`)).status, 410)
|
||||
})
|
||||
@@ -110,6 +110,22 @@ export const viewAsCookie = (customerId = 'c1') =>
|
||||
email: customers[customerId]?.email || '',
|
||||
})
|
||||
|
||||
/** Kundenansicht aus dem Ticketsystem (Einmal-Link, 2 h, Kennung viewId): Mitarbeiter Andrej ohne Admin-Label. */
|
||||
export const kundenansichtCookie = ({ customerId = 'c1', viewId = 'a'.repeat(32), viewExpiresAt = Date.now() + 3600000 } = {}) =>
|
||||
sessionCookie({
|
||||
role: 'admin',
|
||||
viewAs: true,
|
||||
viewId,
|
||||
viewExpiresAt,
|
||||
isAdmin: false,
|
||||
appwriteUserId: 'u_staff2',
|
||||
adminName: 'Andrej',
|
||||
adminEmail: 'andrej@webklar.com',
|
||||
customerId,
|
||||
name: customers[customerId]?.name || '',
|
||||
email: customers[customerId]?.email || '',
|
||||
})
|
||||
|
||||
/** Kunden c1–c4 und die Abos sub_h (c1) und sub_h2 (c2) in den Appwrite-Nachbau legen. */
|
||||
export function seedFixtures(aw) {
|
||||
for (const customer of Object.values(customers)) aw.seed('customers', customer)
|
||||
|
||||
Reference in new issue
Block a user