From 712af0c898b732d71da5ce4269c21619138b3181 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Sun, 4 Jan 2026 15:20:12 +0300 Subject: [PATCH] =?UTF-8?q?feat(auth):=20bearer=20token=20deste=C4=9Fi=20v?= =?UTF-8?q?e=20=C3=A7oklu=20origin=20ayar=C4=B1=20ekle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Authorization header ile Bearer token kimlik doğrulaması eklendi - Token'ların localStorage'da saklanması desteği eklendi - WEB_ALLOWED_ORIGINS ve WEB_ALLOWED_HOSTS konfigürasyonları eklendi - Loop işlerinde profileId ve profileName alanları eklendi - CORS ve Vite sunucusu için çoklu origin desteği sağlandı --- .env.example | 2 +- apps/server/src/auth/auth.middleware.ts | 5 ++- apps/server/src/auth/auth.routes.ts | 13 ++++++-- apps/server/src/config.ts | 1 + apps/server/src/index.ts | 7 ++++- apps/server/src/loop/loop.engine.ts | 4 +++ apps/server/src/loop/loop.routes.ts | 4 ++- apps/server/src/types.ts | 4 +++ apps/web/src/api/client.ts | 11 +++++++ .../web/src/components/loop/LoopSetupCard.tsx | 5 +++ .../src/components/torrents/TorrentTable.tsx | 9 ++++++ apps/web/src/store/useAppStore.ts | 2 ++ apps/web/src/store/useAuthStore.ts | 5 +++ apps/web/vite.config.ts | 31 ++++++++++++------- 14 files changed, 85 insertions(+), 18 deletions(-) diff --git a/.env.example b/.env.example index b95265f..8aa55be 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,6 @@ APP_USERNAME=qbuffer APP_PASSWORD=changeme JWT_SECRET=replace_me APP_HOST=localhost -WEB_ORIGIN=http://localhost:5173 SERVER_PORT=3001 WEB_PORT=5173 POLL_INTERVAL_MS=3000 @@ -15,3 +14,4 @@ MAX_LOOP_LIMIT=1000 STALLED_RECOVERY_MS=300000 TIMER_POLL_MS=60000 NODE_ENV=development +WEB_ALLOWED_ORIGINS=http://192.168.1.205:5175,http://qbuffer.bee diff --git a/apps/server/src/auth/auth.middleware.ts b/apps/server/src/auth/auth.middleware.ts index 62f8231..6df0c52 100644 --- a/apps/server/src/auth/auth.middleware.ts +++ b/apps/server/src/auth/auth.middleware.ts @@ -2,7 +2,10 @@ import { Request, Response, NextFunction } from "express"; import { verifyToken } from "./auth.service" export const requireAuth = (req: Request, res: Response, next: NextFunction) => { - const token = req.cookies?.["qbuffer_token"]; + const cookieToken = req.cookies?.["qbuffer_token"]; + const authHeader = req.headers.authorization; + const bearer = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : undefined; + const token = cookieToken || bearer; if (!token) { return res.status(401).json({ error: "Unauthorized" }); } diff --git a/apps/server/src/auth/auth.routes.ts b/apps/server/src/auth/auth.routes.ts index 652916c..16f1daa 100644 --- a/apps/server/src/auth/auth.routes.ts +++ b/apps/server/src/auth/auth.routes.ts @@ -5,6 +5,13 @@ import { isDev } from "../config" const router = Router(); +const getAuthToken = (req: any) => { + const cookieToken = req.cookies?.["qbuffer_token"]; + const header = req.headers?.authorization as string | undefined; + const bearer = header?.startsWith("Bearer ") ? header.slice(7) : undefined; + return cookieToken || bearer; +}; + const loginLimiter = rateLimit({ windowMs: 60_000, max: 5, @@ -28,7 +35,7 @@ router.post("/login", loginLimiter, async (req, res) => { secure: !isDev, maxAge: 60 * 24 * 60 * 60 * 1000, }); - return res.json({ username: user.username }); + return res.json({ username: user.username, token }); }); router.post("/logout", (_req, res) => { @@ -41,7 +48,7 @@ router.post("/logout", (_req, res) => { }); router.get("/me", (req, res) => { - const token = req.cookies?.["qbuffer_token"]; + const token = getAuthToken(req); if (!token) { return res.status(401).json({ error: "Unauthorized" }); } @@ -54,7 +61,7 @@ router.get("/me", (req, res) => { }); router.get("/socket-token", (req, res) => { - const token = req.cookies?.["qbuffer_token"]; + const token = getAuthToken(req); if (!token) { return res.status(401).json({ error: "Unauthorized" }); } diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index 3771331..51b82bd 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -22,6 +22,7 @@ export const config = { timerPollMs: envNumber(process.env.TIMER_POLL_MS, 60_000), webPort: envNumber(process.env.WEB_PORT, 5173), webOrigin: process.env.WEB_ORIGIN ?? "", + webAllowedOrigins: process.env.WEB_ALLOWED_ORIGINS ?? "", dataDir: "/app/data", dbPath: "/app/data/db.json", logsPath: "/app/data/logs.json", diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 72e1d6a..cee32ca 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -45,7 +45,12 @@ const bootstrap = async () => { if (isDev) { const fallbackOrigin = `http://localhost:${config.webPort}`; - const origins = [config.webOrigin || fallbackOrigin, fallbackOrigin]; + const originList = [config.webOrigin || fallbackOrigin, fallbackOrigin]; + const allowed = config.webAllowedOrigins + .split(",") + .map((origin) => origin.trim()) + .filter(Boolean); + const origins = [...originList, ...allowed]; app.use( cors({ origin: origins, diff --git a/apps/server/src/loop/loop.engine.ts b/apps/server/src/loop/loop.engine.ts index 1f4d77e..94e38da 100644 --- a/apps/server/src/loop/loop.engine.ts +++ b/apps/server/src/loop/loop.engine.ts @@ -27,6 +27,8 @@ export const createLoopJob = async ( allowIp: string; targetLoops: number; delayMs: number; + profileName?: string; + profileId?: string; } ): Promise => { const now = nowIso(); @@ -41,6 +43,8 @@ export const createLoopJob = async ( targetLoops: input.targetLoops, doneLoops: 0, delayMs: input.delayMs, + profileName: input.profileName, + profileId: input.profileId, deleteDataBetweenLoops: true, enforcementMode: "aggressive-soft", status: "RUNNING", diff --git a/apps/server/src/loop/loop.routes.ts b/apps/server/src/loop/loop.routes.ts index 63ae366..80abb8c 100644 --- a/apps/server/src/loop/loop.routes.ts +++ b/apps/server/src/loop/loop.routes.ts @@ -18,7 +18,7 @@ router.post("/start", async (req, res) => { if (!parsed.success) { return res.status(400).json({ error: parsed.error.flatten() }); } - const { hash, allowIp, targetLoops, delayMs } = parsed.data; + const { hash, allowIp, targetLoops, delayMs, profileName, profileId } = parsed.data; const db = await readDb(); if (targetLoops > db.settings.maxLoopLimit) { return res.status(400).json({ error: "Target loops exceed max limit" }); @@ -64,6 +64,8 @@ router.post("/start", async (req, res) => { allowIp, targetLoops, delayMs, + profileName, + profileId, }); res.json(job); }); diff --git a/apps/server/src/types.ts b/apps/server/src/types.ts index 2ce8ca9..2945651 100644 --- a/apps/server/src/types.ts +++ b/apps/server/src/types.ts @@ -32,6 +32,8 @@ export interface LoopJob { targetLoops: number; doneLoops: number; delayMs: number; + profileName?: string; + profileId?: string; deleteDataBetweenLoops: boolean; enforcementMode: EnforcementMode; status: LoopStatus; @@ -62,6 +64,8 @@ export interface Profile { name: string; allowIp: string; delayMs: number; + profileName?: string; + profileId?: string; targetLoops: number; createdAt: string; } diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index 3ae367a..1c63ef3 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -6,3 +6,14 @@ export const api = axios.create({ baseURL: baseURL || undefined, withCredentials: true, }); + +api.interceptors.request.use((config) => { + const token = localStorage.getItem("qbuffer_token"); + if (token) { + config.headers = config.headers || {}; + if (!config.headers.Authorization) { + config.headers.Authorization = `Bearer ${token}`; + } + } + return config; +}); diff --git a/apps/web/src/components/loop/LoopSetupCard.tsx b/apps/web/src/components/loop/LoopSetupCard.tsx index c53437d..7964bbd 100644 --- a/apps/web/src/components/loop/LoopSetupCard.tsx +++ b/apps/web/src/components/loop/LoopSetupCard.tsx @@ -45,6 +45,9 @@ export const LoopSetupCard = () => { if (!selectedHash) return false; const job = jobs.find((j) => j.torrentHash === selectedHash && j.status === "RUNNING"); if (!job) return false; + if (job.profileId) { + return job.profileId === profile.id; + } return ( job.allowIp === profile.allowIp && job.delayMs === profile.delayMs && @@ -144,6 +147,8 @@ export const LoopSetupCard = () => { allowIp: profile.allowIp, targetLoops: profile.targetLoops, delayMs: profile.delayMs, + profileName: profile.name, + profileId: profile.id, }); setLoopForm({ allowIp: profile.allowIp, diff --git a/apps/web/src/components/torrents/TorrentTable.tsx b/apps/web/src/components/torrents/TorrentTable.tsx index b992a24..64434ae 100644 --- a/apps/web/src/components/torrents/TorrentTable.tsx +++ b/apps/web/src/components/torrents/TorrentTable.tsx @@ -77,6 +77,15 @@ export const TorrentTable = () => { const getProfileName = (hash: string) => { const job = jobs.find((j) => j.torrentHash === hash); if (!job) return null; + if (job.profileId) { + const profileById = profiles.find((p) => p.id === job.profileId); + if (profileById?.name) { + return profileById.name; + } + } + if (job.profileName) { + return job.profileName; + } const profile = profiles.find((p) => p.allowIp === job.allowIp && p.delayMs === job.delayMs && diff --git a/apps/web/src/store/useAppStore.ts b/apps/web/src/store/useAppStore.ts index 05db578..b638d24 100644 --- a/apps/web/src/store/useAppStore.ts +++ b/apps/web/src/store/useAppStore.ts @@ -25,6 +25,8 @@ export interface LoopJob { targetLoops: number; doneLoops: number; delayMs: number; + profileName?: string; + profileId?: string; status: string; bans: { bannedIps: string[] }; nextRunAt?: string; diff --git a/apps/web/src/store/useAuthStore.ts b/apps/web/src/store/useAuthStore.ts index 696f4e8..ed0f4e7 100644 --- a/apps/web/src/store/useAuthStore.ts +++ b/apps/web/src/store/useAuthStore.ts @@ -18,6 +18,9 @@ export const useAuthStore = create((set) => ({ set({ loading: true, error: null }); try { const response = await api.post("/api/auth/login", { username, password }); + if (response.data?.token) { + localStorage.setItem("qbuffer_token", response.data.token); + } set({ username: response.data.username, loading: false }); return true; } catch (error) { @@ -27,6 +30,7 @@ export const useAuthStore = create((set) => ({ }, logout: async () => { await api.post("/api/auth/logout"); + localStorage.removeItem("qbuffer_token"); set({ username: null }); }, check: async () => { @@ -34,6 +38,7 @@ export const useAuthStore = create((set) => ({ const response = await api.get("/api/auth/me"); set({ username: response.data.username ?? "session" }); } catch (error) { + localStorage.removeItem("qbuffer_token"); set({ username: null }); } }, diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index f2a0e3c..19cb559 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,15 +1,24 @@ -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; import path from "node:path"; -export default defineConfig({ - plugins: [react()], - build: { - outDir: path.resolve(__dirname, "../server/public"), - emptyOutDir: true, - }, - server: { - host: "0.0.0.0", - port: Number(process.env.WEB_PORT) || 5173, - }, +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, path.resolve(__dirname, "../.."), ""); + const allowedHosts = (env.WEB_ALLOWED_HOSTS || "") + .split(",") + .map((host) => host.trim().replace(/^"|"$/g, "")) + .filter(Boolean); + + return { + plugins: [react()], + build: { + outDir: path.resolve(__dirname, "../server/public"), + emptyOutDir: true, + }, + server: { + host: "0.0.0.0", + port: Number(env.WEB_PORT) || 5173, + allowedHosts: allowedHosts.length ? allowedHosts : true, + }, + }; });