Push-Zeitmail: Review-Fixes (Rate-Limit begrenzt, kein NaN-Datum, Störung ≠ gelöscht)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7d4767a137
commit
6643283c6f
4 files changed
+104
-21
No files matched your search
+44
-12
@@ -13,21 +13,43 @@ import { applyPushTime, formatMinutes, needsTimeEntry, parseMinutes, MAX_MINUTES
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Einfaches Rate-Limit je IP (30 Anfragen / 15 Minuten)
|
||||
// Einfaches Rate-Limit je IP (30 Anfragen / 15 Minuten). Der Speicher bleibt begrenzt: abgelaufene
|
||||
// Eintraege fliegen hoechstens einmal pro Minute raus, und bei mehr als RATE_MAX_KEYS IPs wird der
|
||||
// aelteste Teil verworfen (Map behaelt die Einfuege-Reihenfolge).
|
||||
const RATE_LIMIT = 30
|
||||
const RATE_WINDOW_MS = 15 * 60 * 1000
|
||||
const RATE_MAX_KEYS = 10000
|
||||
const hits = new Map()
|
||||
let lastPrune = 0
|
||||
|
||||
export function resetPushTimeRateLimitForTests() {
|
||||
hits.clear()
|
||||
lastPrune = 0
|
||||
}
|
||||
|
||||
function rateLimited(ip) {
|
||||
const now = Date.now()
|
||||
for (const [key, entry] of hits) if (entry.resetAt <= now) hits.delete(key)
|
||||
const entry = hits.get(ip) || { count: 0, resetAt: now + RATE_WINDOW_MS }
|
||||
export function pushTimeRateLimitSizeForTests() {
|
||||
return hits.size
|
||||
}
|
||||
|
||||
export function isRateLimited(ip, now = Date.now()) {
|
||||
if (now - lastPrune >= 60 * 1000) {
|
||||
lastPrune = now
|
||||
for (const [key, entry] of hits) if (entry.resetAt <= now) hits.delete(key)
|
||||
}
|
||||
let entry = hits.get(ip)
|
||||
if (!entry || entry.resetAt <= now) {
|
||||
hits.delete(ip)
|
||||
if (hits.size >= RATE_MAX_KEYS) {
|
||||
let drop = hits.size - RATE_MAX_KEYS + 1
|
||||
for (const key of hits.keys()) {
|
||||
if (drop-- <= 0) break
|
||||
hits.delete(key)
|
||||
}
|
||||
}
|
||||
entry = { count: 0, resetAt: now + RATE_WINDOW_MS }
|
||||
hits.set(ip, entry)
|
||||
}
|
||||
entry.count += 1
|
||||
hits.set(ip, entry)
|
||||
return entry.count > RATE_LIMIT
|
||||
}
|
||||
|
||||
@@ -86,6 +108,12 @@ function errorPage(res, status, title, text) {
|
||||
page(res, status, title, `<h1>${e(title)}</h1><p class="mut">${e(text)}</p><a class="btn" href="${e(womsHome())}">Zum Ticketsystem →</a>`)
|
||||
}
|
||||
|
||||
/** Stoerung (z. B. Appwrite nicht erreichbar): eigene HTML-Seite statt JSON des globalen Handlers */
|
||||
function failurePage(res, err) {
|
||||
console.error('[pushTime]', err?.message || err)
|
||||
errorPage(res, 503, 'Gerade nicht erreichbar', 'Bitte versuch es gleich noch einmal. Die Zeit wurde nicht gespeichert.')
|
||||
}
|
||||
|
||||
/** Repo und Commit-Titel aus den Arbeitsblatt-Details (Format von gitWorksheet.js) */
|
||||
function pushInfo(details) {
|
||||
const lines = String(details || '').split('\n')
|
||||
@@ -143,7 +171,7 @@ function alreadyPage(res, ws, ticket) {
|
||||
|
||||
/** Token pruefen und Arbeitsblatt laden; schickt bei Fehlern selbst die Seite und liefert null. */
|
||||
async function resolve(req, res) {
|
||||
if (rateLimited(req.ip || 'unbekannt')) {
|
||||
if (isRateLimited(req.ip || 'unbekannt')) {
|
||||
errorPage(res, 429, 'Zu viele Anfragen', 'Bitte versuch es in ein paar Minuten noch einmal.')
|
||||
return null
|
||||
}
|
||||
@@ -159,7 +187,8 @@ async function resolve(req, res) {
|
||||
let ws
|
||||
try {
|
||||
ws = await getDocument(config.collections.worksheets, check.worksheetId)
|
||||
} catch {
|
||||
} catch (err) {
|
||||
if (err?.status !== 404) throw err
|
||||
errorPage(res, 404, 'Nicht mehr vorhanden', 'Dieses Arbeitsblatt gibt es nicht mehr.')
|
||||
return null
|
||||
}
|
||||
@@ -170,18 +199,18 @@ async function resolve(req, res) {
|
||||
return { ws, ticket: await loadTicket(ws) }
|
||||
}
|
||||
|
||||
router.get('/:token', async (req, res, next) => {
|
||||
router.get('/:token', async (req, res) => {
|
||||
try {
|
||||
const found = await resolve(req, res)
|
||||
if (!found) return
|
||||
if (!needsTimeEntry(found.ws)) return alreadyPage(res, found.ws, found.ticket)
|
||||
formPage(res, 200, { ...found, token: req.params.token, minutes: parseMinutes(req.query.min) ?? '' })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
failurePage(res, err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:token', async (req, res, next) => {
|
||||
router.post('/:token', async (req, res) => {
|
||||
try {
|
||||
const found = await resolve(req, res)
|
||||
if (!found) return
|
||||
@@ -199,6 +228,9 @@ router.post('/:token', async (req, res, next) => {
|
||||
|
||||
const result = await applyPushTime(found.ws.$id, minutes)
|
||||
if (result.status === 'already') return alreadyPage(res, result.worksheet, found.ticket)
|
||||
if (result.status === 'invalid') {
|
||||
return errorPage(res, 422, 'Nicht möglich', 'Bei diesem Push fehlt die Uhrzeit. Trag die Zeit bitte direkt im Ticketsystem ein.')
|
||||
}
|
||||
if (result.status !== 'saved') {
|
||||
return errorPage(res, 404, 'Nicht mehr vorhanden', 'Dieses Arbeitsblatt gibt es nicht mehr.')
|
||||
}
|
||||
@@ -210,7 +242,7 @@ router.post('/:token', async (req, res, next) => {
|
||||
`<h1>Danke!</h1><p class="ok">Eingetragen: ${e(formatMinutes(minutes))} auf #${e(woid)}</p>${openTicket(woid)}`
|
||||
)
|
||||
} catch (err) {
|
||||
next(err)
|
||||
failurePage(res, err)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user