Merge changes from main: Add GetHomepage integration and update to v1.2.9

- Added gethomepageRoutes.js for GetHomepage integration
- Updated all package.json files to version 1.2.9
- Updated agent script to version 1.2.9
- Updated version fallbacks in versionRoutes.js and updateScheduler.js
- Updated setup.sh with version 1.2.9
- Merged GetHomepage integration UI (Integrations.jsx)
- Updated docker-entrypoint.sh from main
- Updated VersionUpdateTab component
- Combined automation and gethomepage routes in server.js
- Maintains both BullMQ automation and GetHomepage functionality
This commit is contained in:
Muhammad Ibrahim
2025-10-11 20:35:47 +01:00
parent 0ad1a96871
commit 8c538bd99c
13 changed files with 1111 additions and 283 deletions

View File

@@ -1,12 +1,12 @@
#!/bin/bash #!/bin/bash
# PatchMon Agent Script v1.2.8 # PatchMon Agent Script v1.2.9
# This script sends package update information to the PatchMon server using API credentials # This script sends package update information to the PatchMon server using API credentials
# Configuration # Configuration
PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}" PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}"
API_VERSION="v1" API_VERSION="v1"
AGENT_VERSION="1.2.8" AGENT_VERSION="1.2.9"
CONFIG_FILE="/etc/patchmon/agent.conf" CONFIG_FILE="/etc/patchmon/agent.conf"
CREDENTIALS_FILE="/etc/patchmon/credentials" CREDENTIALS_FILE="/etc/patchmon/credentials"
LOG_FILE="/var/log/patchmon-agent.log" LOG_FILE="/var/log/patchmon-agent.log"

View File

@@ -1,6 +1,6 @@
{ {
"name": "patchmon-backend", "name": "patchmon-backend",
"version": "1.2.7", "version": "1.2.9",
"description": "Backend API for Linux Patch Monitoring System", "description": "Backend API for Linux Patch Monitoring System",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "src/server.js", "main": "src/server.js",

View File

@@ -0,0 +1,236 @@
const express = require("express");
const { createPrismaClient } = require("../config/database");
const bcrypt = require("bcryptjs");
const router = express.Router();
const prisma = createPrismaClient();
// Middleware to authenticate API key
const authenticateApiKey = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Basic ")) {
return res
.status(401)
.json({ error: "Missing or invalid authorization header" });
}
// Decode base64 credentials
const base64Credentials = authHeader.split(" ")[1];
const credentials = Buffer.from(base64Credentials, "base64").toString(
"ascii",
);
const [apiKey, apiSecret] = credentials.split(":");
if (!apiKey || !apiSecret) {
return res.status(401).json({ error: "Invalid credentials format" });
}
// Find the token in database
const token = await prisma.auto_enrollment_tokens.findUnique({
where: { token_key: apiKey },
include: {
users: {
select: {
id: true,
username: true,
role: true,
},
},
},
});
if (!token) {
console.log(`API key not found: ${apiKey}`);
return res.status(401).json({ error: "Invalid API key" });
}
// Check if token is active
if (!token.is_active) {
return res.status(401).json({ error: "API key is disabled" });
}
// Check if token has expired
if (token.expires_at && new Date(token.expires_at) < new Date()) {
return res.status(401).json({ error: "API key has expired" });
}
// Check if token is for gethomepage integration
if (token.metadata?.integration_type !== "gethomepage") {
return res.status(401).json({ error: "Invalid API key type" });
}
// Verify the secret
const isValidSecret = await bcrypt.compare(apiSecret, token.token_secret);
if (!isValidSecret) {
return res.status(401).json({ error: "Invalid API secret" });
}
// Check IP restrictions if any
if (token.allowed_ip_ranges && token.allowed_ip_ranges.length > 0) {
const clientIp = req.ip || req.connection.remoteAddress;
const forwardedFor = req.headers["x-forwarded-for"];
const realIp = req.headers["x-real-ip"];
// Get the actual client IP (considering proxies)
const actualClientIp = forwardedFor
? forwardedFor.split(",")[0].trim()
: realIp || clientIp;
const isAllowedIp = token.allowed_ip_ranges.some((range) => {
// Simple IP range check (can be enhanced for CIDR support)
return actualClientIp.startsWith(range) || actualClientIp === range;
});
if (!isAllowedIp) {
console.log(
`IP validation failed. Client IP: ${actualClientIp}, Allowed ranges: ${token.allowed_ip_ranges.join(", ")}`,
);
return res.status(403).json({ error: "IP address not allowed" });
}
}
// Update last used timestamp
await prisma.auto_enrollment_tokens.update({
where: { id: token.id },
data: { last_used_at: new Date() },
});
// Attach token info to request
req.apiToken = token;
next();
} catch (error) {
console.error("API key authentication error:", error);
res.status(500).json({ error: "Authentication failed" });
}
};
// Get homepage widget statistics
router.get("/stats", authenticateApiKey, async (_req, res) => {
try {
// Get total hosts count
const totalHosts = await prisma.hosts.count({
where: { status: "active" },
});
// Get total outdated packages count
const totalOutdatedPackages = await prisma.host_packages.count({
where: { needs_update: true },
});
// Get total repositories count
const totalRepos = await prisma.repositories.count({
where: { is_active: true },
});
// Get hosts that need updates (have outdated packages)
const hostsNeedingUpdates = await prisma.hosts.count({
where: {
status: "active",
host_packages: {
some: {
needs_update: true,
},
},
},
});
// Get security updates count
const securityUpdates = await prisma.host_packages.count({
where: {
needs_update: true,
is_security_update: true,
},
});
// Get hosts with security updates
const hostsWithSecurityUpdates = await prisma.hosts.count({
where: {
status: "active",
host_packages: {
some: {
needs_update: true,
is_security_update: true,
},
},
},
});
// Get up-to-date hosts count
const upToDateHosts = totalHosts - hostsNeedingUpdates;
// Get recent update activity (last 24 hours)
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const recentUpdates = await prisma.update_history.count({
where: {
timestamp: {
gte: oneDayAgo,
},
status: "success",
},
});
// Get OS distribution
const osDistribution = await prisma.hosts.groupBy({
by: ["os_type"],
where: { status: "active" },
_count: {
id: true,
},
orderBy: {
_count: {
id: "desc",
},
},
});
// Format OS distribution data
const osDistributionFormatted = osDistribution.map((os) => ({
name: os.os_type,
count: os._count.id,
}));
// Extract top 3 OS types for flat display in widgets
const top_os_1 = osDistributionFormatted[0] || { name: "None", count: 0 };
const top_os_2 = osDistributionFormatted[1] || { name: "None", count: 0 };
const top_os_3 = osDistributionFormatted[2] || { name: "None", count: 0 };
// Prepare response data
const stats = {
total_hosts: totalHosts,
total_outdated_packages: totalOutdatedPackages,
total_repos: totalRepos,
hosts_needing_updates: hostsNeedingUpdates,
up_to_date_hosts: upToDateHosts,
security_updates: securityUpdates,
hosts_with_security_updates: hostsWithSecurityUpdates,
recent_updates_24h: recentUpdates,
os_distribution: osDistributionFormatted,
// Flattened OS data for easy widget display
top_os_1_name: top_os_1.name,
top_os_1_count: top_os_1.count,
top_os_2_name: top_os_2.name,
top_os_2_count: top_os_2.count,
top_os_3_name: top_os_3.name,
top_os_3_count: top_os_3.count,
last_updated: new Date().toISOString(),
};
res.json(stats);
} catch (error) {
console.error("Error fetching homepage stats:", error);
res.status(500).json({ error: "Failed to fetch statistics" });
}
});
// Health check endpoint for the API
router.get("/health", authenticateApiKey, async (req, res) => {
res.json({
status: "ok",
timestamp: new Date().toISOString(),
api_key: req.apiToken.token_name,
});
});
module.exports = router;

View File

@@ -14,13 +14,13 @@ const router = express.Router();
function getCurrentVersion() { function getCurrentVersion() {
try { try {
const packageJson = require("../../package.json"); const packageJson = require("../../package.json");
return packageJson?.version || "1.2.7"; return packageJson?.version || "1.2.9";
} catch (packageError) { } catch (packageError) {
console.warn( console.warn(
"Could not read version from package.json, using fallback:", "Could not read version from package.json, using fallback:",
packageError.message, packageError.message,
); );
return "1.2.7"; return "1.2.9";
} }
} }
@@ -274,11 +274,11 @@ router.get(
) { ) {
console.log("GitHub API rate limited, providing fallback data"); console.log("GitHub API rate limited, providing fallback data");
latestRelease = { latestRelease = {
tagName: "v1.2.7", tagName: "v1.2.8",
version: "1.2.7", version: "1.2.8",
publishedAt: "2025-10-02T17:12:53Z", publishedAt: "2025-10-02T17:12:53Z",
htmlUrl: htmlUrl:
"https://github.com/PatchMon/PatchMon/releases/tag/v1.2.7", "https://github.com/PatchMon/PatchMon/releases/tag/v1.2.8",
}; };
latestCommit = { latestCommit = {
sha: "cc89df161b8ea5d48ff95b0eb405fe69042052cd", sha: "cc89df161b8ea5d48ff95b0eb405fe69042052cd",

View File

@@ -61,9 +61,13 @@ const repositoryRoutes = require("./routes/repositoryRoutes");
const versionRoutes = require("./routes/versionRoutes"); const versionRoutes = require("./routes/versionRoutes");
const tfaRoutes = require("./routes/tfaRoutes"); const tfaRoutes = require("./routes/tfaRoutes");
const searchRoutes = require("./routes/searchRoutes"); const searchRoutes = require("./routes/searchRoutes");
const autoEnrollmentRoutes = require("./routes/autoEnrollmentRoutes");
const gethomepageRoutes = require("./routes/gethomepageRoutes");
const automationRoutes = require("./routes/automationRoutes"); const automationRoutes = require("./routes/automationRoutes");
const { queueManager } = require("./services/automation"); const updateScheduler = require("./services/updateScheduler");
const { initSettings } = require("./services/settingsService"); const { initSettings } = require("./services/settingsService");
const { cleanup_expired_sessions } = require("./utils/session_manager");
const { queueManager } = require("./services/automation");
// Initialize Prisma client with optimized connection pooling for multiple instances // Initialize Prisma client with optimized connection pooling for multiple instances
const prisma = createPrismaClient(); const prisma = createPrismaClient();
@@ -416,6 +420,12 @@ app.use(`/api/${apiVersion}/repositories`, repositoryRoutes);
app.use(`/api/${apiVersion}/version`, versionRoutes); app.use(`/api/${apiVersion}/version`, versionRoutes);
app.use(`/api/${apiVersion}/tfa`, tfaRoutes); app.use(`/api/${apiVersion}/tfa`, tfaRoutes);
app.use(`/api/${apiVersion}/search`, searchRoutes); app.use(`/api/${apiVersion}/search`, searchRoutes);
app.use(
`/api/${apiVersion}/auto-enrollment`,
authLimiter,
autoEnrollmentRoutes,
);
app.use(`/api/${apiVersion}/gethomepage`, gethomepageRoutes);
app.use(`/api/${apiVersion}/automation`, automationRoutes); app.use(`/api/${apiVersion}/automation`, automationRoutes);
// Error handling middleware // Error handling middleware
@@ -439,6 +449,10 @@ process.on("SIGINT", async () => {
if (process.env.ENABLE_LOGGING === "true") { if (process.env.ENABLE_LOGGING === "true") {
logger.info("SIGINT received, shutting down gracefully"); logger.info("SIGINT received, shutting down gracefully");
} }
if (app.locals.session_cleanup_interval) {
clearInterval(app.locals.session_cleanup_interval);
}
updateScheduler.stop();
await queueManager.shutdown(); await queueManager.shutdown();
await disconnectPrisma(prisma); await disconnectPrisma(prisma);
process.exit(0); process.exit(0);
@@ -448,6 +462,10 @@ process.on("SIGTERM", async () => {
if (process.env.ENABLE_LOGGING === "true") { if (process.env.ENABLE_LOGGING === "true") {
logger.info("SIGTERM received, shutting down gracefully"); logger.info("SIGTERM received, shutting down gracefully");
} }
if (app.locals.session_cleanup_interval) {
clearInterval(app.locals.session_cleanup_interval);
}
updateScheduler.stop();
await queueManager.shutdown(); await queueManager.shutdown();
await disconnectPrisma(prisma); await disconnectPrisma(prisma);
process.exit(0); process.exit(0);
@@ -723,13 +741,34 @@ async function startServer() {
// Schedule recurring jobs // Schedule recurring jobs
await queueManager.scheduleAllJobs(); await queueManager.scheduleAllJobs();
// Initial session cleanup
await cleanup_expired_sessions();
// Schedule session cleanup every hour
const session_cleanup_interval = setInterval(
async () => {
try {
await cleanup_expired_sessions();
} catch (error) {
console.error("Session cleanup error:", error);
}
},
60 * 60 * 1000,
); // Every hour
app.listen(PORT, () => { app.listen(PORT, () => {
if (process.env.ENABLE_LOGGING === "true") { if (process.env.ENABLE_LOGGING === "true") {
logger.info(`Server running on port ${PORT}`); logger.info(`Server running on port ${PORT}`);
logger.info(`Environment: ${process.env.NODE_ENV}`); logger.info(`Environment: ${process.env.NODE_ENV}`);
logger.info("✅ BullMQ queue manager started"); logger.info("✅ Session cleanup scheduled (every hour)");
} }
// Start update scheduler
updateScheduler.start();
}); });
// Store interval for cleanup on shutdown
app.locals.session_cleanup_interval = session_cleanup_interval;
} catch (error) { } catch (error) {
console.error("❌ Failed to start server:", error.message); console.error("❌ Failed to start server:", error.message);
process.exit(1); process.exit(1);

View File

@@ -104,7 +104,7 @@ class UpdateScheduler {
} }
// Read version from package.json dynamically // Read version from package.json dynamically
let currentVersion = "1.2.7"; // fallback let currentVersion = "1.2.9"; // fallback
try { try {
const packageJson = require("../../package.json"); const packageJson = require("../../package.json");
if (packageJson?.version) { if (packageJson?.version) {
@@ -214,7 +214,7 @@ class UpdateScheduler {
const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`; const httpsRepoUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`;
// Get current version for User-Agent // Get current version for User-Agent
let currentVersion = "1.2.7"; // fallback let currentVersion = "1.2.9"; // fallback
try { try {
const packageJson = require("../../package.json"); const packageJson = require("../../package.json");
if (packageJson?.version) { if (packageJson?.version) {

View File

@@ -8,19 +8,94 @@ log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2 echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2
} }
# Copy files from agents_backup to agents if agents directory is empty and no .sh files are present # Function to extract version from agent script
if [ -d "/app/agents" ] && [ -z "$(find /app/agents -maxdepth 1 -type f -name '*.sh' | head -n 1)" ]; then get_agent_version() {
if [ -d "/app/agents_backup" ]; then local file="$1"
log "Agents directory is empty, copying from backup..." if [ -f "$file" ]; then
cp -r /app/agents_backup/* /app/agents/ grep -m 1 '^AGENT_VERSION=' "$file" | cut -d'"' -f2 2>/dev/null || echo "0.0.0"
else else
log "Warning: agents_backup directory not found" echo "0.0.0"
fi fi
else }
log "Agents directory already contains files, skipping copy"
fi
log "Starting PatchMon Backend (${NODE_ENV:-production})..." # Function to compare versions (returns 0 if $1 > $2)
version_greater() {
# Use sort -V for version comparison
test "$(printf '%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" && test "$1" != "$2"
}
# Check and update agent files if necessary
update_agents() {
local backup_agent="/app/agents_backup/patchmon-agent.sh"
local current_agent="/app/agents/patchmon-agent.sh"
# Check if agents directory exists
if [ ! -d "/app/agents" ]; then
log "ERROR: /app/agents directory not found"
return 1
fi
# Check if backup exists
if [ ! -d "/app/agents_backup" ]; then
log "WARNING: agents_backup directory not found, skipping agent update"
return 0
fi
# Get versions
local backup_version=$(get_agent_version "$backup_agent")
local current_version=$(get_agent_version "$current_agent")
log "Agent version check:"
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 (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: 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"
needs_update=0
fi
# Perform update if needed
if [ $needs_update -eq 1 ]; then
log "Updating agents to version ${backup_version}..."
# Create backup of existing agents if they exist
if [ -f "$current_agent" ]; 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)"
fi
# Copy new agents
cp -r /app/agents_backup/* /app/agents/
# Verify update
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_version}, got: ${new_version})"
fi
fi
}
# Main execution
log "PatchMon Backend Container Starting..."
log "Environment: ${NODE_ENV:-production}"
# Update agents (version-aware)
update_agents
log "Running database migrations..." log "Running database migrations..."
npx prisma migrate deploy npx prisma migrate deploy

View File

@@ -1,7 +1,7 @@
{ {
"name": "patchmon-frontend", "name": "patchmon-frontend",
"private": true, "private": true,
"version": "1.2.7", "version": "1.2.9",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -128,12 +128,14 @@ const VersionUpdateTab = () => {
<span className="text-lg font-mono text-secondary-900 dark:text-white"> <span className="text-lg font-mono text-secondary-900 dark:text-white">
{versionInfo.github.latestRelease.tagName} {versionInfo.github.latestRelease.tagName}
</span> </span>
<div className="text-xs text-secondary-500 dark:text-secondary-400"> {versionInfo.github.latestRelease.publishedAt && (
Published:{" "} <div className="text-xs text-secondary-500 dark:text-secondary-400">
{new Date( Published:{" "}
versionInfo.github.latestRelease.publishedAt, {new Date(
).toLocaleDateString()} versionInfo.github.latestRelease.publishedAt,
</div> ).toLocaleDateString()}
</div>
)}
</div> </div>
</div> </div>
)} )}

File diff suppressed because it is too large Load Diff

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "patchmon", "name": "patchmon",
"version": "1.2.7", "version": "1.2.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "patchmon", "name": "patchmon",
"version": "1.2.7", "version": "1.2.9",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"workspaces": [ "workspaces": [
"backend", "backend",
@@ -23,7 +23,7 @@
}, },
"backend": { "backend": {
"name": "patchmon-backend", "name": "patchmon-backend",
"version": "1.2.7", "version": "1.2.9",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@bull-board/api": "^6.13.0", "@bull-board/api": "^6.13.0",
@@ -56,7 +56,7 @@
}, },
"frontend": { "frontend": {
"name": "patchmon-frontend", "name": "patchmon-frontend",
"version": "1.2.7", "version": "1.2.9",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "patchmon", "name": "patchmon",
"version": "1.2.7", "version": "1.2.9",
"description": "Linux Patch Monitoring System", "description": "Linux Patch Monitoring System",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,

View File

@@ -34,7 +34,7 @@ BLUE='\033[0;34m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# Global variables # Global variables
SCRIPT_VERSION="self-hosting-install.sh v1.2.7-selfhost-2025-01-20-1" SCRIPT_VERSION="self-hosting-install.sh v1.2.9-selfhost-2025-10-11-1"
DEFAULT_GITHUB_REPO="https://github.com/PatchMon/PatchMon.git" DEFAULT_GITHUB_REPO="https://github.com/PatchMon/PatchMon.git"
FQDN="" FQDN=""
CUSTOM_FQDN="" CUSTOM_FQDN=""
@@ -834,7 +834,7 @@ EOF
cat > frontend/.env << EOF cat > frontend/.env << EOF
VITE_API_URL=$SERVER_PROTOCOL_SEL://$FQDN/api/v1 VITE_API_URL=$SERVER_PROTOCOL_SEL://$FQDN/api/v1
VITE_APP_NAME=PatchMon VITE_APP_NAME=PatchMon
VITE_APP_VERSION=1.2.7 VITE_APP_VERSION=1.2.9
EOF EOF
print_status "Environment files created" print_status "Environment files created"
@@ -1206,7 +1206,7 @@ create_agent_version() {
# Priority 2: Use fallback version if not found # Priority 2: Use fallback version if not found
if [ "$current_version" = "N/A" ] || [ -z "$current_version" ]; then if [ "$current_version" = "N/A" ] || [ -z "$current_version" ]; then
current_version="1.2.7" current_version="1.2.9"
print_warning "Could not determine version, using fallback: $current_version" print_warning "Could not determine version, using fallback: $current_version"
fi fi