Terminplaner: Empfaenger-Kappe ueber Betriebe (G01, Freitext in G04/G06)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c35711caad
commit
4d96565240
5 files changed
+257
-18
No files matched your search
@@ -13,8 +13,11 @@ import { pruefeNow, pruefeText } from './util.js'
|
||||
* Buchung, ein Treffer bekommt seine Mails immer.
|
||||
* Oeffentliche Schnittstelle (routes/publicTermine.js): darfAnfrage / darfAnfrageSite (lesende Abrufe und Token-Aufrufe
|
||||
* je IP und je siteKey), darfBuchungsversuch (je IP), darfHaltung (je siteKey), darfWiderrufsversuch (je IP) und darfG01
|
||||
* (Bestaetigungsmails je Adresse und Funktion). IPv6-Adressen zaehlen je /64 (ipSchluessel): ein Anschluss bekommt meist
|
||||
* (Bestaetigungsmails je Adresse). IPv6-Adressen zaehlen je /64 (ipSchluessel): ein Anschluss bekommt meist
|
||||
* ein ganzes /64, die Adresse darin wechselt beliebig.
|
||||
* Empfaenger-Kappe: eine fremde Adresse bekommt ueber alle Betriebe (Funktionen) zusammen hoechstens 3 Mails je Stunde und
|
||||
* 10 je Tag, die ein Gast oder ein Betrieb ausloest (darfG01 beim Gast-Formular, darfEmpfaenger fuer den Freitext des
|
||||
* Betriebs in G04/G06, mails.js anGast); beide teilen dieselben Zaehler.
|
||||
*
|
||||
* Speicher: zwei getrennte Maps mit je hoechstens 10 000 Schluesseln. Die Zaehler je IP (Schluessel mit 'ip:') haben
|
||||
* beliebig viele, von Angreifern frei waehlbare Schluessel; sie duerfen die langlebigen Zaehler je Adresse, Funktion
|
||||
@@ -46,6 +49,9 @@ const HALTUNGEN_JE_SITE = 25
|
||||
const WIDERRUFE_JE_IP = 10
|
||||
/** G01 (Bestaetigungsmail, auch "zu viele"): je Adresse und Funktion und Tag. */
|
||||
const G01_JE_ADRESSE = 5
|
||||
/** Empfaenger-Kappe ueber alle Funktionen: je (normalisierter) Adresse und Stunde, je Adresse und Tag. */
|
||||
const EMPFAENGER_JE_STUNDE = 3
|
||||
const EMPFAENGER_JE_TAG = 10
|
||||
|
||||
/**
|
||||
* Eine Map schluessel -> { start, ende (ms), n }; die Einfuegereihenfolge ist die Reihenfolge der Fensterbeginne.
|
||||
@@ -195,9 +201,27 @@ export function darfWiderrufsversuch(ip, now) {
|
||||
return zaehle(`ip:widerruf:${ipSchluessel(ip)}`, WIDERRUFE_JE_IP, STUNDE_MS, now)
|
||||
}
|
||||
|
||||
/** Darf an diese (normalisierte) Adresse eine G01 gehen? Hoechstens 5 je Adresse und Funktion und Tag (gehalten wie "zu viele"). */
|
||||
/** Die zwei Regeln der Empfaenger-Kappe (Stunde und Tag) fuer eine normalisierte Adresse, ueber alle Funktionen. */
|
||||
const empfaengerRegeln = (adresse) => [
|
||||
{ schluessel: `empfaenger-h:${adresse}`, max: EMPFAENGER_JE_STUNDE, fensterMs: STUNDE_MS },
|
||||
{ schluessel: `empfaenger-d:${adresse}`, max: EMPFAENGER_JE_TAG, fensterMs: TAG_MS },
|
||||
]
|
||||
|
||||
/**
|
||||
* Darf an diese (normalisierte: klein, getrimmt) Adresse eine G01 gehen? Hoechstens 5 je Adresse und Funktion und Tag
|
||||
* (gehalten wie "zu viele") und die Empfaenger-Kappe ueber alle Funktionen (3 je Stunde, 10 je Tag). Alles oder nichts:
|
||||
* eine abgelehnte Anfrage erhoeht keinen Zaehler.
|
||||
*/
|
||||
export function darfG01(featureId, adresse, now) {
|
||||
return zaehle(`g01:${featureId}:${adresse}`, G01_JE_ADRESSE, TAG_MS, now)
|
||||
return zaehleAlle([{ schluessel: `g01:${featureId}:${adresse}`, max: G01_JE_ADRESSE, fensterMs: TAG_MS }, ...empfaengerRegeln(adresse)], now)
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf an diese (normalisierte) Adresse eine Mail mit Freitext des Betriebs gehen (G04, G06)? Dieselben Zaehler wie
|
||||
* darfG01 (3 je Stunde, 10 je Tag ueber alle Funktionen), aber ohne die Regel je Funktion.
|
||||
*/
|
||||
export function darfEmpfaenger(adresse, now) {
|
||||
return zaehleAlle(empfaengerRegeln(adresse), now)
|
||||
}
|
||||
|
||||
/** Leert alle Zaehler (Tests). */
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getDocument } from '../appwriteAdmin.js'
|
||||
import { preisHinweis, preisText, zahlungText } from './einstellungen.js'
|
||||
import { icsTermin } from './ics.js'
|
||||
import { websiteUrl } from './kontext.js'
|
||||
import { darfEmpfaenger } from './limits.js'
|
||||
import { einreihen } from './outbox.js'
|
||||
import { widerrufsbelehrung } from './rechtstexte.js'
|
||||
import { buchungsNummer, verwaltungsToken } from './token.js'
|
||||
@@ -21,6 +22,8 @@ import { datumKurz, datumLang, uhrzeit } from './zeit.js'
|
||||
* - Person oder Platz (Block person in G02/G05) nur, wenn der Betrieb mehrere Ressourcen hat; K20/K21 zeigen sie immer.
|
||||
* - G01 und G08 enthalten keinen Text, den der Gast eingegeben hat (die Mail geht an eine ungeprueft angegebene Adresse);
|
||||
* in keinem Betreff steht ein Gastname.
|
||||
* - Freitext des Betriebs (G04 nachricht, G06 grund und nachricht bei Absage durch den Betrieb) geht nur bis zur
|
||||
* Empfaenger-Kappe (limits.js darfEmpfaenger) an eine Adresse; danach geht die Mail ohne diesen Text raus (anGast).
|
||||
* - Verwaltungslink `${publicBaseUrl}/termin.html#m=<verwaltungsToken>` (G02, G03, G05, G07), Bestaetigungslink `#v=<token>`
|
||||
* (G01, Token von extra.verifyToken), Portal-Link der Betriebsmails `${publicBaseUrl}/dashboard.html#termine`.
|
||||
*
|
||||
@@ -52,6 +55,8 @@ const GAST_ARTEN = ['G01', 'G02', 'G03', 'G04', 'G05', 'G06', 'G07', 'G08']
|
||||
const BETRIEBS_ARTEN = ['K20', 'K21']
|
||||
const ICS_METHODE = { G02: 'REQUEST', G07: 'REQUEST', G06: 'CANCEL', K20: 'PUBLISH' }
|
||||
const VERIFY_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/
|
||||
/** Felder mit Freitext des Betriebs je Gastmail (G06 nur bei von === 'betrieb'). */
|
||||
const FREITEXT_FELDER = { G04: ['nachricht'], G06: ['grund', 'nachricht'] }
|
||||
|
||||
function einfrieren(wert) {
|
||||
if (wert && typeof wert === 'object' && !Object.isFrozen(wert)) {
|
||||
@@ -458,10 +463,26 @@ function kalenderDatei(art, ctx, buchung, extra, jetzt) {
|
||||
|
||||
// ---------------------------------------------------------------- Einreihen
|
||||
|
||||
/**
|
||||
* Zaehlt eine Mail mit Freitext des Betriebs gegen die Empfaenger-Kappe der Adresse (limits.js darfEmpfaenger) und gibt
|
||||
* das extra zurueck, mit dem die Mail rausgeht: unveraendert, solange die Kappe nicht erreicht ist oder kein Freitext
|
||||
* dabei ist (dann zaehlt nichts), sonst ohne die Freitextfelder (Log-Warnung ohne Adresse). G05, K20/K21 und der Rest
|
||||
* sind nicht betroffen, ebenso G06 des Gastes oder des Systems.
|
||||
*/
|
||||
function begrenzeFreitext(art, extra, empfaenger, now) {
|
||||
const felder = FREITEXT_FELDER[art]
|
||||
if (!felder || (art === 'G06' && extra.von !== 'betrieb') || !felder.some((feld) => hatText(extra[feld]))) return extra
|
||||
const adresse = hatText(empfaenger) ? empfaenger.trim().toLowerCase() : ''
|
||||
if (adresse === '' || darfEmpfaenger(adresse, now)) return extra
|
||||
console.warn(`[termine-mails] ${art}: Empfänger-Kappe erreicht, die Mail geht ohne Freitext raus`)
|
||||
return { ...extra, ...Object.fromEntries(felder.map((feld) => [feld, undefined])) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reiht eine Mail an den Gast ein (Absender: Betriebsname, Antworten an die Betriebs-E-Mail, nie BCC). Empfaenger ist
|
||||
* extra.an, sonst buchung.gastEmail. G02 und G07 tragen eine Einladung (REQUEST), G06 die Absage (CANCEL), jeweils
|
||||
* ausser mit extra.ohneIcs.
|
||||
* ausser mit extra.ohneIcs. Freitext des Betriebs in G04/G06 geht nur bis zur Empfaenger-Kappe der Adresse mit (siehe
|
||||
* begrenzeFreitext).
|
||||
* @param {string} art 'G01' ... 'G08'
|
||||
* @param {object} ctx
|
||||
* @param {object|null} buchung
|
||||
@@ -472,7 +493,8 @@ function kalenderDatei(art, ctx, buchung, extra, jetzt) {
|
||||
export async function anGast(art, ctx, buchung, extra = {}, now = new Date()) {
|
||||
if (!GAST_ARTEN.includes(art)) throw new TypeError(`anGast: ${String(art).slice(0, 20)} ist keine Gastmail`)
|
||||
pruefeNow(now, 'anGast')
|
||||
extra = extra ?? {}
|
||||
const an = hatText(extra?.an) ? extra.an : buchung?.gastEmail
|
||||
extra = begrenzeFreitext(art, extra ?? {}, an, now)
|
||||
const { values, blocks } = werteFuer(art, ctx, buchung, extra)
|
||||
const b = ctx.einstellung.betrieb
|
||||
const ics = extra.ohneIcs === true ? null : kalenderDatei(art, ctx, buchung, extra, now)
|
||||
@@ -481,7 +503,7 @@ export async function anGast(art, ctx, buchung, extra = {}, now = new Date()) {
|
||||
featureId: ctx.feature?.$id,
|
||||
bookingId: buchung?.$id || extra.bookingId || OHNE_BUCHUNG,
|
||||
art,
|
||||
an: hatText(extra.an) ? extra.an : buchung?.gastEmail,
|
||||
an,
|
||||
fromName: b.name,
|
||||
replyTo: b.email,
|
||||
template: art,
|
||||
|
||||
Reference in new issue
Block a user