From 50e546ee7e4270059ff73d6ce65a1fd09fddd7f6 Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Tue, 21 Oct 2025 21:29:15 +0100 Subject: [PATCH] Fixed Bullboard authentication via Docker Fixed Agent checking upon entrypoint modified entrypoint to handle both binary files as well as the shell script --- backend/src/server.js | 319 +++++++++++++++- backend/src/services/agentVersionService.js | 32 +- backend/src/services/agentWs.js | 32 +- docker/backend.Dockerfile | 4 +- docker/backend.docker-entrypoint.sh | 90 +++-- docker/nginx.conf.template | 17 +- .../settings/AgentManagementTab.jsx | 346 +++++++++++++----- frontend/src/pages/Automation.jsx | 28 +- 8 files changed, 730 insertions(+), 138 deletions(-) diff --git a/backend/src/server.js b/backend/src/server.js index bf10591..27414d7 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -341,20 +341,50 @@ const parseOrigins = (val) => .map((s) => s.trim()) .filter(Boolean); const allowedOrigins = parseOrigins( - process.env.CORS_ORIGINS || process.env.CORS_ORIGIN || "http://fabio:3000", + process.env.CORS_ORIGINS || + process.env.CORS_ORIGIN || + "http://localhost:3000", ); + +// Add Bull Board origin to allowed origins if not already present +const bullBoardOrigin = process.env.CORS_ORIGIN || "http://localhost:3000"; +if (!allowedOrigins.includes(bullBoardOrigin)) { + allowedOrigins.push(bullBoardOrigin); +} + app.use( cors({ origin: (origin, callback) => { // Allow non-browser/SSR tools with no origin if (!origin) return callback(null, true); if (allowedOrigins.includes(origin)) return callback(null, true); + + // Allow Bull Board requests from the same origin as CORS_ORIGIN + if (origin === bullBoardOrigin) return callback(null, true); + // Allow same-origin requests (e.g., Bull Board accessing its own API) // This allows http://hostname:3001 to make requests to http://hostname:3001 if (origin?.includes(":3001")) return callback(null, true); + + // Allow Bull Board requests from the frontend origin (same host, different port) + // This handles cases where frontend is on port 3000 and backend on 3001 + const frontendOrigin = origin?.replace(/:3001$/, ":3000"); + if (frontendOrigin && allowedOrigins.includes(frontendOrigin)) { + return callback(null, true); + } + return callback(new Error("Not allowed by CORS")); }, credentials: true, + // Additional CORS options for better cookie handling + optionsSuccessStatus: 200, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "Cookie", + "X-Requested-With", + ], }), ); app.use(limiter); @@ -446,7 +476,7 @@ app.use(`/api/${apiVersion}/agent`, agentVersionRoutes); // Bull Board - will be populated after queue manager initializes let bullBoardRouter = null; -const bullBoardSessions = new Map(); // Store authenticated sessions +const _bullBoardSessions = new Map(); // Store authenticated sessions // Mount Bull Board at /bullboard for cleaner URL app.use(`/bullboard`, (_req, res, next) => { @@ -456,16 +486,176 @@ app.use(`/bullboard`, (_req, res, next) => { res.setHeader("Cross-Origin-Embedder-Policy", "unsafe-none"); } + // Add headers to help with WebSocket connections + res.setHeader("X-Frame-Options", "SAMEORIGIN"); + res.setHeader( + "Content-Security-Policy", + "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' ws: wss:;", + ); + next(); }); -// Authentication middleware for Bull Board +// Simplified Bull Board authentication - just validate token once and set a simple auth cookie app.use(`/bullboard`, async (req, res, next) => { - // Skip authentication for static assets only + // Skip authentication for static assets if (req.path.includes("/static/") || req.path.includes("/favicon")) { return next(); } + // Check for existing Bull Board auth cookie + if (req.cookies["bull-board-auth"]) { + // Already authenticated, allow access + return next(); + } + + // No auth cookie - check for token in query + const token = req.query.token; + if (!token) { + return res.status(401).json({ + error: + "Authentication required. Please access Bull Board from the Automation page.", + }); + } + + // Validate token and set auth cookie + req.headers.authorization = `Bearer ${token}`; + return authenticateToken(req, res, (err) => { + if (err) { + return res.status(401).json({ error: "Invalid authentication token" }); + } + return requireAdmin(req, res, (adminErr) => { + if (adminErr) { + return res.status(403).json({ error: "Admin access required" }); + } + + // Set a simple auth cookie that will persist for the session + res.cookie("bull-board-auth", token, { + httpOnly: false, + secure: false, + maxAge: 3600000, // 1 hour + path: "/bullboard", + sameSite: "lax", + }); + + console.log("Bull Board - Authentication successful, cookie set"); + return next(); + }); + }); +}); + +// Remove all the old complex middleware below and replace with the new Bull Board router setup +app.use(`/bullboard`, (req, res, next) => { + if (bullBoardRouter) { + return bullBoardRouter(req, res, next); + } + return res.status(503).json({ error: "Bull Board not initialized yet" }); +}); + +/* +// OLD MIDDLEWARE - REMOVED FOR SIMPLIFICATION - DO NOT USE +if (false) { + const sessionId = req.cookies["bull-board-session"]; + console.log("Bull Board API call - Session ID:", sessionId ? "present" : "missing"); + console.log("Bull Board API call - Cookies:", req.cookies); + console.log("Bull Board API call - Bull Board token cookie:", req.cookies["bull-board-token"] ? "present" : "missing"); + console.log("Bull Board API call - Query token:", req.query.token ? "present" : "missing"); + console.log("Bull Board API call - Auth header:", req.headers.authorization ? "present" : "missing"); + console.log("Bull Board API call - Origin:", req.headers.origin || "missing"); + console.log("Bull Board API call - Referer:", req.headers.referer || "missing"); + + // Check if we have any authentication method available + const hasSession = !!sessionId; + const hasTokenCookie = !!req.cookies["bull-board-token"]; + const hasQueryToken = !!req.query.token; + const hasAuthHeader = !!req.headers.authorization; + const hasReferer = !!req.headers.referer; + + console.log("Bull Board API call - Auth methods available:", { + session: hasSession, + tokenCookie: hasTokenCookie, + queryToken: hasQueryToken, + authHeader: hasAuthHeader, + referer: hasReferer + }); + + // Check for valid session first + if (sessionId) { + const session = bullBoardSessions.get(sessionId); + console.log("Bull Board API call - Session found:", !!session); + if (session && Date.now() - session.timestamp < 3600000) { + // Valid session, extend it + session.timestamp = Date.now(); + console.log("Bull Board API call - Using existing session, proceeding"); + return next(); + } else if (session) { + // Expired session, remove it + console.log("Bull Board API call - Session expired, removing"); + bullBoardSessions.delete(sessionId); + } + } + + // No valid session, check for token as fallback + let token = req.query.token; + if (!token && req.headers.authorization) { + token = req.headers.authorization.replace("Bearer ", ""); + } + if (!token && req.cookies["bull-board-token"]) { + token = req.cookies["bull-board-token"]; + } + + // For API calls, also check if the token is in the referer URL + // This handles cases where the main page hasn't set the cookie yet + if (!token && req.headers.referer) { + try { + const refererUrl = new URL(req.headers.referer); + const refererToken = refererUrl.searchParams.get('token'); + if (refererToken) { + token = refererToken; + console.log("Bull Board API call - Token found in referer URL:", refererToken.substring(0, 20) + "..."); + } else { + console.log("Bull Board API call - No token found in referer URL"); + // If no token in referer and no session, return 401 with redirect info + if (!sessionId) { + console.log("Bull Board API call - No authentication available, returning 401"); + return res.status(401).json({ + error: "Authentication required", + message: "Please refresh the page to re-authenticate" + }); + } + } + } catch (error) { + console.log("Bull Board API call - Error parsing referer URL:", error.message); + } + } + + if (token) { + console.log("Bull Board API call - Token found, authenticating"); + // Add token to headers for authentication + req.headers.authorization = `Bearer ${token}`; + + // Authenticate the user + return authenticateToken(req, res, (err) => { + if (err) { + console.log("Bull Board API call - Token authentication failed"); + return res.status(401).json({ error: "Authentication failed" }); + } + return requireAdmin(req, res, (adminErr) => { + if (adminErr) { + console.log("Bull Board API call - Admin access required"); + return res.status(403).json({ error: "Admin access required" }); + } + console.log("Bull Board API call - Token authentication successful"); + return next(); + }); + }); + } + + // No valid session or token for API calls, deny access + console.log("Bull Board API call - No valid session or token, denying access"); + return res.status(401).json({ error: "Valid Bull Board session or token required" }); + } + // Check for bull-board-session cookie first const sessionId = req.cookies["bull-board-session"]; if (sessionId) { @@ -486,6 +676,9 @@ app.use(`/bullboard`, async (req, res, next) => { if (!token && req.headers.authorization) { token = req.headers.authorization.replace("Bearer ", ""); } + if (!token && req.cookies["bull-board-token"]) { + token = req.cookies["bull-board-token"]; + } // If no token, deny access if (!token) { @@ -514,13 +707,23 @@ app.use(`/bullboard`, async (req, res, next) => { userId: req.user.id, }); - // Set session cookie - res.cookie("bull-board-session", newSessionId, { + // Set session cookie with proper configuration for domain access + const isHttps = process.env.NODE_ENV === "production" || process.env.SERVER_PROTOCOL === "https"; + const cookieOptions = { httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "lax", + secure: isHttps, maxAge: 3600000, // 1 hour - }); + path: "/", // Set path to root so it's available for all Bull Board requests + }; + + // Configure sameSite based on protocol and environment + if (isHttps) { + cookieOptions.sameSite = "none"; // Required for HTTPS cross-origin + } else { + cookieOptions.sameSite = "lax"; // Better for HTTP same-origin + } + + res.cookie("bull-board-session", newSessionId, cookieOptions); // Clean up old sessions periodically if (bullBoardSessions.size > 100) { @@ -536,13 +739,111 @@ app.use(`/bullboard`, async (req, res, next) => { }); }); }); +*/ +// Second middleware block - COMMENTED OUT - using simplified version above instead +/* app.use(`/bullboard`, (req, res, next) => { if (bullBoardRouter) { + // If this is the main Bull Board page (not an API call), inject the token and create session + if (!req.path.includes("/api/") && !req.path.includes("/static/") && req.path === "/bullboard") { + const token = req.query.token; + console.log("Bull Board main page - Token:", token ? "present" : "missing"); + console.log("Bull Board main page - Query params:", req.query); + console.log("Bull Board main page - Origin:", req.headers.origin || "missing"); + console.log("Bull Board main page - Referer:", req.headers.referer || "missing"); + console.log("Bull Board main page - Cookies:", req.cookies); + + if (token) { + // Authenticate the user and create a session immediately on page load + req.headers.authorization = `Bearer ${token}`; + + return authenticateToken(req, res, (err) => { + if (err) { + console.log("Bull Board main page - Token authentication failed"); + return res.status(401).json({ error: "Authentication failed" }); + } + return requireAdmin(req, res, (adminErr) => { + if (adminErr) { + console.log("Bull Board main page - Admin access required"); + return res.status(403).json({ error: "Admin access required" }); + } + + console.log("Bull Board main page - Token authentication successful, creating session"); + + // Create a Bull Board session immediately + const newSessionId = require("node:crypto") + .randomBytes(32) + .toString("hex"); + bullBoardSessions.set(newSessionId, { + timestamp: Date.now(), + userId: req.user.id, + }); + + // Set session cookie with proper configuration for domain access + const sessionCookieOptions = { + httpOnly: true, + secure: false, // Always false for HTTP + maxAge: 3600000, // 1 hour + path: "/", // Set path to root so it's available for all Bull Board requests + sameSite: "lax", // Always lax for HTTP + }; + + res.cookie("bull-board-session", newSessionId, sessionCookieOptions); + console.log("Bull Board main page - Session created:", newSessionId); + console.log("Bull Board main page - Cookie options:", sessionCookieOptions); + + // Also set a token cookie for API calls as a fallback + const tokenCookieOptions = { + httpOnly: false, // Allow JavaScript to access it + secure: false, // Always false for HTTP + maxAge: 3600000, // 1 hour + path: "/", // Set path to root for broader compatibility + sameSite: "lax", // Always lax for HTTP + }; + + res.cookie("bull-board-token", token, tokenCookieOptions); + console.log("Bull Board main page - Token cookie also set for API fallback"); + + // Clean up old sessions periodically + if (bullBoardSessions.size > 100) { + const now = Date.now(); + for (const [sid, session] of bullBoardSessions.entries()) { + if (now - session.timestamp > 3600000) { + bullBoardSessions.delete(sid); + } + } + } + + // Now proceed to serve the Bull Board page + return bullBoardRouter(req, res, next); + }); + }); + } else { + console.log("Bull Board main page - No token provided, checking for existing session"); + // Check if we have an existing session + const sessionId = req.cookies["bull-board-session"]; + if (sessionId) { + const session = bullBoardSessions.get(sessionId); + if (session && Date.now() - session.timestamp < 3600000) { + console.log("Bull Board main page - Using existing session"); + // Extend session + session.timestamp = Date.now(); + return bullBoardRouter(req, res, next); + } else if (session) { + console.log("Bull Board main page - Session expired, removing"); + bullBoardSessions.delete(sessionId); + } + } + console.log("Bull Board main page - No valid session, denying access"); + return res.status(401).json({ error: "Access token required" }); + } + } return bullBoardRouter(req, res, next); } return res.status(503).json({ error: "Bull Board not initialized yet" }); }); +*/ // Error handler specifically for Bull Board routes app.use("/bullboard", (err, req, res, _next) => { diff --git a/backend/src/services/agentVersionService.js b/backend/src/services/agentVersionService.js index dc7da5f..8187cb9 100644 --- a/backend/src/services/agentVersionService.js +++ b/backend/src/services/agentVersionService.js @@ -1,9 +1,9 @@ const axios = require("axios"); const fs = require("node:fs").promises; const path = require("node:path"); -const { exec } = require("node:child_process"); +const { exec, spawn } = require("node:child_process"); const { promisify } = require("node:util"); -const execAsync = promisify(exec); +const _execAsync = promisify(exec); // Simple semver comparison function function compareVersions(version1, version2) { @@ -135,16 +135,34 @@ class AgentVersionService { // Execute the agent binary with help flag to get version info try { - const { stdout, stderr } = await execAsync(`${agentPath} --help`, { + const child = spawn(agentPath, ["--help"], { timeout: 10000, }); - if (stderr) { - console.log("⚠️ Agent help stderr:", stderr); + let stdout = ""; + let stderr = ""; + + child.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + const result = await new Promise((resolve, reject) => { + child.on("close", (code) => { + resolve({ stdout, stderr, code }); + }); + child.on("error", reject); + }); + + if (result.stderr) { + console.log("⚠️ Agent help stderr:", result.stderr); } // Parse version from help output (e.g., "PatchMon Agent v1.3.0") - const versionMatch = stdout.match( + const versionMatch = result.stdout.match( /PatchMon Agent v([0-9]+\.[0-9]+\.[0-9]+)/i, ); if (versionMatch) { @@ -153,7 +171,7 @@ class AgentVersionService { } else { console.log( "⚠️ Could not parse version from agent help output:", - stdout, + result.stdout, ); this.currentVersion = null; } diff --git a/backend/src/services/agentWs.js b/backend/src/services/agentWs.js index 3d34f35..9b28e81 100644 --- a/backend/src/services/agentWs.js +++ b/backend/src/services/agentWs.js @@ -26,7 +26,37 @@ function init(server, prismaClient) { server.on("upgrade", async (request, socket, head) => { try { const { pathname } = url.parse(request.url); - if (!pathname || !pathname.startsWith("/api/")) { + if (!pathname) { + socket.destroy(); + return; + } + + // Handle Bull Board WebSocket connections + if (pathname.startsWith("/bullboard")) { + // For Bull Board, we need to check if the user is authenticated + // Check for session cookie or authorization header + const sessionCookie = request.headers.cookie?.match( + /bull-board-session=([^;]+)/, + )?.[1]; + const authHeader = request.headers.authorization; + + if (!sessionCookie && !authHeader) { + socket.destroy(); + return; + } + + // Accept the WebSocket connection for Bull Board + wss.handleUpgrade(request, socket, head, (ws) => { + ws.on("message", (message) => { + // Echo back for Bull Board WebSocket + ws.send(message); + }); + }); + return; + } + + // Handle agent WebSocket connections + if (!pathname.startsWith("/api/")) { socket.destroy(); return; } diff --git a/docker/backend.Dockerfile b/docker/backend.Dockerfile index 3c095b8..d4992d2 100644 --- a/docker/backend.Dockerfile +++ b/docker/backend.Dockerfile @@ -8,7 +8,7 @@ ENV NODE_ENV=development \ PM_LOG_TO_CONSOLE=true \ PORT=3001 -RUN apk add --no-cache openssl tini curl +RUN apk add --no-cache openssl tini curl libc6-compat USER node @@ -64,7 +64,7 @@ ENV NODE_ENV=production \ JWT_REFRESH_EXPIRES_IN=7d \ SESSION_INACTIVITY_TIMEOUT_MINUTES=30 -RUN apk add --no-cache openssl tini curl +RUN apk add --no-cache openssl tini curl libc6-compat USER node diff --git a/docker/backend.docker-entrypoint.sh b/docker/backend.docker-entrypoint.sh index 0173824..d2bd696 100755 --- a/docker/backend.docker-entrypoint.sh +++ b/docker/backend.docker-entrypoint.sh @@ -8,7 +8,7 @@ log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2 } -# Function to extract version from agent script +# Function to extract version from agent script (legacy) get_agent_version() { local file="$1" if [ -f "$file" ]; then @@ -18,6 +18,32 @@ get_agent_version() { fi } +# Function to get version from binary using --help flag +get_binary_version() { + local binary="$1" + if [ -f "$binary" ]; then + # Make sure binary is executable + chmod +x "$binary" 2>/dev/null || true + + # Try to execute the binary and extract version from help output + # The Go binary shows version in the --help output as "PatchMon Agent v1.3.0" + local version=$("$binary" --help 2>&1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 | tr -d 'v') + if [ -n "$version" ]; then + echo "$version" + else + # Fallback: try --version flag + version=$("$binary" --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1) + if [ -n "$version" ]; then + echo "$version" + else + echo "0.0.0" + fi + fi + else + echo "0.0.0" + fi +} + # Function to compare versions (returns 0 if $1 > $2) version_greater() { # Use sort -V for version comparison @@ -28,6 +54,8 @@ version_greater() { update_agents() { local backup_agent="/app/agents_backup/patchmon-agent.sh" local current_agent="/app/agents/patchmon-agent.sh" + local backup_binary="/app/agents_backup/patchmon-agent-linux-amd64" + local current_binary="/app/agents/patchmon-agent-linux-amd64" # Check if agents directory exists if [ ! -d "/app/agents" ]; then @@ -41,54 +69,72 @@ update_agents() { return 0 fi - # Get versions - local backup_version=$(get_agent_version "$backup_agent") - local current_version=$(get_agent_version "$current_agent") + # Get versions from both script and binary + local backup_script_version=$(get_agent_version "$backup_agent") + local current_script_version=$(get_agent_version "$current_agent") + local backup_binary_version=$(get_binary_version "$backup_binary") + local current_binary_version=$(get_binary_version "$current_binary") log "Agent version check:" - log " Image version: ${backup_version}" - log " Volume version: ${current_version}" + log " Image script version: ${backup_script_version}" + log " Volume script version: ${current_script_version}" + log " Image binary version: ${backup_binary_version}" + log " Volume binary version: ${current_binary_version}" # Determine if update is needed local needs_update=0 - # Case 1: No agents in volume (first time setup) - if [ -z "$(find /app/agents -maxdepth 1 -type f -name '*.sh' 2>/dev/null | head -n 1)" ]; then + # Case 1: No agents in volume at all (first time setup) + if [ -z "$(find /app/agents -maxdepth 1 -type f 2>/dev/null | head -n 1)" ]; then log "Agents directory is empty - performing initial copy" needs_update=1 - # Case 2: Backup version is newer - elif version_greater "$backup_version" "$current_version"; then - log "Newer agent version available (${backup_version} > ${current_version})" + # Case 2: Binary exists but backup binary is newer + elif [ "$current_binary_version" != "0.0.0" ] && version_greater "$backup_binary_version" "$current_binary_version"; then + log "Newer agent binary available (${backup_binary_version} > ${current_binary_version})" + needs_update=1 + # Case 3: No binary in volume, but shell scripts exist (legacy setup) - copy binaries + elif [ "$current_binary_version" = "0.0.0" ] && [ "$backup_binary_version" != "0.0.0" ]; then + log "No binary found in volume but backup has binaries - performing update" needs_update=1 else - log "Agents are up to date" + log "Agents are up to date (binary: ${current_binary_version})" needs_update=0 fi # Perform update if needed if [ $needs_update -eq 1 ]; then - log "Updating agents to version ${backup_version}..." + log "Updating agents to version ${backup_binary_version}..." # Create backup of existing agents if they exist - if [ -f "$current_agent" ]; then + if [ -f "$current_agent" ] || [ -f "$current_binary" ]; then local backup_timestamp=$(date +%Y%m%d_%H%M%S) - local backup_name="/app/agents/patchmon-agent.sh.backup.${backup_timestamp}" - cp "$current_agent" "$backup_name" 2>/dev/null || true - log "Previous agent backed up to: $(basename $backup_name)" + mkdir -p "/app/agents/backups" + + # Backup shell script if it exists + if [ -f "$current_agent" ]; then + cp "$current_agent" "/app/agents/backups/patchmon-agent.sh.${backup_timestamp}" 2>/dev/null || true + log "Previous script backed up" + fi + + # Backup binary if it exists + if [ -f "$current_binary" ]; then + cp "$current_binary" "/app/agents/backups/patchmon-agent-linux-amd64.${backup_timestamp}" 2>/dev/null || true + log "Previous binary backed up" + fi fi - # Copy new agents + # Copy new agents (both scripts and binaries) cp -r /app/agents_backup/* /app/agents/ # Make agent binaries executable chmod +x /app/agents/patchmon-agent-linux-* 2>/dev/null || true # Verify update - local new_version=$(get_agent_version "$current_agent") - if [ "$new_version" = "$backup_version" ]; then - log "✅ Agents successfully updated to version ${new_version}" + local new_binary_version=$(get_binary_version "$current_binary") + if [ "$new_binary_version" = "$backup_binary_version" ]; then + log "✅ Agents successfully updated to version ${new_binary_version}" else - log "⚠️ Warning: Agent update may have failed (expected: ${backup_version}, got: ${new_version})" + log "⚠️ Warning: Agent update may have failed (expected: ${backup_binary_version}, got: ${new_binary_version})" fi fi } diff --git a/docker/nginx.conf.template b/docker/nginx.conf.template index 0f0960f..2c1712e 100644 --- a/docker/nginx.conf.template +++ b/docker/nginx.conf.template @@ -35,17 +35,20 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; + proxy_set_header Cookie $http_cookie; # Forward cookies to backend proxy_cache_bypass $http_upgrade; proxy_read_timeout 300s; proxy_connect_timeout 75s; + + # Enable cookie passthrough in both directions + proxy_pass_header Set-Cookie; + proxy_cookie_path / /; # Preserve original client IP through proxy chain proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for; - # CORS headers for Bull Board - add_header Access-Control-Allow-Origin * always; - add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; - add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always; + # CORS headers for Bull Board - let backend handle CORS properly + # Note: Backend handles CORS with proper origin validation and credentials # Handle preflight requests if ($request_method = 'OPTIONS') { @@ -78,10 +81,8 @@ server { # Preserve original client IP through proxy chain proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for; - # CORS headers for API calls - even though backend is doing it - add_header Access-Control-Allow-Origin * always; - add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; - add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always; + # CORS headers for API calls - let backend handle CORS properly + # Note: Backend handles CORS with proper origin validation and credentials # Handle preflight requests if ($request_method = 'OPTIONS') { diff --git a/frontend/src/components/settings/AgentManagementTab.jsx b/frontend/src/components/settings/AgentManagementTab.jsx index fccc09c..0d8f9b2 100644 --- a/frontend/src/components/settings/AgentManagementTab.jsx +++ b/frontend/src/components/settings/AgentManagementTab.jsx @@ -1,9 +1,33 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { AlertCircle, CheckCircle, Clock, RefreshCw } from "lucide-react"; +import { + AlertCircle, + CheckCircle, + Clock, + Download, + ExternalLink, + RefreshCw, + X, +} from "lucide-react"; +import { useEffect, useState } from "react"; import api from "../../utils/api"; const AgentManagementTab = () => { const _queryClient = useQueryClient(); + const [toast, setToast] = useState(null); + + // Auto-hide toast after 5 seconds + useEffect(() => { + if (toast) { + const timer = setTimeout(() => { + setToast(null); + }, 5000); + return () => clearTimeout(timer); + } + }, [toast]); + + const showToast = (message, type = "success") => { + setToast({ message, type }); + }; // Agent version queries const { @@ -57,9 +81,11 @@ const AgentManagementTab = () => { }, onSuccess: () => { refetchVersion(); + showToast("Successfully checked for updates", "success"); }, onError: (error) => { console.error("Check updates error:", error); + showToast(`Failed to check for updates: ${error.message}`, "error"); }, }); @@ -79,11 +105,11 @@ const AgentManagementTab = () => { // Show success message const message = data.data?.message || "Agent binaries downloaded successfully"; - alert(`✅ ${message}`); + showToast(message, "success"); }, onError: (error) => { console.error("Download update error:", error); - alert(`❌ Download failed: ${error.message}`); + showToast(`Download failed: ${error.message}`, "error"); }, }); @@ -173,111 +199,255 @@ const AgentManagementTab = () => { return (
- {/* Header */} -
-
-

- Agent Version Management -

-

- Monitor agent versions and download updates -

-
-
- -
-
- - {/* Download Updates Button */} -
-
-
-

- {versionInfo?.currentVersion - ? "Download Agent Updates" - : "Download Agent Binaries"} -

-

- {versionInfo?.currentVersion - ? "Download the latest agent binaries from GitHub" - : "No agent binaries found. Download from GitHub to get started."} + {toast.type === "success" ? ( + + ) : ( + + )} +

+
+

+ {toast.message}

+
+ )} + + {/* Header */} +
+

+ Agent Version Management +

+

+ Monitor and manage agent versions across your infrastructure +

+
+ + {/* Status Banner */} +
+
+
+
+ {StatusIcon && ( + + )} +
+
+

+ {versionStatus.message} +

+

+ {versionStatus.status === "up-to-date" && + "All agent binaries are current"} + {versionStatus.status === "update-available" && + "A newer version is available for download"} + {versionStatus.status === "no-agent" && + "Download agent binaries to get started"} + {versionStatus.status === "github-unavailable" && + "Cannot check for updates at this time"} + {![ + "up-to-date", + "update-available", + "no-agent", + "github-unavailable", + ].includes(versionStatus.status) && + "Version information unavailable"} +

+
+
+
- {/* Version Status Card */} -
-
-

- Agent Version Status -

-
- {StatusIcon && ( - + {/* Version Information Grid */} +
+ {/* Current Version Card */} +
+

+ Current Version +

+

+ {versionInfo?.currentVersion || ( + + Not detected + )} - - {versionStatus.message} - -

+

- {versionInfo && ( -
-
- - Current Version: - - - {versionInfo.currentVersion || "Unknown"} + {/* Latest Version Card */} +
+

+ Latest Available +

+

+ {versionInfo?.latestVersion || ( + + Unknown + )} +

+
+ + {/* Last Checked Card */} +
+

+ Last Checked +

+

+ {versionInfo?.lastChecked + ? new Date(versionInfo.lastChecked).toLocaleString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }) + : "Never"} +

+
+
+ + {/* Download Updates Section */} +
+
+
+

+ {!versionInfo?.currentVersion + ? "Get Started with Agent Binaries" + : versionStatus.status === "update-available" + ? "New Agent Version Available" + : "Agent Binaries"} +

+

+ {!versionInfo?.currentVersion + ? "No agent binaries detected. Download from GitHub to begin managing your agents." + : versionStatus.status === "update-available" + ? `A new agent version (${versionInfo.latestVersion}) is available. Download the latest binaries from GitHub.` + : "Download or redownload agent binaries from GitHub."} +

+
+ + + + View on GitHub +
-
- - Latest Version: - - - {versionInfo.latestVersion || "Unknown"} - -
-
- - Last Checked: - - - {versionInfo.lastChecked - ? new Date(versionInfo.lastChecked).toLocaleString() - : "Never"} - +
+
+
+ + {/* Supported Architectures */} + {versionInfo?.supportedArchitectures && + versionInfo.supportedArchitectures.length > 0 && ( +
+

+ Supported Architectures +

+
+ {versionInfo.supportedArchitectures.map((arch) => ( +
+ + {arch} + +
+ ))}
)} -
); }; diff --git a/frontend/src/pages/Automation.jsx b/frontend/src/pages/Automation.jsx index b03bd53..1e593af 100644 --- a/frontend/src/pages/Automation.jsx +++ b/frontend/src/pages/Automation.jsx @@ -228,7 +228,33 @@ const Automation = () => { // Use the proxied URL through the frontend (port 3000) // This avoids CORS issues as everything goes through the same origin const url = `/bullboard?token=${encodeURIComponent(token)}`; - window.open(url, "_blank", "width=1200,height=800"); + // Open in a new tab instead of a new window + const bullBoardWindow = window.open(url, "_blank"); + + // Add a message listener to handle authentication failures + if (bullBoardWindow) { + // Listen for authentication failures and refresh with token + const checkAuth = () => { + try { + // Check if the Bull Board window is still open + if (bullBoardWindow.closed) return; + + // Inject a script to handle authentication failures + bullBoardWindow.postMessage( + { + type: "BULL_BOARD_TOKEN", + token: token, + }, + window.location.origin, + ); + } catch (e) { + console.log("Could not communicate with Bull Board window:", e); + } + }; + + // Send token after a short delay to ensure Bull Board is loaded + setTimeout(checkAuth, 1000); + } }; const triggerManualJob = async (jobType, data = {}) => {