revert 377971411a
revert feat(api): merkezi rate limiting sistemi ekle Yeni rate-limiter middleware modülü oluşturuldu. loginLimiter (5 istek/dakika), apiLimiter (30 istek/dakika) ve uploadLimiter (10 istek/dakika) tanımlandı. Auth, loop, timer ve torrent rotalarına rate limiting uygulandı. Torrent rotalarında SHA-1 hash validasyonu eklendi.
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