[object Object]

← back to NationalPaperHangers

admin/uploads: per-installer rate limit (30/hr) keyed on installer.id — protects upload dir from runaway/hostile authenticated client

8c19628eb4eb42dd6ec2ad15c7f1d3b91219b3df · 2026-05-06 11:47:59 -0700 · Steve

Files touched

Diff

commit 8c19628eb4eb42dd6ec2ad15c7f1d3b91219b3df
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 6 11:47:59 2026 -0700

    admin/uploads: per-installer rate limit (30/hr) keyed on installer.id — protects upload dir from runaway/hostile authenticated client
---
 routes/admin.js | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/routes/admin.js b/routes/admin.js
index f92c997..3b3be36 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -3,6 +3,7 @@ const path = require('path');
 const fs = require('fs');
 const crypto = require('crypto');
 const multer = require('multer');
+const rateLimit = require('express-rate-limit');
 const db = require('../lib/db');
 const { requireInstaller, requirePaidTier } = require('../lib/auth');
 const stripe = require('../lib/stripe');
@@ -645,7 +646,18 @@ router.post('/template', async (req, res, next) => {
 // Drag-drop image upload (used by /admin/template hero + portfolio)
 // ===================================================================
 
-router.post('/uploads', (req, res, next) => {
+// Per-installer rate limit on uploads — caps a runaway/hostile authenticated
+// installer at 30 uploads / hour. Keyed on installer.id (not IP) so studios
+// behind shared NATs don't share a bucket. requireInstaller above guarantees
+// req.installer is present at this point.
+const uploadLimiter = rateLimit({
+  windowMs: 60 * 60 * 1000, max: 30,
+  standardHeaders: true, legacyHeaders: false,
+  keyGenerator: (req) => 'inst:' + (req.installer && req.installer.id || 'anon'),
+  message: { ok: false, error: 'Upload rate limit reached — try again in an hour.' }
+});
+
+router.post('/uploads', uploadLimiter, (req, res, next) => {
   upload.single('file')(req, res, async (err) => {
     if (err) {
       const msg = err.message === 'UNSUPPORTED_TYPE'

← a7e5ecf watch page: ops-curated YT videos about pro installations +  ·  back to NationalPaperHangers  ·  claude-codex r3 cleanup: iframe width/height (CLS fix, MEDIU dd2fcde →