Buchungskalender: Statusmaschine, Token und Rechnungsnummern
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
802b5d3ad4
commit
947e9da2e3
9 files changed
+760
No files matched your search
@@ -0,0 +1,78 @@
|
||||
import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto'
|
||||
import { config } from '../../config.js'
|
||||
import { ipSchluessel } from '../termine/limits.js'
|
||||
|
||||
/**
|
||||
* Tokens des Buchungskalenders. Buchungslink-Token: 32 Byte zufaellig (base64url), gespeichert wird nur der SHA-256-Hash.
|
||||
* Formular-Token: Kopie von shop/token.js mit eigener Beschriftung 'uk-form' ("<Zeit base36>.<HMAC>" je siteKey).
|
||||
*/
|
||||
|
||||
const FORM_MAX_ALTER_MS = 2 * 60 * 60 * 1000
|
||||
const FORM_TOKEN_MAX_LAENGE = 100
|
||||
const MAC_RE = /^[A-Za-z0-9_-]{43}$/
|
||||
const ZEIT_RE = /^[0-9a-z]{1,13}$/
|
||||
const MIN_STANDARD_SEKUNDEN = 3
|
||||
|
||||
const sha256Hex = (text) => createHash('sha256').update(text).digest('hex')
|
||||
|
||||
export function neuesToken() {
|
||||
const token = randomBytes(32).toString('base64url')
|
||||
return { token, hash: sha256Hex(token) }
|
||||
}
|
||||
|
||||
/** Zeitkonstanter Vergleich des gespeicherten Hashes mit dem Hash des vorgelegten Tokens. */
|
||||
export function tokenPasst(hash, token) {
|
||||
if (typeof hash !== 'string' || hash === '' || typeof token !== 'string' || token === '' || token.length > 200) return false
|
||||
const erwartet = Buffer.from(hash)
|
||||
const gegeben = Buffer.from(sha256Hex(token))
|
||||
return erwartet.length === gegeben.length && timingSafeEqual(erwartet, gegeben)
|
||||
}
|
||||
|
||||
export const formMinSekunden = () => {
|
||||
const wert = config.unterkunft?.formMinSekunden
|
||||
return Number.isFinite(wert) && wert >= 0 ? wert : MIN_STANDARD_SEKUNDEN
|
||||
}
|
||||
|
||||
function schluessel() {
|
||||
const geheimnis = config.sessionSecret
|
||||
if (typeof geheimnis !== 'string' || geheimnis.length < 32) {
|
||||
throw new Error('Buchungskalender-Token: SESSION_SECRET fehlt oder ist zu kurz (mindestens 32 Zeichen).')
|
||||
}
|
||||
return createHmac('sha256', geheimnis).update('uk-form').digest()
|
||||
}
|
||||
|
||||
const zeitMs = (now, was) => {
|
||||
const ms = now instanceof Date ? now.getTime() : Number(now)
|
||||
if (!Number.isFinite(ms) || ms < 0) throw new TypeError(`${was}: now ist kein gueltiger Zeitpunkt`)
|
||||
return ms
|
||||
}
|
||||
|
||||
const formMac = (siteKey, zeit) => createHmac('sha256', schluessel()).update(`${siteKey}|${zeit}`).digest('base64url')
|
||||
|
||||
export function formToken(siteKey, now = new Date()) {
|
||||
if (typeof siteKey !== 'string' || siteKey === '') throw new TypeError('formToken: siteKey fehlt')
|
||||
const zeit = zeitMs(now, 'formToken').toString(36)
|
||||
return `${zeit}.${formMac(siteKey, zeit)}`
|
||||
}
|
||||
|
||||
/** 'ok' | 'zu_schnell' | 'abgelaufen' (aelter als 2 h) | 'ungueltig'. */
|
||||
export function pruefeFormToken(siteKey, token, now = new Date()) {
|
||||
if (typeof siteKey !== 'string' || siteKey === '') return 'ungueltig'
|
||||
if (typeof token !== 'string' || token.length > FORM_TOKEN_MAX_LAENGE) return 'ungueltig'
|
||||
const teile = token.split('.')
|
||||
if (teile.length !== 2) return 'ungueltig'
|
||||
const [zeit, mac] = teile
|
||||
if (!ZEIT_RE.test(zeit) || !MAC_RE.test(mac)) return 'ungueltig'
|
||||
const erwartet = Buffer.from(formMac(siteKey, zeit))
|
||||
const gegeben = Buffer.from(mac)
|
||||
if (gegeben.length !== erwartet.length || !timingSafeEqual(gegeben, erwartet)) return 'ungueltig'
|
||||
const alter = zeitMs(now, 'pruefeFormToken') - parseInt(zeit, 36)
|
||||
if (alter > FORM_MAX_ALTER_MS) return 'abgelaufen'
|
||||
if (alter < formMinSekunden() * 1000) return 'zu_schnell'
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
/** Gesalzener Kurz-Hash der IP (16 Hex-Zeichen); die Adresse selbst wird nie gespeichert. */
|
||||
export function ipHashVon(ip) {
|
||||
return createHash('sha256').update(`uk-ip:${config.sessionSecret}:${ipSchluessel(ip)}`).digest('hex').slice(0, 16)
|
||||
}
|
||||
Reference in new issue
Block a user