News: Cache-Buster für Bild-URLs, HEAD, Limiter-Feinschliff

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-09-30 16:52:47 +00:00
1 parent 319f8937aa
commit a421308019
5 files changed
+57 -12

No files matched your search

+3 -3
View File
@@ -82,7 +82,7 @@ router.get('/', requireSession, withProject(), async (req, res) => {
}
})
router.post('/', requireSession, rejectViewAs, limitWrites, withProject(), async (req, res) => {
router.post('/', requireSession, rejectViewAs, withProject(), limitWrites, async (req, res) => {
try {
const post = await createPost({ customerId: getSessionCustomerId(req), projectId: req.projectId, input: req.body })
res.status(201).json(post)
@@ -131,7 +131,7 @@ router.get('/image/:projectId/:file', requireSession, async (req, res) => {
}
})
router.patch('/:id', requireSession, rejectViewAs, limitWrites, withProject(), async (req, res) => {
router.patch('/:id', requireSession, rejectViewAs, withProject(), limitWrites, async (req, res) => {
try {
const post = await updatePost({ customerId: getSessionCustomerId(req), projectId: req.projectId, id: req.params.id, input: req.body })
res.json(post)
@@ -140,7 +140,7 @@ router.patch('/:id', requireSession, rejectViewAs, limitWrites, withProject(), a
}
})
router.delete('/:id', requireSession, rejectViewAs, limitWrites, withProject(), async (req, res) => {
router.delete('/:id', requireSession, rejectViewAs, withProject(), limitWrites, async (req, res) => {
try {
await deletePost({ customerId: getSessionCustomerId(req), projectId: req.projectId, id: req.params.id })
res.status(204).end()
+10 -4
View File
@@ -19,10 +19,16 @@ const IMAGE_FILE_RE = /^([A-Za-z0-9][A-Za-z0-9._-]*?)(-s)?\.webp$/
// Limit je IP und Minute (im Speicher); die App steht hinter Traefik, 'trust proxy' ist gesetzt -> req.ip ist die Client-IP
const hits = new Map()
let lastPrune = 0
function rateLimited(ip) {
const now = Date.now()
for (const [key, entry] of hits) if (now - entry.start >= WINDOW_MS) hits.delete(key)
// Aufraeumen hoechstens einmal je Fenster
if (now - lastPrune >= WINDOW_MS) {
lastPrune = now
for (const [key, entry] of hits) if (now - entry.start >= WINDOW_MS) hits.delete(key)
}
let entry = hits.get(ip)
if (entry && now - entry.start >= WINDOW_MS) entry = null
if (!entry) {
entry = { start: now, count: 0 }
hits.set(ip, entry)
@@ -34,12 +40,12 @@ function rateLimited(ip) {
router.use((req, res, next) => {
res.set({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, If-None-Match',
'Access-Control-Max-Age': '86400',
})
if (req.method === 'OPTIONS') return res.sendStatus(204)
if (req.method !== 'GET') return res.status(405).json({ error: 'Methode nicht erlaubt' })
if (req.method !== 'GET' && req.method !== 'HEAD') return res.status(405).json({ error: 'Methode nicht erlaubt' })
if (rateLimited(req.ip)) {
res.set('Retry-After', '60')
return res.status(429).json({ error: 'Zu viele Anfragen. Bitte versuch es gleich noch einmal.' })
@@ -67,7 +73,7 @@ router.get('/:siteKey', async (req, res) => {
return sendJsonWithEtag(req, res, {
posts: posts.map((p) => ({
...p,
image: p.image ? { w: p.image.w, h: p.image.h, src: `${base}/${p.id}.webp`, srcSmall: `${base}/${p.id}-s.webp` } : null,
image: p.image ? { w: p.image.w, h: p.image.h, src: `${base}/${p.id}.webp?v=${p.image.v}`, srcSmall: `${base}/${p.id}-s.webp?v=${p.image.v}` } : null,
})),
hasMore,
})