feat(auth): bearer token desteği ve çoklu origin ayarı ekle
- 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ı
This commit is contained in:
@@ -5,7 +5,6 @@ APP_USERNAME=qbuffer
|
|||||||
APP_PASSWORD=changeme
|
APP_PASSWORD=changeme
|
||||||
JWT_SECRET=replace_me
|
JWT_SECRET=replace_me
|
||||||
APP_HOST=localhost
|
APP_HOST=localhost
|
||||||
WEB_ORIGIN=http://localhost:5173
|
|
||||||
SERVER_PORT=3001
|
SERVER_PORT=3001
|
||||||
WEB_PORT=5173
|
WEB_PORT=5173
|
||||||
POLL_INTERVAL_MS=3000
|
POLL_INTERVAL_MS=3000
|
||||||
@@ -15,3 +14,4 @@ MAX_LOOP_LIMIT=1000
|
|||||||
STALLED_RECOVERY_MS=300000
|
STALLED_RECOVERY_MS=300000
|
||||||
TIMER_POLL_MS=60000
|
TIMER_POLL_MS=60000
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
WEB_ALLOWED_ORIGINS=http://192.168.1.205:5175,http://qbuffer.bee
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { Request, Response, NextFunction } from "express";
|
|||||||
import { verifyToken } from "./auth.service"
|
import { verifyToken } from "./auth.service"
|
||||||
|
|
||||||
export const requireAuth = (req: Request, res: Response, next: NextFunction) => {
|
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) {
|
if (!token) {
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ import { isDev } from "../config"
|
|||||||
|
|
||||||
const router = Router();
|
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({
|
const loginLimiter = rateLimit({
|
||||||
windowMs: 60_000,
|
windowMs: 60_000,
|
||||||
max: 5,
|
max: 5,
|
||||||
@@ -28,7 +35,7 @@ router.post("/login", loginLimiter, async (req, res) => {
|
|||||||
secure: !isDev,
|
secure: !isDev,
|
||||||
maxAge: 60 * 24 * 60 * 60 * 1000,
|
maxAge: 60 * 24 * 60 * 60 * 1000,
|
||||||
});
|
});
|
||||||
return res.json({ username: user.username });
|
return res.json({ username: user.username, token });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/logout", (_req, res) => {
|
router.post("/logout", (_req, res) => {
|
||||||
@@ -41,7 +48,7 @@ router.post("/logout", (_req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/me", (req, res) => {
|
router.get("/me", (req, res) => {
|
||||||
const token = req.cookies?.["qbuffer_token"];
|
const token = getAuthToken(req);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
@@ -54,7 +61,7 @@ router.get("/me", (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/socket-token", (req, res) => {
|
router.get("/socket-token", (req, res) => {
|
||||||
const token = req.cookies?.["qbuffer_token"];
|
const token = getAuthToken(req);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const config = {
|
|||||||
timerPollMs: envNumber(process.env.TIMER_POLL_MS, 60_000),
|
timerPollMs: envNumber(process.env.TIMER_POLL_MS, 60_000),
|
||||||
webPort: envNumber(process.env.WEB_PORT, 5173),
|
webPort: envNumber(process.env.WEB_PORT, 5173),
|
||||||
webOrigin: process.env.WEB_ORIGIN ?? "",
|
webOrigin: process.env.WEB_ORIGIN ?? "",
|
||||||
|
webAllowedOrigins: process.env.WEB_ALLOWED_ORIGINS ?? "",
|
||||||
dataDir: "/app/data",
|
dataDir: "/app/data",
|
||||||
dbPath: "/app/data/db.json",
|
dbPath: "/app/data/db.json",
|
||||||
logsPath: "/app/data/logs.json",
|
logsPath: "/app/data/logs.json",
|
||||||
|
|||||||
@@ -45,7 +45,12 @@ const bootstrap = async () => {
|
|||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
const fallbackOrigin = `http://localhost:${config.webPort}`;
|
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(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
origin: origins,
|
origin: origins,
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export const createLoopJob = async (
|
|||||||
allowIp: string;
|
allowIp: string;
|
||||||
targetLoops: number;
|
targetLoops: number;
|
||||||
delayMs: number;
|
delayMs: number;
|
||||||
|
profileName?: string;
|
||||||
|
profileId?: string;
|
||||||
}
|
}
|
||||||
): Promise<LoopJob> => {
|
): Promise<LoopJob> => {
|
||||||
const now = nowIso();
|
const now = nowIso();
|
||||||
@@ -41,6 +43,8 @@ export const createLoopJob = async (
|
|||||||
targetLoops: input.targetLoops,
|
targetLoops: input.targetLoops,
|
||||||
doneLoops: 0,
|
doneLoops: 0,
|
||||||
delayMs: input.delayMs,
|
delayMs: input.delayMs,
|
||||||
|
profileName: input.profileName,
|
||||||
|
profileId: input.profileId,
|
||||||
deleteDataBetweenLoops: true,
|
deleteDataBetweenLoops: true,
|
||||||
enforcementMode: "aggressive-soft",
|
enforcementMode: "aggressive-soft",
|
||||||
status: "RUNNING",
|
status: "RUNNING",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ router.post("/start", async (req, res) => {
|
|||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return res.status(400).json({ error: parsed.error.flatten() });
|
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();
|
const db = await readDb();
|
||||||
if (targetLoops > db.settings.maxLoopLimit) {
|
if (targetLoops > db.settings.maxLoopLimit) {
|
||||||
return res.status(400).json({ error: "Target loops exceed max limit" });
|
return res.status(400).json({ error: "Target loops exceed max limit" });
|
||||||
@@ -64,6 +64,8 @@ router.post("/start", async (req, res) => {
|
|||||||
allowIp,
|
allowIp,
|
||||||
targetLoops,
|
targetLoops,
|
||||||
delayMs,
|
delayMs,
|
||||||
|
profileName,
|
||||||
|
profileId,
|
||||||
});
|
});
|
||||||
res.json(job);
|
res.json(job);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export interface LoopJob {
|
|||||||
targetLoops: number;
|
targetLoops: number;
|
||||||
doneLoops: number;
|
doneLoops: number;
|
||||||
delayMs: number;
|
delayMs: number;
|
||||||
|
profileName?: string;
|
||||||
|
profileId?: string;
|
||||||
deleteDataBetweenLoops: boolean;
|
deleteDataBetweenLoops: boolean;
|
||||||
enforcementMode: EnforcementMode;
|
enforcementMode: EnforcementMode;
|
||||||
status: LoopStatus;
|
status: LoopStatus;
|
||||||
@@ -62,6 +64,8 @@ export interface Profile {
|
|||||||
name: string;
|
name: string;
|
||||||
allowIp: string;
|
allowIp: string;
|
||||||
delayMs: number;
|
delayMs: number;
|
||||||
|
profileName?: string;
|
||||||
|
profileId?: string;
|
||||||
targetLoops: number;
|
targetLoops: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,14 @@ export const api = axios.create({
|
|||||||
baseURL: baseURL || undefined,
|
baseURL: baseURL || undefined,
|
||||||
withCredentials: true,
|
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;
|
||||||
|
});
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ export const LoopSetupCard = () => {
|
|||||||
if (!selectedHash) return false;
|
if (!selectedHash) return false;
|
||||||
const job = jobs.find((j) => j.torrentHash === selectedHash && j.status === "RUNNING");
|
const job = jobs.find((j) => j.torrentHash === selectedHash && j.status === "RUNNING");
|
||||||
if (!job) return false;
|
if (!job) return false;
|
||||||
|
if (job.profileId) {
|
||||||
|
return job.profileId === profile.id;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
job.allowIp === profile.allowIp &&
|
job.allowIp === profile.allowIp &&
|
||||||
job.delayMs === profile.delayMs &&
|
job.delayMs === profile.delayMs &&
|
||||||
@@ -144,6 +147,8 @@ export const LoopSetupCard = () => {
|
|||||||
allowIp: profile.allowIp,
|
allowIp: profile.allowIp,
|
||||||
targetLoops: profile.targetLoops,
|
targetLoops: profile.targetLoops,
|
||||||
delayMs: profile.delayMs,
|
delayMs: profile.delayMs,
|
||||||
|
profileName: profile.name,
|
||||||
|
profileId: profile.id,
|
||||||
});
|
});
|
||||||
setLoopForm({
|
setLoopForm({
|
||||||
allowIp: profile.allowIp,
|
allowIp: profile.allowIp,
|
||||||
|
|||||||
@@ -77,6 +77,15 @@ export const TorrentTable = () => {
|
|||||||
const getProfileName = (hash: string) => {
|
const getProfileName = (hash: string) => {
|
||||||
const job = jobs.find((j) => j.torrentHash === hash);
|
const job = jobs.find((j) => j.torrentHash === hash);
|
||||||
if (!job) return null;
|
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) =>
|
const profile = profiles.find((p) =>
|
||||||
p.allowIp === job.allowIp &&
|
p.allowIp === job.allowIp &&
|
||||||
p.delayMs === job.delayMs &&
|
p.delayMs === job.delayMs &&
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export interface LoopJob {
|
|||||||
targetLoops: number;
|
targetLoops: number;
|
||||||
doneLoops: number;
|
doneLoops: number;
|
||||||
delayMs: number;
|
delayMs: number;
|
||||||
|
profileName?: string;
|
||||||
|
profileId?: string;
|
||||||
status: string;
|
status: string;
|
||||||
bans: { bannedIps: string[] };
|
bans: { bannedIps: string[] };
|
||||||
nextRunAt?: string;
|
nextRunAt?: string;
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
set({ loading: true, error: null });
|
set({ loading: true, error: null });
|
||||||
try {
|
try {
|
||||||
const response = await api.post("/api/auth/login", { username, password });
|
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 });
|
set({ username: response.data.username, loading: false });
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -27,6 +30,7 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
},
|
},
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
await api.post("/api/auth/logout");
|
await api.post("/api/auth/logout");
|
||||||
|
localStorage.removeItem("qbuffer_token");
|
||||||
set({ username: null });
|
set({ username: null });
|
||||||
},
|
},
|
||||||
check: async () => {
|
check: async () => {
|
||||||
@@ -34,6 +38,7 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
const response = await api.get("/api/auth/me");
|
const response = await api.get("/api/auth/me");
|
||||||
set({ username: response.data.username ?? "session" });
|
set({ username: response.data.username ?? "session" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
localStorage.removeItem("qbuffer_token");
|
||||||
set({ username: null });
|
set({ username: null });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
export default defineConfig({
|
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()],
|
plugins: [react()],
|
||||||
build: {
|
build: {
|
||||||
outDir: path.resolve(__dirname, "../server/public"),
|
outDir: path.resolve(__dirname, "../server/public"),
|
||||||
@@ -10,6 +17,8 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: Number(process.env.WEB_PORT) || 5173,
|
port: Number(env.WEB_PORT) || 5173,
|
||||||
|
allowedHosts: allowedHosts.length ? allowedHosts : true,
|
||||||
},
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user