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.
This commit is contained in:
2026-01-05 17:31:19 +00:00
parent 9f3b2cbb24
commit a4de80b98d
5 changed files with 68 additions and 24 deletions

View File

@@ -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) {

View File

@@ -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" });

View File

@@ -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." },
});

View File

@@ -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) {

View File

@@ -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);
});