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:
@@ -1,7 +1,7 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
import rateLimit from "express-rate-limit";
|
||||||
import { signToken, verifyCredentials, verifyToken } from "./auth.service"
|
import { signToken, verifyCredentials, verifyToken } from "./auth.service"
|
||||||
import { isDev } from "../config"
|
import { isDev } from "../config"
|
||||||
import { loginLimiter } from "../middleware/rate-limiter"
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -12,6 +12,13 @@ const getAuthToken = (req: any) => {
|
|||||||
return cookieToken || bearer;
|
return cookieToken || bearer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loginLimiter = rateLimit({
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 5,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
router.post("/login", loginLimiter, async (req, res) => {
|
router.post("/login", loginLimiter, async (req, res) => {
|
||||||
const { username, password } = req.body ?? {};
|
const { username, password } = req.body ?? {};
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ import { config } from "../config";
|
|||||||
import { setArchiveStatus } from "../torrent/torrent.archive";
|
import { setArchiveStatus } from "../torrent/torrent.archive";
|
||||||
import { nowIso } from "../utils/time";
|
import { nowIso } from "../utils/time";
|
||||||
import { readLoopLogs } from "../storage/loopLogs";
|
import { readLoopLogs } from "../storage/loopLogs";
|
||||||
import { apiLimiter } from "../middleware/rate-limiter";
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post("/start", apiLimiter, async (req, res) => {
|
router.post("/start", async (req, res) => {
|
||||||
const parsed = loopStartSchema.safeParse(req.body);
|
const parsed = loopStartSchema.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
@@ -71,7 +70,7 @@ router.post("/start", apiLimiter, async (req, res) => {
|
|||||||
res.json(job);
|
res.json(job);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/stop/:jobId", apiLimiter, async (req, res) => {
|
router.post("/stop/:jobId", async (req, res) => {
|
||||||
const { jobId } = req.params;
|
const { jobId } = req.params;
|
||||||
const job = await stopLoopJob(jobId);
|
const job = await stopLoopJob(jobId);
|
||||||
if (!job) {
|
if (!job) {
|
||||||
@@ -86,7 +85,7 @@ router.post("/stop/:jobId", apiLimiter, async (req, res) => {
|
|||||||
res.json(job);
|
res.json(job);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/stop-by-hash", apiLimiter, async (req, res) => {
|
router.post("/stop-by-hash", async (req, res) => {
|
||||||
const { hash } = req.body ?? {};
|
const { hash } = req.body ?? {};
|
||||||
if (!hash) {
|
if (!hash) {
|
||||||
return res.status(400).json({ error: "Missing hash" });
|
return res.status(400).json({ error: "Missing hash" });
|
||||||
|
|||||||
@@ -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." },
|
|
||||||
});
|
|
||||||
@@ -4,7 +4,6 @@ import { readDb, writeDb } from "../storage/jsondb";
|
|||||||
import { TimerRule } from "../types";
|
import { TimerRule } from "../types";
|
||||||
import { nowIso } from "../utils/time";
|
import { nowIso } from "../utils/time";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiLimiter } from "../middleware/rate-limiter";
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ router.get("/rules", async (_req, res) => {
|
|||||||
res.json(db.timerRules ?? []);
|
res.json(db.timerRules ?? []);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/rules", apiLimiter, async (req, res) => {
|
router.post("/rules", async (req, res) => {
|
||||||
const parsed = ruleSchema.safeParse(req.body);
|
const parsed = ruleSchema.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
@@ -35,7 +34,7 @@ router.post("/rules", apiLimiter, async (req, res) => {
|
|||||||
res.json(rule);
|
res.json(rule);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete("/rules/:ruleId", apiLimiter, async (req, res) => {
|
router.delete("/rules/:ruleId", async (req, res) => {
|
||||||
const db = await readDb();
|
const db = await readDb();
|
||||||
const next = (db.timerRules ?? []).filter((rule) => rule.id !== req.params.ruleId);
|
const next = (db.timerRules ?? []).filter((rule) => rule.id !== req.params.ruleId);
|
||||||
if (next.length === (db.timerRules ?? []).length) {
|
if (next.length === (db.timerRules ?? []).length) {
|
||||||
|
|||||||
@@ -7,22 +7,14 @@ import { getArchiveStatus, setArchiveStatus } from "./torrent.archive";
|
|||||||
import { nowIso } from "../utils/time";
|
import { nowIso } from "../utils/time";
|
||||||
import { appendAuditLog, logger } from "../utils/logger";
|
import { appendAuditLog, logger } from "../utils/logger";
|
||||||
import { config } from "../config";
|
import { config } from "../config";
|
||||||
import { apiLimiter, uploadLimiter } from "../middleware/rate-limiter";
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const upload = multer({ dest: "/tmp" });
|
const upload = multer({ dest: "/tmp" });
|
||||||
|
|
||||||
// qBittorrent hash'leri 40 karakter hexadecimal (SHA-1)
|
router.post("/select", async (req, res) => {
|
||||||
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 ?? {};
|
const { hash } = req.body ?? {};
|
||||||
if (!hash || !isValidHash(hash)) {
|
if (!hash) {
|
||||||
return res.status(400).json({ error: "Geçersiz hash formatı" });
|
return res.status(400).json({ error: "Missing hash" });
|
||||||
}
|
}
|
||||||
const existing = await getArchiveStatus(hash);
|
const existing = await getArchiveStatus(hash);
|
||||||
if (existing?.status === "READY") {
|
if (existing?.status === "READY") {
|
||||||
@@ -44,10 +36,10 @@ router.post("/select", apiLimiter, async (req, res) => {
|
|||||||
res.json({ ok: true, hash, archive: { status: "MISSING" } });
|
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 ?? {};
|
const { hash } = req.body ?? {};
|
||||||
if (!hash || !isValidHash(hash)) {
|
if (!hash) {
|
||||||
return res.status(400).json({ error: "Geçersiz hash formatı" });
|
return res.status(400).json({ error: "Missing hash" });
|
||||||
}
|
}
|
||||||
const existing = await getArchiveStatus(hash);
|
const existing = await getArchiveStatus(hash);
|
||||||
if (existing?.status === "READY") {
|
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." });
|
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 ?? {};
|
const { hash } = req.body ?? {};
|
||||||
if (!hash || !req.file || !isValidHash(hash)) {
|
if (!hash || !req.file) {
|
||||||
return res.status(400).json({ error: "Geçersiz hash formatı veya dosya eksik" });
|
return res.status(400).json({ error: "Missing hash or file" });
|
||||||
}
|
}
|
||||||
const inputHash = String(hash).toLowerCase();
|
const inputHash = String(hash).toLowerCase();
|
||||||
const buffer = await fs.readFile(req.file.path);
|
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) => {
|
router.get("/archive/status/:hash", async (req, res) => {
|
||||||
const { hash } = req.params;
|
const status = await getArchiveStatus(req.params.hash);
|
||||||
if (!isValidHash(hash)) {
|
|
||||||
return res.status(400).json({ error: "Geçersiz hash formatı" });
|
|
||||||
}
|
|
||||||
const status = await getArchiveStatus(hash);
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return res.json({ hash, status: "MISSING" });
|
return res.json({ hash: req.params.hash, status: "MISSING" });
|
||||||
}
|
}
|
||||||
return res.json(status);
|
return res.json(status);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user