From a4de80b98dce3c2515346a8fa574c92f6e7c8e4a Mon Sep 17 00:00:00 2001 From: wisecolt Date: Mon, 5 Jan 2026 17:31:19 +0000 Subject: [PATCH] revert 9f3b2cbb24de4def79e1a402485a8f26ceab0cca MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit revert refactor(api): rate limiting sistemini basitleştir ve sadece login endpoint'inde tut Merkezi rate limiting middleware dosyasını kaldırıp rate limiting'i sadece login endpoint'ine özel hale getirildi. Diğer API endpoint'lerindeki rate limiting kısıtlamaları (loop, timer, torrent) kaldırıldı. Login rate limiter artık auth.routes.ts dosyasında inline olarak tanımlanıyor. --- apps/server/src/auth/auth.routes.ts | 9 +----- apps/server/src/loop/loop.routes.ts | 7 ++-- apps/server/src/middleware/rate-limiter.ts | 37 ++++++++++++++++++++++ apps/server/src/timer/timer.routes.ts | 5 +-- apps/server/src/torrent/torrent.routes.ts | 34 +++++++++++++------- 5 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 apps/server/src/middleware/rate-limiter.ts diff --git a/apps/server/src/auth/auth.routes.ts b/apps/server/src/auth/auth.routes.ts index 16f1daa..96250c5 100644 --- a/apps/server/src/auth/auth.routes.ts +++ b/apps/server/src/auth/auth.routes.ts @@ -1,7 +1,7 @@ import { Router } from "express"; -import rateLimit from "express-rate-limit"; import { signToken, verifyCredentials, verifyToken } from "./auth.service" import { isDev } from "../config" +import { loginLimiter } from "../middleware/rate-limiter" const router = Router(); @@ -12,13 +12,6 @@ const getAuthToken = (req: any) => { return cookieToken || bearer; }; -const loginLimiter = rateLimit({ - windowMs: 60_000, - max: 5, - standardHeaders: true, - legacyHeaders: false, -}); - router.post("/login", loginLimiter, async (req, res) => { const { username, password } = req.body ?? {}; if (!username || !password) { diff --git a/apps/server/src/loop/loop.routes.ts b/apps/server/src/loop/loop.routes.ts index 80abb8c..78093fd 100644 --- a/apps/server/src/loop/loop.routes.ts +++ b/apps/server/src/loop/loop.routes.ts @@ -10,10 +10,11 @@ import { config } from "../config"; import { setArchiveStatus } from "../torrent/torrent.archive"; import { nowIso } from "../utils/time"; import { readLoopLogs } from "../storage/loopLogs"; +import { apiLimiter } from "../middleware/rate-limiter"; const router = Router(); -router.post("/start", async (req, res) => { +router.post("/start", apiLimiter, async (req, res) => { const parsed = loopStartSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: parsed.error.flatten() }); @@ -70,7 +71,7 @@ router.post("/start", async (req, res) => { res.json(job); }); -router.post("/stop/:jobId", async (req, res) => { +router.post("/stop/:jobId", apiLimiter, async (req, res) => { const { jobId } = req.params; const job = await stopLoopJob(jobId); if (!job) { @@ -85,7 +86,7 @@ router.post("/stop/:jobId", async (req, res) => { res.json(job); }); -router.post("/stop-by-hash", async (req, res) => { +router.post("/stop-by-hash", apiLimiter, async (req, res) => { const { hash } = req.body ?? {}; if (!hash) { return res.status(400).json({ error: "Missing hash" }); diff --git a/apps/server/src/middleware/rate-limiter.ts b/apps/server/src/middleware/rate-limiter.ts new file mode 100644 index 0000000..4c9f1ab --- /dev/null +++ b/apps/server/src/middleware/rate-limiter.ts @@ -0,0 +1,37 @@ +import rateLimit from "express-rate-limit"; + +/** + * Login endpoint'i için sıkı rate limiter + * 5 istek / dakika + */ +export const loginLimiter = rateLimit({ + windowMs: 60_000, + max: 5, + standardHeaders: true, + legacyHeaders: false, + message: { error: "Çok fazla giriş denemesi. Lütfen 1 dakika bekleyin." }, +}); + +/** + * State-changing işlemler için rate limiter + * 30 istek / dakika + */ +export const apiLimiter = rateLimit({ + windowMs: 60_000, + max: 30, + standardHeaders: true, + legacyHeaders: false, + message: { error: "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin." }, +}); + +/** + * Dosya yükleme için rate limiter + * 10 istek / dakika + */ +export const uploadLimiter = rateLimit({ + windowMs: 60_000, + max: 10, + standardHeaders: true, + legacyHeaders: false, + message: { error: "Yükleme limiti aşıldı. Lütfen daha sonra tekrar deneyin." }, +}); diff --git a/apps/server/src/timer/timer.routes.ts b/apps/server/src/timer/timer.routes.ts index 2c91d4a..2de4623 100644 --- a/apps/server/src/timer/timer.routes.ts +++ b/apps/server/src/timer/timer.routes.ts @@ -4,6 +4,7 @@ import { readDb, writeDb } from "../storage/jsondb"; import { TimerRule } from "../types"; import { nowIso } from "../utils/time"; import { z } from "zod"; +import { apiLimiter } from "../middleware/rate-limiter"; const router = Router(); @@ -17,7 +18,7 @@ router.get("/rules", async (_req, res) => { res.json(db.timerRules ?? []); }); -router.post("/rules", async (req, res) => { +router.post("/rules", apiLimiter, async (req, res) => { const parsed = ruleSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: parsed.error.flatten() }); @@ -34,7 +35,7 @@ router.post("/rules", async (req, res) => { res.json(rule); }); -router.delete("/rules/:ruleId", async (req, res) => { +router.delete("/rules/:ruleId", apiLimiter, async (req, res) => { const db = await readDb(); const next = (db.timerRules ?? []).filter((rule) => rule.id !== req.params.ruleId); if (next.length === (db.timerRules ?? []).length) { diff --git a/apps/server/src/torrent/torrent.routes.ts b/apps/server/src/torrent/torrent.routes.ts index 99347fb..64df6fe 100644 --- a/apps/server/src/torrent/torrent.routes.ts +++ b/apps/server/src/torrent/torrent.routes.ts @@ -7,14 +7,22 @@ import { getArchiveStatus, setArchiveStatus } from "./torrent.archive"; import { nowIso } from "../utils/time"; import { appendAuditLog, logger } from "../utils/logger"; import { config } from "../config"; +import { apiLimiter, uploadLimiter } from "../middleware/rate-limiter"; const router = Router(); const upload = multer({ dest: "/tmp" }); -router.post("/select", async (req, res) => { +// qBittorrent hash'leri 40 karakter hexadecimal (SHA-1) +const VALID_HASH_REGEX = /^[a-f0-9]{40}$/i; + +function isValidHash(hash: string): boolean { + return VALID_HASH_REGEX.test(hash); +} + +router.post("/select", apiLimiter, async (req, res) => { const { hash } = req.body ?? {}; - if (!hash) { - return res.status(400).json({ error: "Missing hash" }); + if (!hash || !isValidHash(hash)) { + return res.status(400).json({ error: "Geçersiz hash formatı" }); } const existing = await getArchiveStatus(hash); if (existing?.status === "READY") { @@ -36,10 +44,10 @@ router.post("/select", async (req, res) => { res.json({ ok: true, hash, archive: { status: "MISSING" } }); }); -router.post("/archive/from-selected", async (req, res) => { +router.post("/archive/from-selected", apiLimiter, async (req, res) => { const { hash } = req.body ?? {}; - if (!hash) { - return res.status(400).json({ error: "Missing hash" }); + if (!hash || !isValidHash(hash)) { + return res.status(400).json({ error: "Geçersiz hash formatı" }); } const existing = await getArchiveStatus(hash); if (existing?.status === "READY") { @@ -59,10 +67,10 @@ router.post("/archive/from-selected", async (req, res) => { return res.status(400).json({ error: "Magnet export disabled; upload .torrent manually." }); }); -router.post("/archive/upload", upload.single("file"), async (req, res) => { +router.post("/archive/upload", uploadLimiter, upload.single("file"), async (req, res) => { const { hash } = req.body ?? {}; - if (!hash || !req.file) { - return res.status(400).json({ error: "Missing hash or file" }); + if (!hash || !req.file || !isValidHash(hash)) { + return res.status(400).json({ error: "Geçersiz hash formatı veya dosya eksik" }); } const inputHash = String(hash).toLowerCase(); const buffer = await fs.readFile(req.file.path); @@ -111,9 +119,13 @@ router.post("/archive/upload", upload.single("file"), async (req, res) => { }); router.get("/archive/status/:hash", async (req, res) => { - const status = await getArchiveStatus(req.params.hash); + const { hash } = req.params; + if (!isValidHash(hash)) { + return res.status(400).json({ error: "Geçersiz hash formatı" }); + } + const status = await getArchiveStatus(hash); if (!status) { - return res.json({ hash: req.params.hash, status: "MISSING" }); + return res.json({ hash, status: "MISSING" }); } return res.json(status); });