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
@@ -11,12 +11,12 @@ export function sessionMiddleware() {
|
|||||||
return cookieParser(config.sessionSecret)
|
return cookieParser(config.sessionSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setPortalSession(res, data) {
|
export function setPortalSession(res, data, { maxAgeMs = SESSION_MAX_AGE_MS } = {}) {
|
||||||
res.cookie(config.cookieName, JSON.stringify(data), {
|
res.cookie(config.cookieName, JSON.stringify(data), {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
maxAge: SESSION_MAX_AGE_MS,
|
maxAge: maxAgeMs,
|
||||||
signed: true,
|
signed: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ import {
|
|||||||
} from '../services/previewSession.js'
|
} from '../services/previewSession.js'
|
||||||
import { isPreviewProjectReady } from '../services/previewReadiness.js'
|
import { isPreviewProjectReady } from '../services/previewReadiness.js'
|
||||||
import { consumeStaffPreviewToken } from '../services/staffPreviewTokens.js'
|
import { consumeStaffPreviewToken } from '../services/staffPreviewTokens.js'
|
||||||
|
import { getPortalAccessStatus } from '../services/portalAccessStatus.js'
|
||||||
|
import { startKundenansicht } from '../services/kundenansicht.js'
|
||||||
|
import { logViewAction } from '../services/customerViewLog.js'
|
||||||
import { resolveStaffAccount } from '../services/staffAccount.js'
|
import { resolveStaffAccount } from '../services/staffAccount.js'
|
||||||
import { MFA_COOKIE, MFA_TTL_MS, resendStaffMfa, startStaffMfa, verifyStaffMfa } from '../services/staffMfa.js'
|
import { MFA_COOKIE, MFA_TTL_MS, resendStaffMfa, startStaffMfa, verifyStaffMfa } from '../services/staffMfa.js'
|
||||||
|
|
||||||
@@ -343,6 +346,28 @@ router.get('/staff-switch', async (req, res) => {
|
|||||||
return res.redirect('/admin.html')
|
return res.redirect('/admin.html')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Kundenansicht aus dem Ticketsystem (POST /api/team/customers/:id/kundenansicht): Einmal-Link -> Portal des Kunden
|
||||||
|
router.get('/kundenansicht', async (req, res) => {
|
||||||
|
const gone = (text) => res.status(410).type('text/plain; charset=utf-8').send(text)
|
||||||
|
const tokenData = consumeStaffPreviewToken(String(req.query.token || ''), 'kundenansicht')
|
||||||
|
if (!tokenData) return gone('Der Link ist abgelaufen. Bitte die Kundenansicht im Ticketsystem erneut öffnen.')
|
||||||
|
try {
|
||||||
|
const { customer, hasAccess } = await getPortalAccessStatus(tokenData.customerId)
|
||||||
|
if (!customer || !hasAccess) return gone('Kein Portal-Zugang. Bitte die Kundenansicht im Ticketsystem erneut öffnen.')
|
||||||
|
const session = await startKundenansicht(res, { staff: tokenData.staff, customer })
|
||||||
|
await logViewAction({
|
||||||
|
customerId: customer.$id,
|
||||||
|
staffUserId: session.appwriteUserId,
|
||||||
|
staffName: session.adminName,
|
||||||
|
action: 'geoeffnet',
|
||||||
|
})
|
||||||
|
return res.redirect('/dashboard.html')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[kundenansicht] Öffnen fehlgeschlagen:', err.message)
|
||||||
|
return res.status(500).type('text/plain; charset=utf-8').send('Die Kundenansicht konnte nicht geöffnet werden.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Selbstheilung: Portal-Session lebt oft laenger als das preview_session-Cookie
|
// Selbstheilung: Portal-Session lebt oft laenger als das preview_session-Cookie
|
||||||
// (bzw. das Cookie fehlt nach WebView-/Browser-Datenverlust). Ohne gueltiges
|
// (bzw. das Cookie fehlt nach WebView-/Browser-Datenverlust). Ohne gueltiges
|
||||||
// Preview-Cookie laufen die *.project.webklar.com-Hosts in eine 302-Schleife.
|
// Preview-Cookie laufen die *.project.webklar.com-Hosts in eine 302-Schleife.
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
import { config } from '../../config.js'
|
||||||
|
import { getPortalAccessStatus } from '../../services/portalAccessStatus.js'
|
||||||
|
import { createStaffPreviewToken } from '../../services/staffPreviewTokens.js'
|
||||||
|
|
||||||
|
// Kunden im Team-Eingang (/api/team/customers), requireStaffJwt sitzt in routes/team/index.js
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
// Kundenansicht oeffnen: Einmal-Link (60 s), den das Ticketsystem in einem neuen Tab aufruft
|
||||||
|
router.post('/:id/kundenansicht', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { customer, hasAccess } = await getPortalAccessStatus(req.params.id)
|
||||||
|
if (!customer) return res.status(404).json({ error: 'Kunde nicht gefunden' })
|
||||||
|
if (!hasAccess) return res.status(409).json({ code: 'no_portal_access', error: 'Kein Portal-Zugang' })
|
||||||
|
const token = createStaffPreviewToken({ purpose: 'kundenansicht', customerId: customer.$id, staff: req.staff })
|
||||||
|
return res.json({ openUrl: `${config.publicBaseUrl}/api/auth/kundenansicht?token=${encodeURIComponent(token)}` })
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: err.message || 'Kundenansicht konnte nicht geöffnet werden' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
import { requireStaffJwt } from '../../middleware/staffJwt.js'
|
import { requireStaffJwt } from '../../middleware/staffJwt.js'
|
||||||
|
import customersRoutes from './customers.js'
|
||||||
|
|
||||||
// Team-Eingang fuer das Ticketsystem: alle Mitarbeiter, Appwrite-JWT (Spec 2026-10-01 Admin-Panel ins Ticketsystem)
|
// Team-Eingang fuer das Ticketsystem: alle Mitarbeiter, Appwrite-JWT (Spec 2026-10-01 Admin-Panel ins Ticketsystem)
|
||||||
const router = Router()
|
const router = Router()
|
||||||
router.use(requireStaffJwt)
|
router.use(requireStaffJwt)
|
||||||
|
router.use('/customers', customersRoutes)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { randomBytes } from 'node:crypto'
|
||||||
|
import { clearPortalSession, setPortalSession } from '../middleware/session.js'
|
||||||
|
import { logViewAction } from './customerViewLog.js'
|
||||||
|
import {
|
||||||
|
PREVIEW_COOKIE_NAME,
|
||||||
|
clearPreviewSession,
|
||||||
|
createPreviewSessionToken,
|
||||||
|
previewSessionCookieOptions,
|
||||||
|
} from './previewSession.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kundenansicht aus dem Ticketsystem (Spec 2026-10-01, Abschnitt 3): ein Mitarbeiter oeffnet per Einmal-Link das
|
||||||
|
* Portal eines Kunden. Die Sitzung traegt eine zufaellige Kennung (viewId) gegen Verwechslung zweier Tabs und
|
||||||
|
* endet nach 2 Stunden. Sitzungen ohne viewId stammen aus "Als Kunde ansehen" im alten Admin-Panel.
|
||||||
|
*/
|
||||||
|
export const KUNDENANSICHT_TTL_MS = 2 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
export const isKundenansicht = (session) => Boolean(session?.viewAs && session.viewId)
|
||||||
|
|
||||||
|
export const isExpired = (session, now = Date.now()) => now >= Number(session?.viewExpiresAt || 0)
|
||||||
|
|
||||||
|
/** Portal-Sitzung und Preview-Zugang fuer genau diesen Kunden setzen; liefert die Sitzung. */
|
||||||
|
export async function startKundenansicht(res, { staff, customer, now = Date.now() }) {
|
||||||
|
const session = {
|
||||||
|
role: 'admin',
|
||||||
|
viewAs: true,
|
||||||
|
viewId: randomBytes(16).toString('hex'),
|
||||||
|
viewExpiresAt: now + KUNDENANSICHT_TTL_MS,
|
||||||
|
isAdmin: Boolean(staff.isAdmin),
|
||||||
|
appwriteUserId: staff.appwriteUserId,
|
||||||
|
adminName: staff.name || 'Mitarbeiter',
|
||||||
|
adminEmail: staff.email || '',
|
||||||
|
customerId: customer.$id,
|
||||||
|
name: customer.name || '',
|
||||||
|
email: customer.email || '',
|
||||||
|
}
|
||||||
|
setPortalSession(res, session, { maxAgeMs: KUNDENANSICHT_TTL_MS })
|
||||||
|
await setKundenansichtPreview(res, session, KUNDENANSICHT_TTL_MS / 1000)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preview-Cookie wie beim Kunden selbst (nur seine Projekte), hoechstens bis zum Ende der Kundenansicht. */
|
||||||
|
export async function setKundenansichtPreview(res, session, maxAgeSec) {
|
||||||
|
const token = await createPreviewSessionToken(
|
||||||
|
{
|
||||||
|
userId: session.appwriteUserId,
|
||||||
|
customerId: session.customerId,
|
||||||
|
email: session.email || '',
|
||||||
|
name: session.name || '',
|
||||||
|
role: 'customer',
|
||||||
|
labels: [],
|
||||||
|
authSource: 'kundenansicht',
|
||||||
|
},
|
||||||
|
{ maxAgeSec }
|
||||||
|
)
|
||||||
|
return res.cookie(PREVIEW_COOKIE_NAME, token, previewSessionCookieOptions({ maxAgeSec }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function endKundenansicht(res) {
|
||||||
|
clearPortalSession(res)
|
||||||
|
clearPreviewSession(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "beendet"/"abgelaufen" je Ansicht nur einmal protokollieren (parallele Anfragen eines Tabs)
|
||||||
|
const LOGGED_TTL_MS = 3 * 60 * 60 * 1000
|
||||||
|
const logged = new Map()
|
||||||
|
|
||||||
|
export async function logOnce(session, action) {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const [key, at] of logged) if (now - at > LOGGED_TTL_MS) logged.delete(key)
|
||||||
|
const key = `${session.viewId}:${action}`
|
||||||
|
if (logged.has(key)) return
|
||||||
|
logged.set(key, now)
|
||||||
|
await logViewAction({
|
||||||
|
customerId: session.customerId,
|
||||||
|
staffUserId: session.appwriteUserId,
|
||||||
|
staffName: session.adminName,
|
||||||
|
action,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { config } from '../config.js'
|
||||||
|
import { getDocument, getPortalAccessByCustomerId } from './appwriteAdmin.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Portal-Zugang eines Kunden fuer das Ticketsystem: hasAccess wie beim Login (customers.portalAccessEnabled und ein
|
||||||
|
* freigeschaltetes customerPortalAccess-Dokument), dazu der letzte Login. customer = null bei unbekanntem Kunden.
|
||||||
|
*/
|
||||||
|
export async function getPortalAccessStatus(customerId) {
|
||||||
|
const customer = await getDocument(config.collections.customers, customerId).catch((err) => {
|
||||||
|
if (err.status === 404) return null
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
if (!customer) return { customer: null, hasAccess: false, lastLoginAt: null }
|
||||||
|
const access = await getPortalAccessByCustomerId(customer.$id)
|
||||||
|
return {
|
||||||
|
customer,
|
||||||
|
hasAccess: Boolean(customer.portalAccessEnabled && access?.enabled),
|
||||||
|
lastLoginAt: access?.lastLoginAt || null,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,12 @@ export const PREVIEW_COOKIE_NAME = 'preview_session'
|
|||||||
|
|
||||||
const secret = new TextEncoder().encode(config.preview.sessionSecret)
|
const secret = new TextEncoder().encode(config.preview.sessionSecret)
|
||||||
|
|
||||||
export async function createPreviewSessionToken(payload) {
|
/** maxAgeSec kuerzer als die Standard-Laufzeit z. B. fuer die Kundenansicht (2 Stunden). */
|
||||||
|
export async function createPreviewSessionToken(payload, { maxAgeSec = config.preview.sessionMaxAge } = {}) {
|
||||||
return new SignJWT(payload)
|
return new SignJWT(payload)
|
||||||
.setProtectedHeader({ alg: 'HS256' })
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
.setIssuedAt()
|
.setIssuedAt()
|
||||||
.setExpirationTime(`${config.preview.sessionMaxAge}s`)
|
.setExpirationTime(`${maxAgeSec}s`)
|
||||||
.sign(secret)
|
.sign(secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,14 +23,14 @@ export async function verifyPreviewSessionToken(token) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function previewSessionCookieOptions() {
|
export function previewSessionCookieOptions({ maxAgeSec = config.preview.sessionMaxAge } = {}) {
|
||||||
return {
|
return {
|
||||||
domain: config.preview.cookieDomain,
|
domain: config.preview.cookieDomain,
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
maxAge: config.preview.sessionMaxAge * 1000,
|
maxAge: maxAgeSec * 1000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export function createStaffPreviewToken(payload) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Einmal einloesen. purpose trennt Preview-Links ('preview', Standard) vom Wechsel ins Projekt-Admin
|
* Einmal einloesen. purpose trennt Preview-Links ('preview', Standard) vom Wechsel ins Projekt-Admin
|
||||||
* ('switch'); ein Token fuer den falschen Zweck ist trotzdem verbraucht.
|
* ('switch') und von der Kundenansicht aus dem Ticketsystem ('kundenansicht'); ein Token fuer den falschen
|
||||||
|
* Zweck ist trotzdem verbraucht.
|
||||||
*/
|
*/
|
||||||
export function consumeStaffPreviewToken(token, purpose = 'preview') {
|
export function consumeStaffPreviewToken(token, purpose = 'preview') {
|
||||||
purgeExpired()
|
purgeExpired()
|
||||||
|
|||||||
@@ -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 || '',
|
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. */
|
/** Kunden c1–c4 und die Abos sub_h (c1) und sub_h2 (c2) in den Appwrite-Nachbau legen. */
|
||||||
export function seedFixtures(aw) {
|
export function seedFixtures(aw) {
|
||||||
for (const customer of Object.values(customers)) aw.seed('customers', customer)
|
for (const customer of Object.values(customers)) aw.seed('customers', customer)
|
||||||
|
|||||||
Reference in new issue
Block a user