diff --git a/apps/server/src/auth/auth.routes.ts b/apps/server/src/auth/auth.routes.ts index 96250c5..16f1daa 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,6 +12,13 @@ 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 78093fd..80abb8c 100644 --- a/apps/server/src/loop/loop.routes.ts +++ b/apps/server/src/loop/loop.routes.ts @@ -10,11 +10,10 @@ 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", apiLimiter, async (req, res) => { +router.post("/start", async (req, res) => { const parsed = loopStartSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: parsed.error.flatten() }); @@ -71,7 +70,7 @@ router.post("/start", apiLimiter, async (req, res) => { res.json(job); }); -router.post("/stop/:jobId", apiLimiter, async (req, res) => { +router.post("/stop/:jobId", async (req, res) => { const { jobId } = req.params; const job = await stopLoopJob(jobId); if (!job) { @@ -86,7 +85,7 @@ router.post("/stop/:jobId", apiLimiter, async (req, res) => { res.json(job); }); -router.post("/stop-by-hash", apiLimiter, async (req, res) => { +router.post("/stop-by-hash", 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 deleted file mode 100644 index 4c9f1ab..0000000 --- a/apps/server/src/middleware/rate-limiter.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 2de4623..2c91d4a 100644 --- a/apps/server/src/timer/timer.routes.ts +++ b/apps/server/src/timer/timer.routes.ts @@ -4,7 +4,6 @@ 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(); @@ -18,7 +17,7 @@ router.get("/rules", async (_req, res) => { res.json(db.timerRules ?? []); }); -router.post("/rules", apiLimiter, async (req, res) => { +router.post("/rules", async (req, res) => { const parsed = ruleSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: parsed.error.flatten() }); @@ -35,7 +34,7 @@ router.post("/rules", apiLimiter, async (req, res) => { res.json(rule); }); -router.delete("/rules/:ruleId", apiLimiter, async (req, res) => { +router.delete("/rules/:ruleId", 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 64df6fe..99347fb 100644 --- a/apps/server/src/torrent/torrent.routes.ts +++ b/apps/server/src/torrent/torrent.routes.ts @@ -7,22 +7,14 @@ 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" }); -// 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) => { +router.post("/select", async (req, res) => { const { hash } = req.body ?? {}; - if (!hash || !isValidHash(hash)) { - return res.status(400).json({ error: "Geçersiz hash formatı" }); + if (!hash) { + return res.status(400).json({ error: "Missing hash" }); } const existing = await getArchiveStatus(hash); if (existing?.status === "READY") { @@ -44,10 +36,10 @@ router.post("/select", apiLimiter, async (req, res) => { res.json({ ok: true, hash, archive: { status: "MISSING" } }); }); -router.post("/archive/from-selected", apiLimiter, async (req, res) => { +router.post("/archive/from-selected", async (req, res) => { const { hash } = req.body ?? {}; - if (!hash || !isValidHash(hash)) { - return res.status(400).json({ error: "Geçersiz hash formatı" }); + if (!hash) { + return res.status(400).json({ error: "Missing hash" }); } const existing = await getArchiveStatus(hash); if (existing?.status === "READY") { @@ -67,10 +59,10 @@ router.post("/archive/from-selected", apiLimiter, async (req, res) => { return res.status(400).json({ error: "Magnet export disabled; upload .torrent manually." }); }); -router.post("/archive/upload", uploadLimiter, upload.single("file"), async (req, res) => { +router.post("/archive/upload", upload.single("file"), async (req, res) => { const { hash } = req.body ?? {}; - if (!hash || !req.file || !isValidHash(hash)) { - return res.status(400).json({ error: "Geçersiz hash formatı veya dosya eksik" }); + if (!hash || !req.file) { + return res.status(400).json({ error: "Missing hash or file" }); } const inputHash = String(hash).toLowerCase(); const buffer = await fs.readFile(req.file.path); @@ -119,13 +111,9 @@ router.post("/archive/upload", uploadLimiter, upload.single("file"), async (req, }); router.get("/archive/status/:hash", async (req, res) => { - const { hash } = req.params; - if (!isValidHash(hash)) { - return res.status(400).json({ error: "Geçersiz hash formatı" }); - } - const status = await getArchiveStatus(hash); + const status = await getArchiveStatus(req.params.hash); if (!status) { - return res.json({ hash, status: "MISSING" }); + return res.json({ hash: req.params.hash, status: "MISSING" }); } return res.json(status); });