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

@@ -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() {
try {
const packageJson = require("../../package.json");
return packageJson?.version || "1.2.7";
return packageJson?.version || "1.2.9";
} catch (packageError) {
console.warn(
"Could not read version from package.json, using fallback:",
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");
latestRelease = {
tagName: "v1.2.7",
version: "1.2.7",
tagName: "v1.2.8",
version: "1.2.8",
publishedAt: "2025-10-02T17:12:53Z",
htmlUrl:
"https://github.com/PatchMon/PatchMon/releases/tag/v1.2.7",
"https://github.com/PatchMon/PatchMon/releases/tag/v1.2.8",
};
latestCommit = {
sha: "cc89df161b8ea5d48ff95b0eb405fe69042052cd",

View File

@@ -61,9 +61,13 @@ const repositoryRoutes = require("./routes/repositoryRoutes");
const versionRoutes = require("./routes/versionRoutes");
const tfaRoutes = require("./routes/tfaRoutes");
const searchRoutes = require("./routes/searchRoutes");
const autoEnrollmentRoutes = require("./routes/autoEnrollmentRoutes");
const gethomepageRoutes = require("./routes/gethomepageRoutes");
const automationRoutes = require("./routes/automationRoutes");
const { queueManager } = require("./services/automation");
const updateScheduler = require("./services/updateScheduler");
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
const prisma = createPrismaClient();
@@ -416,6 +420,12 @@ app.use(`/api/${apiVersion}/repositories`, repositoryRoutes);
app.use(`/api/${apiVersion}/version`, versionRoutes);
app.use(`/api/${apiVersion}/tfa`, tfaRoutes);
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);
// Error handling middleware
@@ -439,6 +449,10 @@ process.on("SIGINT", async () => {
if (process.env.ENABLE_LOGGING === "true") {
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 disconnectPrisma(prisma);
process.exit(0);
@@ -448,6 +462,10 @@ process.on("SIGTERM", async () => {
if (process.env.ENABLE_LOGGING === "true") {
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 disconnectPrisma(prisma);
process.exit(0);
@@ -723,13 +741,34 @@ async function startServer() {
// Schedule recurring jobs
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, () => {
if (process.env.ENABLE_LOGGING === "true") {
logger.info(`Server running on port ${PORT}`);
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) {
console.error("❌ Failed to start server:", error.message);
process.exit(1);

View File

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