import { Router, type Request, type Response, type NextFunction } from "express"; import { writeFileSync, mkdirSync } from "fs"; import { join } from "path"; import { randomUUID } from "crypto"; import multer from "multer"; import { extractToken } from "../auth.js"; import { isHfStorageEnabled, setUserToken, uploadImageToHf } from "../hf-storage.js"; import { getDataDir } from "../utils.js"; // Simple in-memory sliding-window rate limiter. // The upload route is intentionally not auth-gated (see docs/TESTS.md 4.1.3), // so we rely on per-IP bucketing to prevent anonymous abuse. const UPLOAD_WINDOW_MS = 60_000; const UPLOAD_MAX_PER_WINDOW = 30; const uploadBuckets = new Map(); function rateLimitUpload(req: Request, res: Response, next: NextFunction): void { const key = (req.headers["x-forwarded-for"] as string | undefined)?.split(",")[0]?.trim() || req.ip || req.socket.remoteAddress || "unknown"; const now = Date.now(); const cutoff = now - UPLOAD_WINDOW_MS; const bucket = (uploadBuckets.get(key) || []).filter((ts) => ts > cutoff); if (bucket.length >= UPLOAD_MAX_PER_WINDOW) { const retryAfter = Math.ceil((bucket[0] + UPLOAD_WINDOW_MS - now) / 1000); res.setHeader("Retry-After", String(Math.max(1, retryAfter))); res.status(429).json({ error: "Too many uploads, please slow down." }); return; } bucket.push(now); uploadBuckets.set(key, bucket); next(); } export function createUploadRouter(): Router { const router = Router(); const uploadMiddleware = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } }); router.post("/api/upload", rateLimitUpload, uploadMiddleware.single("file"), async (req, res) => { try { const file = (req as any).file as Express.Multer.File | undefined; if (!file) { res.status(400).json({ error: "No file provided" }); return; } const ext = file.originalname.split(".").pop() || "png"; const filename = `${randomUUID()}.${ext}`; const userToken = extractToken(req.headers.cookie) ?? undefined; if (userToken) setUserToken(userToken); if (isHfStorageEnabled()) { const url = await uploadImageToHf(file.buffer, filename, userToken); res.json({ url }); return; } const uploadsDir = join(getDataDir(), "uploads"); mkdirSync(uploadsDir, { recursive: true }); writeFileSync(join(uploadsDir, filename), file.buffer); res.json({ url: `/uploads/${filename}` }); } catch (err: any) { console.error("[upload] error:", err); res.status(500).json({ error: err.message || "Upload failed" }); } }); return router; }