mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-27 10:03:45 +00:00
Merge pull request #161 from PatchMon/feature/gethomepage
Feature/gethomepage + new version 1.2.9
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
#!/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
|
||||
|
||||
# Configuration
|
||||
PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}"
|
||||
API_VERSION="v1"
|
||||
AGENT_VERSION="1.2.8"
|
||||
AGENT_VERSION="1.2.9"
|
||||
CONFIG_FILE="/etc/patchmon/agent.conf"
|
||||
CREDENTIALS_FILE="/etc/patchmon/credentials"
|
||||
LOG_FILE="/var/log/patchmon-agent.log"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "patchmon-backend",
|
||||
"version": "1.2.8",
|
||||
"version": "1.2.9",
|
||||
"description": "Backend API for Linux Patch Monitoring System",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "src/server.js",
|
||||
|
||||
236
backend/src/routes/gethomepageRoutes.js
Normal file
236
backend/src/routes/gethomepageRoutes.js
Normal 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;
|
||||
@@ -14,13 +14,13 @@ const router = express.Router();
|
||||
function getCurrentVersion() {
|
||||
try {
|
||||
const packageJson = require("../../package.json");
|
||||
return packageJson?.version || "1.2.8";
|
||||
return packageJson?.version || "1.2.9";
|
||||
} catch (packageError) {
|
||||
console.warn(
|
||||
"Could not read version from package.json, using fallback:",
|
||||
packageError.message,
|
||||
);
|
||||
return "1.2.8";
|
||||
return "1.2.9";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,11 +292,11 @@ router.get(
|
||||
) {
|
||||
console.log("GitHub API rate limited, providing fallback data");
|
||||
latestRelease = {
|
||||
tagName: "1.2.8",
|
||||
tagName: "v1.2.8",
|
||||
version: "1.2.8",
|
||||
publishedAt: "2025-10-02T17:12:53Z",
|
||||
htmlUrl:
|
||||
"https://github.com/PatchMon/PatchMon/releases/tag/1.2.8",
|
||||
"https://github.com/PatchMon/PatchMon/releases/tag/v1.2.8",
|
||||
};
|
||||
latestCommit = {
|
||||
sha: "cc89df161b8ea5d48ff95b0eb405fe69042052cd",
|
||||
@@ -318,10 +318,9 @@ router.get(
|
||||
latestRelease = settings.latest_version
|
||||
? {
|
||||
version: settings.latest_version,
|
||||
tagName: settings.latest_version,
|
||||
tagName: `v${settings.latest_version}`,
|
||||
publishedAt: null, // Only use date from GitHub API, not cached data
|
||||
// Note: URL may need 'v' prefix depending on actual tag format in repo
|
||||
htmlUrl: `${githubRepoUrl.replace(/\.git$/, "")}/releases/tag/${settings.latest_version}`,
|
||||
htmlUrl: `${githubRepoUrl.replace(/\.git$/, "")}/releases/tag/v${settings.latest_version}`,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ 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 updateScheduler = require("./services/updateScheduler");
|
||||
const { initSettings } = require("./services/settingsService");
|
||||
const { cleanup_expired_sessions } = require("./utils/session_manager");
|
||||
@@ -422,6 +423,7 @@ app.use(
|
||||
authLimiter,
|
||||
autoEnrollmentRoutes,
|
||||
);
|
||||
app.use(`/api/${apiVersion}/gethomepage`, gethomepageRoutes);
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, _req, res, _next) => {
|
||||
|
||||
@@ -104,7 +104,7 @@ class UpdateScheduler {
|
||||
}
|
||||
|
||||
// Read version from package.json dynamically
|
||||
let currentVersion = "1.2.8"; // 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.8"; // fallback
|
||||
let currentVersion = "1.2.9"; // fallback
|
||||
try {
|
||||
const packageJson = require("../../package.json");
|
||||
if (packageJson?.version) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "patchmon-frontend",
|
||||
"private": true,
|
||||
"version": "1.2.8",
|
||||
"version": "1.2.9",
|
||||
"license": "AGPL-3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -79,13 +79,13 @@ const Queue = () => {
|
||||
hostname: "web-server-01",
|
||||
ip: "192.168.1.100",
|
||||
type: "Agent Update Collection",
|
||||
description: "Agent v1.2.7 → v1.2.8",
|
||||
description: "Agent v1.2.8 → v1.2.9",
|
||||
status: "pending",
|
||||
priority: "medium",
|
||||
lastCommunication: "2024-01-15 10:00:00",
|
||||
nextExpectedCommunication: "2024-01-15 11:00:00",
|
||||
currentVersion: "1.2.7",
|
||||
targetVersion: "1.2.8",
|
||||
currentVersion: "1.2.8",
|
||||
targetVersion: "1.2.9",
|
||||
retryCount: 0,
|
||||
maxRetries: 5,
|
||||
},
|
||||
@@ -99,8 +99,8 @@ const Queue = () => {
|
||||
priority: "high",
|
||||
lastCommunication: "2024-01-15 10:15:00",
|
||||
nextExpectedCommunication: "2024-01-15 11:15:00",
|
||||
currentVersion: "1.2.8",
|
||||
targetVersion: "1.2.8",
|
||||
currentVersion: "1.2.9",
|
||||
targetVersion: "1.2.9",
|
||||
retryCount: 0,
|
||||
maxRetries: 3,
|
||||
},
|
||||
@@ -109,13 +109,13 @@ const Queue = () => {
|
||||
hostname: "app-server-03",
|
||||
ip: "192.168.1.102",
|
||||
type: "Agent Update Collection",
|
||||
description: "Agent v1.2.6 → v1.2.8",
|
||||
description: "Agent v1.2.7 → v1.2.9",
|
||||
status: "completed",
|
||||
priority: "low",
|
||||
lastCommunication: "2024-01-15 09:30:00",
|
||||
completedAt: "2024-01-15 09:45:00",
|
||||
currentVersion: "1.2.8",
|
||||
targetVersion: "1.2.8",
|
||||
currentVersion: "1.2.9",
|
||||
targetVersion: "1.2.9",
|
||||
retryCount: 0,
|
||||
maxRetries: 5,
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "patchmon",
|
||||
"version": "1.2.7",
|
||||
"version": "1.2.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "patchmon",
|
||||
"version": "1.2.7",
|
||||
"version": "1.2.9",
|
||||
"license": "AGPL-3.0",
|
||||
"workspaces": [
|
||||
"backend",
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"backend": {
|
||||
"name": "patchmon-backend",
|
||||
"version": "1.2.7",
|
||||
"version": "1.2.9",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.1.0",
|
||||
@@ -52,7 +52,7 @@
|
||||
},
|
||||
"frontend": {
|
||||
"name": "patchmon-frontend",
|
||||
"version": "1.2.7",
|
||||
"version": "1.2.9",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "patchmon",
|
||||
"version": "1.2.8",
|
||||
"version": "1.2.9",
|
||||
"description": "Linux Patch Monitoring System",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
|
||||
6
setup.sh
6
setup.sh
@@ -34,7 +34,7 @@ BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Global variables
|
||||
SCRIPT_VERSION="self-hosting-install.sh v1.2.8-selfhost-2025-10-10-6"
|
||||
SCRIPT_VERSION="self-hosting-install.sh v1.2.9-selfhost-2025-10-10-6"
|
||||
DEFAULT_GITHUB_REPO="https://github.com/PatchMon/PatchMon.git"
|
||||
FQDN=""
|
||||
CUSTOM_FQDN=""
|
||||
@@ -867,7 +867,7 @@ EOF
|
||||
cat > frontend/.env << EOF
|
||||
VITE_API_URL=$SERVER_PROTOCOL_SEL://$FQDN/api/v1
|
||||
VITE_APP_NAME=PatchMon
|
||||
VITE_APP_VERSION=1.2.8
|
||||
VITE_APP_VERSION=1.2.9
|
||||
EOF
|
||||
|
||||
print_status "Environment files created"
|
||||
@@ -1239,7 +1239,7 @@ create_agent_version() {
|
||||
|
||||
# Priority 2: Use fallback version if not found
|
||||
if [ "$current_version" = "N/A" ] || [ -z "$current_version" ]; then
|
||||
current_version="1.2.8"
|
||||
current_version="1.2.9"
|
||||
print_warning "Could not determine version, using fallback: $current_version"
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user