Files
Webklar-Kundenbereich/server/services/analyticsClient.js
T
2026-10-01 13:32:21 +00:00

67 lines
2.5 KiB
JavaScript

// Client fuer die Admin-API des Analytics-Collectors (interner Listener, Port 8091).
// Token und Secret erscheinen nie in Fehlermeldungen oder Logs.
import crypto from 'node:crypto'
import { config } from '../config.js'
const HEAT_TTL_SECONDS = 30 * 60
function fail(message, status) {
const err = new Error(message)
err.status = status
return err
}
export function createAnalyticsClient(opts = {}) {
const cfg = { ...config.analyse, ...opts }
const timeoutMs = opts.timeoutMs || 10000
async function request(method, path, body) {
if (!cfg.adminToken) throw fail('Auswertung nicht eingerichtet', 503)
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
try {
const response = await fetch(`${String(cfg.url).replace(/\/$/, '')}${path}`, {
method,
headers: {
Authorization: `Bearer ${cfg.adminToken}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
})
const data = await response.json().catch(() => ({}))
if (!response.ok) throw fail(`Auswertung antwortet mit Fehler ${response.status}`, response.status)
return data
} catch (e) {
if (e.status) throw e
if (e.name === 'AbortError') throw fail('Auswertung antwortet nicht (Zeitueberschreitung)', 504)
throw fail('Auswertung nicht erreichbar', 502)
} finally {
clearTimeout(timer)
}
}
const sitePath = (siteKey) => `/a/admin/sites/${encodeURIComponent(siteKey)}`
return {
putSite(siteKey, { projectId, domains, active }) {
return request('PUT', sitePath(siteKey), { projectId, domains, active })
},
deleteSite(siteKey) {
return request('DELETE', sitePath(siteKey))
},
getStats(siteKey, range) {
return request('GET', `/a/admin/stats/${encodeURIComponent(siteKey)}?range=${encodeURIComponent(range)}`)
},
// Format (R3): <b64url(JSON {s,p,v,e})>.<hex HMAC-SHA256(heatSecret, b64part)>, byte-gleich zu analytics/src/heat.js
heatToken({ siteKey, path, vp, now = Date.now() }) {
if (!cfg.heatSecret) throw fail('Auswertung nicht eingerichtet', 503)
const exp = Math.floor(now / 1000) + HEAT_TTL_SECONDS
const part = Buffer.from(JSON.stringify({ s: siteKey, p: path, v: vp, e: exp })).toString('base64url')
return `${part}.${crypto.createHmac('sha256', cfg.heatSecret).update(part).digest('hex')}`
},
}
}
export const analyticsClient = createAnalyticsClient()