Compare commits

...

2 Commits

Author SHA1 Message Date
Muhammad Ibrahim
50e546ee7e Fixed Bullboard authentication via Docker
Fixed Agent checking upon entrypoint
modified entrypoint to handle both binary files as well as the shell script
2025-10-21 21:29:15 +01:00
Muhammad Ibrahim
2174abf395 Static Lib for Go Agent 2025-10-21 00:07:51 +01:00
9 changed files with 730 additions and 138 deletions

Binary file not shown.

View File

@@ -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) => {

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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') {

View File

@@ -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 (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-2xl font-bold text-secondary-900 dark:text-white">
Agent Version Management
</h2>
<p className="text-secondary-600 dark:text-secondary-300">
Monitor agent versions and download updates
</p>
</div>
<div className="flex space-x-3">
<button
type="button"
onClick={() => checkUpdatesMutation.mutate()}
disabled={checkUpdatesMutation.isPending}
className="flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
{/* Toast Notification */}
{toast && (
<div
className={`fixed top-4 right-4 z-50 max-w-md rounded-lg shadow-lg border-2 p-4 flex items-start space-x-3 animate-in slide-in-from-top-5 ${
toast.type === "success"
? "bg-green-50 dark:bg-green-900/90 border-green-500 dark:border-green-600"
: "bg-red-50 dark:bg-red-900/90 border-red-500 dark:border-red-600"
}`}
>
<div
className={`flex-shrink-0 rounded-full p-1 ${
toast.type === "success"
? "bg-green-100 dark:bg-green-800"
: "bg-red-100 dark:bg-red-800"
}`}
>
<RefreshCw
className={`h-4 w-4 mr-2 ${checkUpdatesMutation.isPending ? "animate-spin" : ""}`}
/>
Check Updates
</button>
</div>
</div>
{/* Download Updates Button */}
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow p-6 border border-secondary-200 dark:border-secondary-600">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
{versionInfo?.currentVersion
? "Download Agent Updates"
: "Download Agent Binaries"}
</h3>
<p className="text-secondary-600 dark:text-secondary-300">
{versionInfo?.currentVersion
? "Download the latest agent binaries from GitHub"
: "No agent binaries found. Download from GitHub to get started."}
{toast.type === "success" ? (
<CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" />
) : (
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
)}
</div>
<div className="flex-1">
<p
className={`text-sm font-medium ${
toast.type === "success"
? "text-green-800 dark:text-green-100"
: "text-red-800 dark:text-red-100"
}`}
>
{toast.message}
</p>
</div>
<button
type="button"
onClick={() => downloadUpdateMutation.mutate()}
disabled={downloadUpdateMutation.isPending}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
onClick={() => setToast(null)}
className={`flex-shrink-0 rounded-lg p-1 transition-colors ${
toast.type === "success"
? "hover:bg-green-100 dark:hover:bg-green-800 text-green-600 dark:text-green-400"
: "hover:bg-red-100 dark:hover:bg-red-800 text-red-600 dark:text-red-400"
}`}
>
<X className="h-4 w-4" />
</button>
</div>
)}
{/* Header */}
<div className="mb-6">
<h2 className="text-2xl font-bold text-secondary-900 dark:text-white mb-2">
Agent Version Management
</h2>
<p className="text-secondary-600 dark:text-secondary-400">
Monitor and manage agent versions across your infrastructure
</p>
</div>
{/* Status Banner */}
<div
className={`rounded-xl shadow-sm p-6 border-2 ${
versionStatus.status === "up-to-date"
? "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800"
: versionStatus.status === "update-available"
? "bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800"
: versionStatus.status === "no-agent"
? "bg-orange-50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800"
: "bg-white dark:bg-secondary-800 border-secondary-200 dark:border-secondary-600"
}`}
>
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4">
<div
className={`p-3 rounded-lg ${
versionStatus.status === "up-to-date"
? "bg-green-100 dark:bg-green-800"
: versionStatus.status === "update-available"
? "bg-yellow-100 dark:bg-yellow-800"
: versionStatus.status === "no-agent"
? "bg-orange-100 dark:bg-orange-800"
: "bg-secondary-100 dark:bg-secondary-700"
}`}
>
{StatusIcon && (
<StatusIcon className={`h-6 w-6 ${versionStatus.color}`} />
)}
</div>
<div>
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white mb-1">
{versionStatus.message}
</h3>
<p className="text-sm text-secondary-600 dark:text-secondary-400">
{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"}
</p>
</div>
</div>
<button
type="button"
onClick={() => checkUpdatesMutation.mutate()}
disabled={checkUpdatesMutation.isPending}
className="flex items-center px-4 py-2 bg-white dark:bg-secondary-700 text-secondary-700 dark:text-secondary-200 rounded-lg hover:bg-secondary-50 dark:hover:bg-secondary-600 border border-secondary-300 dark:border-secondary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-sm hover:shadow"
>
<RefreshCw
className={`h-4 w-4 mr-2 ${downloadUpdateMutation.isPending ? "animate-spin" : ""}`}
className={`h-4 w-4 mr-2 ${checkUpdatesMutation.isPending ? "animate-spin" : ""}`}
/>
{downloadUpdateMutation.isPending
? "Downloading..."
: versionInfo?.currentVersion
? "Download Updates"
: "Download Agent Binaries"}
{checkUpdatesMutation.isPending
? "Checking..."
: "Check for Updates"}
</button>
</div>
</div>
{/* Version Status Card */}
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow p-6 border border-secondary-200 dark:border-secondary-600">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
Agent Version Status
</h3>
<div className="flex items-center space-x-2">
{StatusIcon && (
<StatusIcon className={`h-5 w-5 ${versionStatus.color}`} />
{/* Version Information Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Current Version Card */}
<div className="bg-white dark:bg-secondary-800 rounded-xl shadow-sm p-6 border border-secondary-200 dark:border-secondary-600 hover:shadow-md transition-shadow duration-200">
<h4 className="text-sm font-medium text-secondary-500 dark:text-secondary-400 mb-2">
Current Version
</h4>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{versionInfo?.currentVersion || (
<span className="text-lg text-secondary-400 dark:text-secondary-500">
Not detected
</span>
)}
<span className={`text-sm font-medium ${versionStatus.color}`}>
{versionStatus.message}
</span>
</div>
</p>
</div>
{versionInfo && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="text-secondary-500 dark:text-secondary-400">
Current Version:
</span>
<span className="ml-2 font-medium text-secondary-900 dark:text-white">
{versionInfo.currentVersion || "Unknown"}
{/* Latest Version Card */}
<div className="bg-white dark:bg-secondary-800 rounded-xl shadow-sm p-6 border border-secondary-200 dark:border-secondary-600 hover:shadow-md transition-shadow duration-200">
<h4 className="text-sm font-medium text-secondary-500 dark:text-secondary-400 mb-2">
Latest Available
</h4>
<p className="text-2xl font-bold text-secondary-900 dark:text-white">
{versionInfo?.latestVersion || (
<span className="text-lg text-secondary-400 dark:text-secondary-500">
Unknown
</span>
)}
</p>
</div>
{/* Last Checked Card */}
<div className="bg-white dark:bg-secondary-800 rounded-xl shadow-sm p-6 border border-secondary-200 dark:border-secondary-600 hover:shadow-md transition-shadow duration-200">
<h4 className="text-sm font-medium text-secondary-500 dark:text-secondary-400 mb-2">
Last Checked
</h4>
<p className="text-lg font-semibold text-secondary-900 dark:text-white">
{versionInfo?.lastChecked
? new Date(versionInfo.lastChecked).toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})
: "Never"}
</p>
</div>
</div>
{/* Download Updates Section */}
<div className="bg-gradient-to-br from-primary-50 to-blue-50 dark:from-secondary-800 dark:to-secondary-800 rounded-xl shadow-sm p-8 border border-primary-200 dark:border-secondary-600">
<div className="flex items-center justify-between">
<div className="flex-1">
<h3 className="text-xl font-bold text-secondary-900 dark:text-white mb-3">
{!versionInfo?.currentVersion
? "Get Started with Agent Binaries"
: versionStatus.status === "update-available"
? "New Agent Version Available"
: "Agent Binaries"}
</h3>
<p className="text-secondary-700 dark:text-secondary-300 mb-4">
{!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."}
</p>
<div className="flex items-center space-x-4">
<button
type="button"
onClick={() => downloadUpdateMutation.mutate()}
disabled={downloadUpdateMutation.isPending}
className="flex items-center px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 shadow-md hover:shadow-lg font-medium"
>
{downloadUpdateMutation.isPending ? (
<>
<RefreshCw className="h-5 w-5 mr-2 animate-spin" />
Downloading...
</>
) : (
<>
<Download className="h-5 w-5 mr-2" />
{!versionInfo?.currentVersion
? "Download Binaries"
: versionStatus.status === "update-available"
? "Download New Agent Version"
: "Redownload Binaries"}
</>
)}
</button>
<a
href="https://github.com/PatchMon/PatchMon-agent/releases"
target="_blank"
rel="noopener noreferrer"
className="flex items-center px-4 py-3 text-secondary-700 dark:text-secondary-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors duration-200 font-medium"
>
<ExternalLink className="h-4 w-4 mr-2" />
View on GitHub
</a>
</div>
<div>
<span className="text-secondary-500 dark:text-secondary-400">
Latest Version:
</span>
<span className="ml-2 font-medium text-secondary-900 dark:text-white">
{versionInfo.latestVersion || "Unknown"}
</span>
</div>
<div>
<span className="text-secondary-500 dark:text-secondary-400">
Last Checked:
</span>
<span className="ml-2 font-medium text-secondary-900 dark:text-white">
{versionInfo.lastChecked
? new Date(versionInfo.lastChecked).toLocaleString()
: "Never"}
</span>
</div>
</div>
</div>
{/* Supported Architectures */}
{versionInfo?.supportedArchitectures &&
versionInfo.supportedArchitectures.length > 0 && (
<div className="bg-white dark:bg-secondary-800 rounded-xl shadow-sm p-6 border border-secondary-200 dark:border-secondary-600">
<h4 className="text-lg font-semibold text-secondary-900 dark:text-white mb-4">
Supported Architectures
</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{versionInfo.supportedArchitectures.map((arch) => (
<div
key={arch}
className="flex items-center justify-center px-4 py-3 bg-secondary-50 dark:bg-secondary-700 rounded-lg border border-secondary-200 dark:border-secondary-600"
>
<code className="text-sm font-mono text-secondary-700 dark:text-secondary-300">
{arch}
</code>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
};

View File

@@ -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 = {}) => {