Compare commits

..

10 Commits

Author SHA1 Message Date
renovate[bot]
e96ae2843d Update dependency @vitejs/plugin-react to v5 2025-10-21 11:56:24 +00:00
9 Technology Group LTD
00abbc8c62 Merge pull request #191 from PatchMon/feature/go-agent
Feature/go agent
2025-10-20 23:06:35 +01:00
9 Technology Group LTD
c9aef78912 Merge pull request #190 from PatchMon/feature/go-agent
Remove /bullboard from caching
2025-10-20 20:26:58 +01:00
9 Technology Group LTD
fd2df0729e Merge pull request #189 from PatchMon/feature/go-agent
added bullboard url for docker nginx template
2025-10-20 19:46:50 +01:00
9 Technology Group LTD
d7f7b24f8f Merge pull request #188 from PatchMon/feature/go-agent
Added axios in package.json
2025-10-20 19:21:07 +01:00
9 Technology Group LTD
1ef2308d56 Agent version detection and added nginx template 2025-10-20 18:55:43 +01:00
9 Technology Group LTD
fcd1b52e0e Merge pull request #186 from PatchMon/feature/go-agent
Bull Board
2025-10-19 20:58:03 +01:00
9 Technology Group LTD
5be8e01aa3 Merge pull request #185 from PatchMon/feature/go-agent
Modified the proxmox_auto-enroll.sh script to suit the new method
2025-10-19 19:03:17 +01:00
9 Technology Group LTD
293733dc0b Merge pull request #183 from PatchMon/feature/go-agent
Improved detection logic and upgrade mechanism using intermeditary sc…
2025-10-19 18:01:34 +01:00
9 Technology Group LTD
c7ab40e4a2 Merge pull request #182 from PatchMon/feature/go-agent
Fixed upgrade detection logic
2025-10-18 21:59:48 +01:00
14 changed files with 467 additions and 1212 deletions

Binary file not shown.

View File

@@ -1,29 +1,23 @@
# Database Configuration
DATABASE_URL="postgresql://patchmon_user:your-password-here@localhost:5432/patchmon_db"
DATABASE_URL="postgresql://patchmon_user:p@tchm0n_p@55@localhost:5432/patchmon_db"
PM_DB_CONN_MAX_ATTEMPTS=30
PM_DB_CONN_WAIT_INTERVAL=2
# JWT Configuration
JWT_SECRET=your-secure-random-secret-key-change-this-in-production
JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_USER=your-redis-username-here
REDIS_PASSWORD=your-redis-password-here
REDIS_DB=0
# Server Configuration
PORT=3001
NODE_ENV=production
NODE_ENV=development
# 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
@@ -32,18 +26,20 @@ 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
# TFA Configuration (optional - used if TFA is enabled)
# 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_REMEMBER_ME_EXPIRES_IN=30d
TFA_MAX_REMEMBER_SESSIONS=5
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3

View File

@@ -341,50 +341,20 @@ const parseOrigins = (val) =>
.map((s) => s.trim())
.filter(Boolean);
const allowedOrigins = parseOrigins(
process.env.CORS_ORIGINS ||
process.env.CORS_ORIGIN ||
"http://localhost:3000",
process.env.CORS_ORIGINS || process.env.CORS_ORIGIN || "http://fabio: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);
@@ -476,7 +446,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) => {
@@ -486,176 +456,16 @@ 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();
});
// Simplified Bull Board authentication - just validate token once and set a simple auth cookie
// Authentication middleware for Bull Board
app.use(`/bullboard`, async (req, res, next) => {
// Skip authentication for static assets
// Skip authentication for static assets only
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) {
@@ -676,9 +486,6 @@ if (false) {
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) {
@@ -707,23 +514,13 @@ if (false) {
userId: req.user.id,
});
// Set session cookie with proper configuration for domain access
const isHttps = process.env.NODE_ENV === "production" || process.env.SERVER_PROTOCOL === "https";
const cookieOptions = {
// Set session cookie
res.cookie("bull-board-session", newSessionId, {
httpOnly: true,
secure: isHttps,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
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) {
@@ -739,111 +536,13 @@ if (false) {
});
});
});
*/
// 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, spawn } = require("node:child_process");
const { exec } = 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,34 +135,16 @@ class AgentVersionService {
// Execute the agent binary with help flag to get version info
try {
const child = spawn(agentPath, ["--help"], {
const { stdout, stderr } = await execAsync(`${agentPath} --help`, {
timeout: 10000,
});
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);
if (stderr) {
console.log("⚠️ Agent help stderr:", stderr);
}
// Parse version from help output (e.g., "PatchMon Agent v1.3.0")
const versionMatch = result.stdout.match(
const versionMatch = stdout.match(
/PatchMon Agent v([0-9]+\.[0-9]+\.[0-9]+)/i,
);
if (versionMatch) {
@@ -171,7 +153,7 @@ class AgentVersionService {
} else {
console.log(
"⚠️ Could not parse version from agent help output:",
result.stdout,
stdout,
);
this.currentVersion = null;
}

View File

@@ -26,37 +26,7 @@ function init(server, prismaClient) {
server.on("upgrade", async (request, socket, head) => {
try {
const { pathname } = url.parse(request.url);
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/")) {
if (!pathname || !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 libc6-compat
RUN apk add --no-cache openssl tini curl
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 libc6-compat
RUN apk add --no-cache openssl tini curl
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 (legacy)
# Function to extract version from agent script
get_agent_version() {
local file="$1"
if [ -f "$file" ]; then
@@ -18,32 +18,6 @@ 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
@@ -54,8 +28,6 @@ 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
@@ -69,72 +41,54 @@ update_agents() {
return 0
fi
# 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")
# Get versions
local backup_version=$(get_agent_version "$backup_agent")
local current_version=$(get_agent_version "$current_agent")
log "Agent version check:"
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}"
log " Image version: ${backup_version}"
log " Volume version: ${current_version}"
# Determine if update is needed
local needs_update=0
# 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
# 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
log "Agents directory is empty - performing initial copy"
needs_update=1
# 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"
# Case 2: Backup version is newer
elif version_greater "$backup_version" "$current_version"; then
log "Newer agent version available (${backup_version} > ${current_version})"
needs_update=1
else
log "Agents are up to date (binary: ${current_binary_version})"
log "Agents are up to date"
needs_update=0
fi
# Perform update if needed
if [ $needs_update -eq 1 ]; then
log "Updating agents to version ${backup_binary_version}..."
log "Updating agents to version ${backup_version}..."
# Create backup of existing agents if they exist
if [ -f "$current_agent" ] || [ -f "$current_binary" ]; then
if [ -f "$current_agent" ]; then
local backup_timestamp=$(date +%Y%m%d_%H%M%S)
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
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)"
fi
# Copy new agents (both scripts and binaries)
# Copy new agents
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_binary_version=$(get_binary_version "$current_binary")
if [ "$new_binary_version" = "$backup_binary_version" ]; then
log "✅ Agents successfully updated to version ${new_binary_version}"
local new_version=$(get_agent_version "$current_agent")
if [ "$new_version" = "$backup_version" ]; then
log "✅ Agents successfully updated to version ${new_version}"
else
log "⚠️ Warning: Agent update may have failed (expected: ${backup_binary_version}, got: ${new_binary_version})"
log "⚠️ Warning: Agent update may have failed (expected: ${backup_version}, got: ${new_version})"
fi
fi
}

View File

@@ -35,20 +35,17 @@ 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 - let backend handle CORS properly
# Note: Backend handles CORS with proper origin validation and credentials
# 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') {
@@ -81,8 +78,10 @@ server {
# Preserve original client IP through proxy chain
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
# CORS headers for API calls - let backend handle CORS properly
# Note: Backend handles CORS with proper origin validation and credentials
# 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;
# Handle preflight requests
if ($request_method = 'OPTIONS') {

View File

@@ -1,10 +0,0 @@
# 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

View File

@@ -32,7 +32,7 @@
"devDependencies": {
"@types/react": "^18.3.14",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",

View File

@@ -1,33 +1,9 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
AlertCircle,
CheckCircle,
Clock,
Download,
ExternalLink,
RefreshCw,
X,
} from "lucide-react";
import { useEffect, useState } from "react";
import { AlertCircle, CheckCircle, Clock, RefreshCw } from "lucide-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 {
@@ -81,11 +57,9 @@ 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");
},
});
@@ -105,11 +79,11 @@ const AgentManagementTab = () => {
// Show success message
const message =
data.data?.message || "Agent binaries downloaded successfully";
showToast(message, "success");
alert(`${message}`);
},
onError: (error) => {
console.error("Download update error:", error);
showToast(`Download failed: ${error.message}`, "error");
alert(`Download failed: ${error.message}`);
},
});
@@ -199,255 +173,111 @@ const AgentManagementTab = () => {
return (
<div className="space-y-6">
{/* 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"
}`}
>
{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={() => 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>
<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-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"
className="flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
<RefreshCw
className={`h-4 w-4 mr-2 ${checkUpdatesMutation.isPending ? "animate-spin" : ""}`}
/>
{checkUpdatesMutation.isPending
? "Checking..."
: "Check for Updates"}
Check Updates
</button>
</div>
</div>
{/* 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>
)}
</p>
</div>
{/* 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>
{/* 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."}
</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"
>
<RefreshCw
className={`h-4 w-4 mr-2 ${downloadUpdateMutation.isPending ? "animate-spin" : ""}`}
/>
{downloadUpdateMutation.isPending
? "Downloading..."
: versionInfo?.currentVersion
? "Download Updates"
: "Download Agent Binaries"}
</button>
</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>
{/* 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}`} />
)}
<span className={`text-sm font-medium ${versionStatus.color}`}>
{versionStatus.message}
</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>
))}
{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"}
</span>
</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>
</div>
);
};

View File

@@ -228,33 +228,7 @@ 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)}`;
// 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);
}
window.open(url, "_blank", "width=1200,height=800");
};
const triggerManualJob = async (jobType, data = {}) => {

34
package-lock.json generated
View File

@@ -83,7 +83,7 @@
"devDependencies": {
"@types/react": "^18.3.14",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
@@ -142,6 +142,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -595,6 +596,7 @@
"resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-6.13.1.tgz",
"integrity": "sha512-DzPjCFzjEbDukhfSd7nLdTLVKIv5waARQuAXETSRqiKTN4vSA1KNdaJ8p72YwHujKO19yFW1zWjNKrzsa8DCIg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@bull-board/api": "6.13.1"
}
@@ -636,6 +638,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -1501,9 +1504,9 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
"version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
"integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==",
"dev": true,
"license": "MIT"
},
@@ -1938,6 +1941,7 @@
"integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -1960,21 +1964,21 @@
"license": "MIT"
},
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz",
"integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.28.0",
"@babel/core": "^7.28.4",
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
"@rolldown/pluginutils": "1.0.0-beta.27",
"@rolldown/pluginutils": "1.0.0-beta.38",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.17.0"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
@@ -2235,6 +2239,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -2454,6 +2459,7 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -3160,6 +3166,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -4978,6 +4985,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -5121,6 +5129,7 @@
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/config": "6.16.2",
"@prisma/engines": "6.16.2"
@@ -5342,6 +5351,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -5364,6 +5374,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -6211,6 +6222,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -6399,6 +6411,7 @@
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -6492,6 +6505,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},

729
setup.sh
View File

@@ -651,11 +651,10 @@ configure_redis() {
return 1
fi
# Generate Redis username based on instance (global variable for use in create_env_files)
# Generate Redis username based on instance
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"
@@ -762,9 +761,12 @@ 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"
# 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"
# 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"
return 0
}
@@ -1061,17 +1063,12 @@ AGENT_RATE_LIMIT_MAX=1000
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_USER=$REDIS_USER
REDIS_PASSWORD=$REDIS_USER_PASSWORD
REDIS_PASSWORD=$REDIS_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
@@ -1133,216 +1130,108 @@ EOF
print_status "Systemd service created: $SERVICE_NAME (running as $INSTANCE_USER)"
}
# 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"
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;
# 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;
}
}
# HTTPS server block
server {
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;
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
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"
if [ "$USE_LETSENCRYPT" = "true" ]; then
# HTTP-only config first for Certbot challenge
cat > "/etc/nginx/sites-available/$FQDN" << EOF
server {
listen 80;
server_name $FQDN;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://\$server_name\$request_uri;
}
}
EOF
else
# HTTP-only configuration for local hosting
cat > "/etc/nginx/sites-available/$FQDN" << EOF
server {
listen 80;
server_name $FQDN;
# 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 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
fi
# Enable site
ln -sf "/etc/nginx/sites-available/$FQDN" "/etc/nginx/sites-enabled/"
@@ -1365,10 +1254,96 @@ 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"
print_status "Valid SSL certificate already exists for $FQDN, skipping certificate generation"
# Generate SSL config with existing certificate
generate_nginx_config "$FQDN" "$APP_DIR" "$BACKEND_PORT" "true"
# 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
# Enable the site
ln -sf "/etc/nginx/sites-available/$FQDN" "/etc/nginx/sites-enabled/"
@@ -1387,7 +1362,7 @@ setup_letsencrypt() {
print_info "No valid certificate found, generating new SSL certificate..."
# Wait for nginx to be ready
# Wait a moment for nginx to be ready
sleep 5
# Obtain SSL certificate
@@ -1395,17 +1370,100 @@ setup_letsencrypt() {
certbot --nginx -d "$FQDN" --non-interactive --agree-tos --email "$EMAIL" --redirect
log_message "SSL certificate obtained successfully"
# Generate SSL nginx configuration
generate_nginx_config "$FQDN" "$APP_DIR" "$BACKEND_PORT" "true"
# 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;
# 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
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
# Setup auto-renewal
echo "0 12 * * * /usr/bin/certbot renew --quiet" | crontab -
@@ -1964,190 +2022,6 @@ 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"
@@ -2271,12 +2145,6 @@ 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"
@@ -2344,27 +2212,6 @@ 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