mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-23 16:13:57 +00:00
Compare commits
4 Commits
renovate/v
...
c98203a997
Author | SHA1 | Date | |
---|---|---|---|
|
c98203a997 | ||
|
37c8f5fa76 | ||
|
50e546ee7e | ||
|
2174abf395 |
Binary file not shown.
@@ -1,23 +1,29 @@
|
||||
# Database Configuration
|
||||
DATABASE_URL="postgresql://patchmon_user:p@tchm0n_p@55@localhost:5432/patchmon_db"
|
||||
DATABASE_URL="postgresql://patchmon_user:your-password-here@localhost:5432/patchmon_db"
|
||||
PM_DB_CONN_MAX_ATTEMPTS=30
|
||||
PM_DB_CONN_WAIT_INTERVAL=2
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_USER=your-redis-username-here
|
||||
REDIS_PASSWORD=your-redis-password-here
|
||||
REDIS_DB=0
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-secure-random-secret-key-change-this-in-production
|
||||
JWT_EXPIRES_IN=1h
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
|
||||
# Server Configuration
|
||||
PORT=3001
|
||||
NODE_ENV=development
|
||||
NODE_ENV=production
|
||||
|
||||
# API Configuration
|
||||
API_VERSION=v1
|
||||
|
||||
# CORS Configuration
|
||||
CORS_ORIGIN=http://localhost:3000
|
||||
|
||||
# Session Configuration
|
||||
SESSION_INACTIVITY_TIMEOUT_MINUTES=30
|
||||
|
||||
# User Configuration
|
||||
DEFAULT_USER_ROLE=user
|
||||
|
||||
# Rate Limiting (times in milliseconds)
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX=5000
|
||||
@@ -26,20 +32,18 @@ AUTH_RATE_LIMIT_MAX=500
|
||||
AGENT_RATE_LIMIT_WINDOW_MS=60000
|
||||
AGENT_RATE_LIMIT_MAX=1000
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_USER=your-redis-username-here
|
||||
REDIS_PASSWORD=your-redis-password-here
|
||||
REDIS_DB=0
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
ENABLE_LOGGING=true
|
||||
|
||||
# User Registration
|
||||
DEFAULT_USER_ROLE=user
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-secure-random-secret-key-change-this-in-production
|
||||
JWT_EXPIRES_IN=1h
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
SESSION_INACTIVITY_TIMEOUT_MINUTES=30
|
||||
|
||||
# TFA Configuration
|
||||
# TFA Configuration (optional - used if TFA is enabled)
|
||||
TFA_REMEMBER_ME_EXPIRES_IN=30d
|
||||
TFA_MAX_REMEMBER_SESSIONS=5
|
||||
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3
|
||||
|
@@ -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) => {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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') {
|
||||
|
10
frontend/env.example
Normal file
10
frontend/env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# Frontend Environment Configuration
|
||||
# This file is used by Vite during build and runtime
|
||||
|
||||
# API URL - Update this to match your backend server
|
||||
VITE_API_URL=http://localhost:3001/api/v1
|
||||
|
||||
# Application Metadata
|
||||
VITE_APP_NAME=PatchMon
|
||||
VITE_APP_VERSION=1.3.0
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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 = {}) => {
|
||||
|
635
setup.sh
635
setup.sh
@@ -651,10 +651,11 @@ configure_redis() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Generate Redis username based on instance
|
||||
# Generate Redis username based on instance (global variable for use in create_env_files)
|
||||
REDIS_USER="patchmon_${DB_SAFE_NAME}"
|
||||
|
||||
# Generate separate user password (more secure than reusing admin password)
|
||||
# This will be stored in the .env file for the application to use
|
||||
REDIS_USER_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
|
||||
|
||||
print_info "Creating Redis user: $REDIS_USER for database $REDIS_DB"
|
||||
@@ -761,12 +762,9 @@ configure_redis() {
|
||||
redis-cli -h 127.0.0.1 -p 6379 --user "$REDIS_USER" --pass "$REDIS_USER_PASSWORD" --no-auth-warning -n "$REDIS_DB" SET "patchmon:initialized" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /dev/null
|
||||
print_status "Marked Redis database $REDIS_DB as in-use"
|
||||
|
||||
# Update .env with the USER PASSWORD, not admin password
|
||||
echo "REDIS_USER=$REDIS_USER" >> .env
|
||||
echo "REDIS_PASSWORD=$REDIS_USER_PASSWORD" >> .env
|
||||
echo "REDIS_DB=$REDIS_DB" >> .env
|
||||
|
||||
print_status "Redis user password: $REDIS_USER_PASSWORD"
|
||||
# Note: Redis credentials will be written to .env by create_env_files() function
|
||||
print_status "Redis user '$REDIS_USER' configured successfully"
|
||||
print_info "Redis credentials will be saved to backend/.env"
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -1063,12 +1061,17 @@ AGENT_RATE_LIMIT_MAX=1000
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_USER=$REDIS_USER
|
||||
REDIS_PASSWORD=$REDIS_PASSWORD
|
||||
REDIS_PASSWORD=$REDIS_USER_PASSWORD
|
||||
REDIS_DB=$REDIS_DB
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
ENABLE_LOGGING=true
|
||||
|
||||
# TFA Configuration
|
||||
TFA_REMEMBER_ME_EXPIRES_IN=30d
|
||||
TFA_MAX_REMEMBER_SESSIONS=5
|
||||
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3
|
||||
EOF
|
||||
|
||||
# Frontend .env
|
||||
@@ -1130,88 +1133,90 @@ EOF
|
||||
print_status "Systemd service created: $SERVICE_NAME (running as $INSTANCE_USER)"
|
||||
}
|
||||
|
||||
# Setup nginx configuration
|
||||
setup_nginx() {
|
||||
print_info "Setting up nginx configuration..."
|
||||
log_message "Setting up nginx configuration for $FQDN"
|
||||
# Unified nginx configuration generator
|
||||
generate_nginx_config() {
|
||||
local fqdn="$1"
|
||||
local app_dir="$2"
|
||||
local backend_port="$3"
|
||||
local ssl_enabled="$4" # "true" or "false"
|
||||
local config_file="/etc/nginx/sites-available/$fqdn"
|
||||
|
||||
if [ "$USE_LETSENCRYPT" = "true" ]; then
|
||||
# HTTP-only config first for Certbot challenge
|
||||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
||||
print_info "Generating nginx configuration for $fqdn (SSL: $ssl_enabled)"
|
||||
|
||||
if [ "$ssl_enabled" = "true" ]; then
|
||||
# SSL Configuration
|
||||
cat > "$config_file" << EOF
|
||||
# HTTP to HTTPS redirect
|
||||
server {
|
||||
listen 80;
|
||||
server_name $FQDN;
|
||||
server_name $fqdn;
|
||||
|
||||
# Let's Encrypt challenge location
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
}
|
||||
|
||||
# Redirect all other traffic to HTTPS
|
||||
location / {
|
||||
return 301 https://\$server_name\$request_uri;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
else
|
||||
# HTTP-only configuration for local hosting
|
||||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
||||
|
||||
# HTTPS server block
|
||||
server {
|
||||
listen 80;
|
||||
server_name $FQDN;
|
||||
listen 443 ssl http2;
|
||||
server_name $fqdn;
|
||||
|
||||
# SSL Configuration
|
||||
ssl_certificate /etc/letsencrypt/live/$fqdn/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/$fqdn/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
# Security headers (applied to all responses)
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Frontend
|
||||
location / {
|
||||
root $APP_DIR/frontend/dist;
|
||||
root $app_dir/frontend/dist;
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
}
|
||||
|
||||
# Bull Board proxy
|
||||
location /bullboard {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||||
proxy_pass http://127.0.0.1:$backend_port;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
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;
|
||||
proxy_cache_bypass \$http_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
|
||||
# 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;
|
||||
# Enable cookie passthrough
|
||||
proxy_pass_header Set-Cookie;
|
||||
proxy_cookie_path / /;
|
||||
|
||||
# Preserve original client IP
|
||||
proxy_set_header X-Original-Forwarded-For \$http_x_forwarded_for;
|
||||
|
||||
# Handle preflight requests
|
||||
if (\$request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# API routes
|
||||
# Bull Board proxy
|
||||
location /bullboard {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
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;
|
||||
|
||||
# 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;
|
||||
|
||||
# Handle preflight requests
|
||||
if (\$request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# API proxy
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||||
proxy_pass http://127.0.0.1:$backend_port;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
@@ -1222,16 +1227,122 @@ server {
|
||||
proxy_cache_bypass \$http_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
|
||||
# Preserve original client IP
|
||||
proxy_set_header X-Original-Forwarded-For \$http_x_forwarded_for;
|
||||
|
||||
if (\$request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# Health check
|
||||
# Static assets caching (exclude Bull Board assets)
|
||||
location ~* ^/(?!bullboard).*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT/health;
|
||||
proxy_pass http://127.0.0.1:$backend_port/health;
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
else
|
||||
# HTTP-only configuration
|
||||
cat > "$config_file" << EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name $fqdn;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options DENY always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Frontend
|
||||
location / {
|
||||
root $app_dir/frontend/dist;
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
|
||||
# Bull Board proxy
|
||||
location /bullboard {
|
||||
proxy_pass http://127.0.0.1:$backend_port;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
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;
|
||||
proxy_cache_bypass \$http_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
|
||||
# Enable cookie passthrough
|
||||
proxy_pass_header Set-Cookie;
|
||||
proxy_cookie_path / /;
|
||||
|
||||
# Preserve original client IP
|
||||
proxy_set_header X-Original-Forwarded-For \$http_x_forwarded_for;
|
||||
|
||||
if (\$request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# API proxy
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:$backend_port;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_cache_bypass \$http_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
|
||||
# Preserve original client IP
|
||||
proxy_set_header X-Original-Forwarded-For \$http_x_forwarded_for;
|
||||
|
||||
if (\$request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# Static assets caching (exclude Bull Board assets)
|
||||
location ~* ^/(?!bullboard).*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
proxy_pass http://127.0.0.1:$backend_port/health;
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
print_status "Nginx configuration generated for $fqdn"
|
||||
}
|
||||
|
||||
# Setup nginx configuration
|
||||
setup_nginx() {
|
||||
print_info "Setting up nginx configuration..."
|
||||
log_message "Setting up nginx configuration for $FQDN"
|
||||
|
||||
# Generate HTTP-only config first (needed for Let's Encrypt challenge if SSL enabled)
|
||||
generate_nginx_config "$FQDN" "$APP_DIR" "$BACKEND_PORT" "false"
|
||||
|
||||
# Enable site
|
||||
ln -sf "/etc/nginx/sites-available/$FQDN" "/etc/nginx/sites-enabled/"
|
||||
@@ -1254,96 +1365,10 @@ setup_letsencrypt() {
|
||||
|
||||
# Check if a valid certificate already exists
|
||||
if certbot certificates 2>/dev/null | grep -q "$FQDN" && certbot certificates 2>/dev/null | grep -A 10 "$FQDN" | grep -q "VALID"; then
|
||||
print_status "Valid SSL certificate already exists for $FQDN, skipping certificate generation"
|
||||
print_status "Valid SSL certificate already exists for $FQDN"
|
||||
|
||||
# Update Nginx config with existing HTTPS configuration
|
||||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name $FQDN;
|
||||
return 301 https://\$server_name\$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name $FQDN;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/$FQDN/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/$FQDN/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
# Security headers
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
|
||||
# Frontend
|
||||
location / {
|
||||
root $APP_DIR/frontend/dist;
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
}
|
||||
|
||||
# Bull Board proxy
|
||||
location /bullboard {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
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;
|
||||
|
||||
# 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;
|
||||
|
||||
# Handle preflight requests
|
||||
if (\$request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# API proxy
|
||||
# Bull Board proxy
|
||||
location /bullboard {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
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;
|
||||
|
||||
# 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;
|
||||
|
||||
# Handle preflight requests
|
||||
if (\$request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_cache_bypass \$http_upgrade;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
# Generate SSL config with existing certificate
|
||||
generate_nginx_config "$FQDN" "$APP_DIR" "$BACKEND_PORT" "true"
|
||||
|
||||
# Enable the site
|
||||
ln -sf "/etc/nginx/sites-available/$FQDN" "/etc/nginx/sites-enabled/"
|
||||
@@ -1362,7 +1387,7 @@ EOF
|
||||
|
||||
print_info "No valid certificate found, generating new SSL certificate..."
|
||||
|
||||
# Wait a moment for nginx to be ready
|
||||
# Wait for nginx to be ready
|
||||
sleep 5
|
||||
|
||||
# Obtain SSL certificate
|
||||
@@ -1370,100 +1395,17 @@ EOF
|
||||
certbot --nginx -d "$FQDN" --non-interactive --agree-tos --email "$EMAIL" --redirect
|
||||
log_message "SSL certificate obtained successfully"
|
||||
|
||||
# Update Nginx config with full HTTPS configuration
|
||||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
||||
server {
|
||||
listen 80;
|
||||
server_name $FQDN;
|
||||
return 301 https://\$server_name\$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name $FQDN;
|
||||
# Generate SSL nginx configuration
|
||||
generate_nginx_config "$FQDN" "$APP_DIR" "$BACKEND_PORT" "true"
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/$FQDN/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/$FQDN/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
# Frontend
|
||||
location / {
|
||||
root $APP_DIR/frontend/dist;
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
}
|
||||
|
||||
# Bull Board proxy
|
||||
location /bullboard {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
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;
|
||||
|
||||
# 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;
|
||||
|
||||
# Handle preflight requests
|
||||
if (\$request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# API routes
|
||||
# Bull Board proxy
|
||||
location /bullboard {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
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;
|
||||
|
||||
# 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;
|
||||
|
||||
# Handle preflight requests
|
||||
if (\$request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$remote_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_cache_bypass \$http_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_connect_timeout 75s;
|
||||
}
|
||||
|
||||
# Health check
|
||||
location /health {
|
||||
proxy_pass http://127.0.0.1:$BACKEND_PORT/health;
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
nginx -t
|
||||
nginx -s reload
|
||||
# Test and reload nginx
|
||||
if nginx -t; then
|
||||
systemctl reload nginx
|
||||
print_status "Nginx configuration updated successfully"
|
||||
else
|
||||
print_error "Nginx configuration test failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Setup auto-renewal
|
||||
echo "0 12 * * * /usr/bin/certbot renew --quiet" | crontab -
|
||||
@@ -2022,6 +1964,190 @@ select_installation_to_update() {
|
||||
done
|
||||
}
|
||||
|
||||
# Check and update Redis configuration for existing installation
|
||||
update_redis_configuration() {
|
||||
print_info "Checking Redis configuration..."
|
||||
|
||||
# Check if Redis configuration exists in .env
|
||||
if [ -f "$instance_dir/backend/.env" ]; then
|
||||
if grep -q "^REDIS_HOST=" "$instance_dir/backend/.env" && \
|
||||
grep -q "^REDIS_PASSWORD=" "$instance_dir/backend/.env"; then
|
||||
print_status "Redis configuration already exists in .env"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
print_warning "Redis configuration not found in .env - this is a legacy installation"
|
||||
print_info "Setting up Redis for this instance..."
|
||||
|
||||
# Detect package manager if not already set
|
||||
if [ -z "$PKG_INSTALL" ]; then
|
||||
if command -v apt >/dev/null 2>&1; then
|
||||
PKG_INSTALL="apt install -y"
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
PKG_INSTALL="apt-get install -y"
|
||||
else
|
||||
print_error "No supported package manager found"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure Redis is installed and running
|
||||
if ! systemctl is-active --quiet redis-server; then
|
||||
print_info "Installing Redis..."
|
||||
$PKG_INSTALL redis-server
|
||||
systemctl start redis-server
|
||||
systemctl enable redis-server
|
||||
fi
|
||||
|
||||
# Generate Redis variables for this instance
|
||||
# Extract DB_SAFE_NAME from existing database name
|
||||
DB_SAFE_NAME=$(echo "$DB_NAME" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||
REDIS_USER="patchmon_${DB_SAFE_NAME}"
|
||||
REDIS_USER_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
|
||||
|
||||
# Find available Redis database
|
||||
print_info "Finding available Redis database..."
|
||||
local redis_db=0
|
||||
local max_attempts=16
|
||||
|
||||
while [ $redis_db -lt $max_attempts ]; do
|
||||
local key_count
|
||||
key_count=$(redis-cli -h localhost -p 6379 -n "$redis_db" DBSIZE 2>&1 | grep -v "ERR" || echo "1")
|
||||
|
||||
if [ "$key_count" = "0" ] || [ "$key_count" = "(integer) 0" ]; then
|
||||
print_status "Found available Redis database: $redis_db"
|
||||
REDIS_DB=$redis_db
|
||||
break
|
||||
fi
|
||||
redis_db=$((redis_db + 1))
|
||||
done
|
||||
|
||||
if [ -z "$REDIS_DB" ]; then
|
||||
print_warning "No empty Redis database found, using database 0"
|
||||
REDIS_DB=0
|
||||
fi
|
||||
|
||||
# Generate admin password if not exists
|
||||
REDIS_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
|
||||
|
||||
# Configure Redis with ACL if needed
|
||||
print_info "Configuring Redis ACL..."
|
||||
|
||||
# Create ACL file if it doesn't exist
|
||||
if [ ! -f /etc/redis/users.acl ]; then
|
||||
touch /etc/redis/users.acl
|
||||
chown redis:redis /etc/redis/users.acl
|
||||
chmod 640 /etc/redis/users.acl
|
||||
fi
|
||||
|
||||
# Configure ACL file in redis.conf
|
||||
if ! grep -q "^aclfile" /etc/redis/redis.conf 2>/dev/null; then
|
||||
echo "aclfile /etc/redis/users.acl" >> /etc/redis/redis.conf
|
||||
fi
|
||||
|
||||
# Remove requirepass (incompatible with ACL)
|
||||
if grep -q "^requirepass" /etc/redis/redis.conf 2>/dev/null; then
|
||||
sed -i 's/^requirepass.*/# &/' /etc/redis/redis.conf
|
||||
fi
|
||||
|
||||
# Create admin user if it doesn't exist
|
||||
if ! grep -q "^user admin" /etc/redis/users.acl; then
|
||||
echo "user admin on sanitize-payload >$REDIS_PASSWORD ~* &* +@all" >> /etc/redis/users.acl
|
||||
systemctl restart redis-server
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
# Create instance-specific Redis user
|
||||
print_info "Creating Redis user: $REDIS_USER"
|
||||
|
||||
# Try to authenticate with admin (may already exist from another instance)
|
||||
local acl_result
|
||||
acl_result=$(redis-cli -h 127.0.0.1 -p 6379 --user admin --pass "$REDIS_PASSWORD" --no-auth-warning ACL SETUSER "$REDIS_USER" on ">${REDIS_USER_PASSWORD}" ~* +@all 2>&1)
|
||||
|
||||
if [ "$acl_result" = "OK" ] || echo "$acl_result" | grep -q "OK"; then
|
||||
print_status "Redis user created successfully"
|
||||
redis-cli -h 127.0.0.1 -p 6379 --user admin --pass "$REDIS_PASSWORD" --no-auth-warning ACL SAVE > /dev/null 2>&1
|
||||
else
|
||||
print_warning "Could not create Redis user with ACL, trying without authentication..."
|
||||
# Fallback for systems without ACL configured
|
||||
redis-cli -h 127.0.0.1 -p 6379 CONFIG SET requirepass "$REDIS_USER_PASSWORD" > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Backup existing .env
|
||||
cp "$instance_dir/backend/.env" "$instance_dir/backend/.env.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
print_info "Backed up existing .env file"
|
||||
|
||||
# Add Redis configuration to .env
|
||||
print_info "Adding Redis configuration to .env..."
|
||||
cat >> "$instance_dir/backend/.env" << EOF
|
||||
|
||||
# Redis Configuration (added during update)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_USER=$REDIS_USER
|
||||
REDIS_PASSWORD=$REDIS_USER_PASSWORD
|
||||
REDIS_DB=$REDIS_DB
|
||||
EOF
|
||||
|
||||
print_status "Redis configuration added to .env"
|
||||
print_info "Redis User: $REDIS_USER"
|
||||
print_info "Redis Database: $REDIS_DB"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Update nginx configuration for existing installation
|
||||
update_nginx_configuration() {
|
||||
print_info "Updating nginx configuration..."
|
||||
|
||||
# Detect SSL status
|
||||
local ssl_enabled="false"
|
||||
if [ -f "/etc/letsencrypt/live/$SELECTED_INSTANCE/fullchain.pem" ]; then
|
||||
ssl_enabled="true"
|
||||
print_info "SSL certificate detected, updating HTTPS configuration"
|
||||
else
|
||||
print_info "No SSL certificate found, updating HTTP configuration"
|
||||
fi
|
||||
|
||||
# Backup existing config
|
||||
local backup_file="/etc/nginx/sites-available/$SELECTED_INSTANCE.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
if [ -f "/etc/nginx/sites-available/$SELECTED_INSTANCE" ]; then
|
||||
cp "/etc/nginx/sites-available/$SELECTED_INSTANCE" "$backup_file"
|
||||
print_info "Backed up existing nginx config to: $backup_file"
|
||||
fi
|
||||
|
||||
# Extract backend port
|
||||
local backend_port=$(grep -o 'proxy_pass http://127.0.0.1:[0-9]*' "/etc/nginx/sites-available/$SELECTED_INSTANCE" 2>/dev/null | grep -oP ':\K[0-9]+' | head -1)
|
||||
if [ -z "$backend_port" ] && [ -f "$instance_dir/backend/.env" ]; then
|
||||
backend_port=$(grep '^PORT=' "$instance_dir/backend/.env" | cut -d'=' -f2 | tr -d ' ')
|
||||
fi
|
||||
|
||||
if [ -z "$backend_port" ]; then
|
||||
print_warning "Could not determine backend port, skipping nginx config update"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_info "Detected backend port: $backend_port"
|
||||
|
||||
# Generate new configuration using the unified function
|
||||
generate_nginx_config "$SELECTED_INSTANCE" "$instance_dir" "$backend_port" "$ssl_enabled"
|
||||
|
||||
# Test and reload nginx
|
||||
if nginx -t; then
|
||||
systemctl reload nginx
|
||||
print_status "Nginx configuration updated successfully"
|
||||
else
|
||||
print_error "Nginx configuration test failed"
|
||||
# Restore backup
|
||||
if [ -f "$backup_file" ]; then
|
||||
mv "$backup_file" "/etc/nginx/sites-available/$SELECTED_INSTANCE"
|
||||
print_info "Restored backup nginx configuration"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Update existing installation
|
||||
update_installation() {
|
||||
local instance_dir="/opt/$SELECTED_INSTANCE"
|
||||
@@ -2145,6 +2271,12 @@ update_installation() {
|
||||
npx prisma generate
|
||||
npx prisma migrate deploy
|
||||
|
||||
# Check and update Redis configuration if needed (for legacy installations)
|
||||
update_redis_configuration
|
||||
|
||||
# Update nginx configuration with latest improvements
|
||||
update_nginx_configuration
|
||||
|
||||
# Start the service
|
||||
print_info "Starting service: $service_name"
|
||||
systemctl start "$service_name"
|
||||
@@ -2212,6 +2344,27 @@ main() {
|
||||
fi
|
||||
|
||||
# Normal installation mode
|
||||
# Check if existing installations are present
|
||||
local existing_installs=($(detect_installations))
|
||||
if [ ${#existing_installs[@]} -gt 0 ]; then
|
||||
print_warning "⚠️ Found ${#existing_installs[@]} existing PatchMon installation(s):"
|
||||
for install in "${existing_installs[@]}"; do
|
||||
print_info " - $install"
|
||||
done
|
||||
echo ""
|
||||
print_warning "If you want to UPDATE an existing installation, run:"
|
||||
print_info " sudo bash $0 --update"
|
||||
echo ""
|
||||
print_warning "If you want to create a NEW installation alongside the existing one(s), continue below."
|
||||
echo ""
|
||||
read_yes_no "Do you want to continue with NEW installation?" CONTINUE_NEW "n"
|
||||
|
||||
if [ "$CONTINUE_NEW" != "y" ]; then
|
||||
print_info "Installation cancelled. Run with --update flag to update existing installations."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run interactive setup
|
||||
interactive_setup
|
||||
|
||||
|
Reference in New Issue
Block a user