Compare commits

...

10 Commits

Author SHA1 Message Date
Muhammad Ibrahim
4b35fc9ab9 Fixed upgrade detection logic 2025-10-18 21:53:35 +01:00
9 Technology Group LTD
191a1afada Enhance Redis user creation and security
Updated Redis user creation process to enhance security by generating a separate user password. Adjusted Redis CLI commands to include host and port specifications.
2025-10-18 21:05:36 +01:00
9 Technology Group LTD
175f10b8b7 Improve Redis user creation error handling
Refactor Redis user creation to capture command output and verify success.
2025-10-18 21:01:57 +01:00
9 Technology Group LTD
080bcbe22e Merge pull request #181 from PatchMon/feature/go-agent
Upgrade from <1.2.8 to 1.3.0
2025-10-18 17:38:02 +01:00
Muhammad Ibrahim
3175ed79a5 Added arm32 based agent
Added support for migrating from legacy bash script to new binary via intermediatry 1.2.9 script
2025-10-18 17:28:46 +01:00
Muhammad Ibrahim
fba6d0ede5 Added REDIS_USER variable in the generation of .env 2025-10-18 16:34:10 +01:00
Muhammad Ibrahim
54a5012012 Created tools folder
Modified setup.sh to now cater for redis installation
2025-10-18 16:26:36 +01:00
Muhammad Ibrahim
5004e062b4 Setup Redis passwords to be used in Vm installation or via Docker
Setup so that CORS_ORIGIN error appears on the frontend to help new installations
2025-10-18 16:14:09 +01:00
9 Technology Group LTD
44d52a5536 Merge pull request #180 from PatchMon/feature/go-agent
fixing redis environment issue and some UI fixes
2025-10-18 02:06:34 +01:00
9 Technology Group LTD
c328123bd3 Merge pull request #179 from PatchMon/feature/go-agent
Major release 1.3.0 - New architecture
2025-10-17 22:43:09 +01:00
26 changed files with 1453 additions and 170 deletions

34
.dockerignore Normal file
View File

@@ -0,0 +1,34 @@
# Environment files
**/.env
**/.env.*
**/env.example
# Node modules
**/node_modules
# Logs
**/logs
**/*.log
# Git
**/.git
**/.gitignore
# IDE files
**/.vscode
**/.idea
**/*.swp
**/*.swo
# OS files
**/.DS_Store
**/Thumbs.db
# Build artifacts
**/dist
**/build
**/coverage
# Temporary files
**/tmp
**/temp

Binary file not shown.

Binary file not shown.

BIN
agents/patchmon-agent-linux-arm Executable file

Binary file not shown.

Binary file not shown.

555
agents/patchmon-agent.sh Executable file
View File

@@ -0,0 +1,555 @@
#!/bin/bash
# PatchMon Agent Migration Script v1.2.9
# This script migrates from legacy bash agent (v1.2.8) to Go agent (v1.3.0+)
# It acts as an intermediary during the upgrade process
# Configuration
PATCHMON_SERVER="${PATCHMON_SERVER:-http://localhost:3001}"
API_VERSION="v1"
AGENT_VERSION="1.2.9"
CREDENTIALS_FILE="/etc/patchmon/credentials"
LOG_FILE="/var/log/patchmon-agent.log"
# This placeholder will be dynamically replaced by the server when serving this
# script based on the "ignore SSL self-signed" setting. If set to -k, curl will
# ignore certificate validation. Otherwise, it will be empty for secure default.
CURL_FLAGS=""
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging function
log() {
if [[ -w "$(dirname "$LOG_FILE")" ]] 2>/dev/null; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] MIGRATION: $1" >> "$LOG_FILE" 2>/dev/null
fi
}
# Error handling
error() {
echo -e "${RED}ERROR: $1${NC}" >&2
log "ERROR: $1"
exit 1
}
# Info logging
info() {
echo -e "${BLUE} $1${NC}" >&2
log "INFO: $1"
}
# Success logging
success() {
echo -e "${GREEN}$1${NC}" >&2
log "SUCCESS: $1"
}
# Warning logging
warning() {
echo -e "${YELLOW}⚠️ $1${NC}" >&2
log "WARNING: $1"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
error "This migration script must be run as root"
fi
}
# Load API credentials from legacy format
load_legacy_credentials() {
if [[ ! -f "$CREDENTIALS_FILE" ]]; then
error "Legacy credentials file not found at $CREDENTIALS_FILE"
fi
source "$CREDENTIALS_FILE"
if [[ -z "$API_ID" ]] || [[ -z "$API_KEY" ]]; then
error "API_ID and API_KEY must be configured in $CREDENTIALS_FILE"
fi
# Use PATCHMON_URL from credentials if available
if [[ -n "$PATCHMON_URL" ]]; then
PATCHMON_SERVER="$PATCHMON_URL"
fi
}
# Convert legacy credentials to YAML format
convert_credentials_to_yaml() {
local yaml_file="/etc/patchmon/credentials.yml"
info "Converting credentials to YAML format..."
cat > "$yaml_file" << EOF
api_id: "$API_ID"
api_key: "$API_KEY"
EOF
chmod 600 "$yaml_file"
success "Credentials converted to YAML format"
}
# Create Go agent configuration
create_go_agent_config() {
local config_file="/etc/patchmon/config.yml"
info "Creating Go agent configuration..."
cat > "$config_file" << EOF
patchmon_server: "$PATCHMON_SERVER"
api_version: "$API_VERSION"
credentials_file: "/etc/patchmon/credentials.yml"
log_file: "/etc/patchmon/logs/patchmon-agent.log"
log_level: "info"
EOF
chmod 644 "$config_file"
success "Go agent configuration created"
}
# Download Go agent binary
download_go_agent() {
local arch=$(uname -m)
local goos="linux"
local goarch=""
# Map architecture
case "$arch" in
"x86_64")
goarch="amd64"
;;
"i386"|"i686")
goarch="386"
;;
"aarch64"|"arm64")
goarch="arm64"
;;
"armv7l"|"armv6l"|"armv5l")
goarch="arm"
;;
*)
error "Unsupported architecture: $arch"
;;
esac
local download_url="$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/go-binary?arch=$goarch"
local temp_dir="/etc/patchmon/tmp"
local temp_binary="$temp_dir/patchmon-agent-new"
# Create temp directory if it doesn't exist
mkdir -p "$temp_dir"
info "Downloading Go agent binary for $goos-$goarch..."
# Download with API credentials
if curl $CURL_FLAGS -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" \
-o "$temp_binary" "$download_url"; then
# Verify binary - check if it's a valid executable
chmod +x "$temp_binary"
# Check if file exists and is executable
if [[ -f "$temp_binary" ]] && [[ -x "$temp_binary" ]]; then
success "Go agent binary downloaded successfully"
echo "$temp_binary"
else
# Try to get more info about the file
local file_output=$(file "$temp_binary" 2>/dev/null || echo "unknown")
rm -f "$temp_binary" # Clean up failed download
error "Downloaded file is not executable. File info: $file_output"
fi
else
rm -f "$temp_binary" # Clean up failed download
error "Failed to download Go agent binary"
fi
}
# Install Go agent binary and service
install_go_agent() {
local temp_binary="$1"
local install_path="/usr/local/bin/patchmon-agent"
# Check if temp_binary is provided and exists
if [[ -z "$temp_binary" ]] || [[ ! -f "$temp_binary" ]]; then
error "No valid binary provided for installation"
fi
info "Installing Go agent binary..."
# Create backup of current script if it exists
if [[ -f "/usr/local/bin/patchmon-agent.sh" ]]; then
local backup_path="/usr/local/bin/patchmon-agent.sh.backup.$(date +%Y%m%d_%H%M%S)"
cp "/usr/local/bin/patchmon-agent.sh" "$backup_path"
info "Backed up legacy script to: $backup_path"
fi
# Install new binary
mv "$temp_binary" "$install_path"
success "Go agent binary installed to: $install_path"
# Clean up the temporary file (it's now moved, so this is just safety)
rm -f "$temp_binary"
# Install and start systemd service
install_systemd_service
}
# Install and start systemd service
install_systemd_service() {
info "Installing systemd service..."
# Create systemd service file
cat > /etc/systemd/system/patchmon-agent.service << EOF
[Unit]
Description=PatchMon Agent
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/patchmon-agent serve
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd and enable service
systemctl daemon-reload
systemctl enable patchmon-agent.service
# Start the service
info "Starting PatchMon agent service..."
if systemctl start patchmon-agent.service; then
success "PatchMon agent service started successfully"
# Wait a moment for service to start
sleep 3
# Check if service is running
if systemctl is-active --quiet patchmon-agent.service; then
success "PatchMon agent service is running"
else
warning "PatchMon agent service failed to start"
fi
else
warning "Failed to start PatchMon agent service"
fi
}
# Remove cron entries
remove_cron_entries() {
info "Removing legacy cron entries..."
# Get current crontab
local current_crontab=$(crontab -l 2>/dev/null || echo "")
if [[ -n "$current_crontab" ]]; then
# Remove any lines containing patchmon-agent
local new_crontab=$(echo "$current_crontab" | grep -v "patchmon-agent" || true)
# Update crontab if it changed
if [[ "$current_crontab" != "$new_crontab" ]]; then
if [[ -n "$new_crontab" ]]; then
echo "$new_crontab" | crontab -
success "Legacy cron entries removed (kept other cron jobs)"
else
crontab -r 2>/dev/null || true
success "All cron entries removed"
fi
else
info "No patchmon cron entries found to remove"
fi
else
info "No crontab found"
fi
}
# Configure Go agent
configure_go_agent() {
info "Configuring Go agent..."
# Create necessary directories
mkdir -p /etc/patchmon/logs
# Check if the Go agent binary exists
if [[ ! -f "/usr/local/bin/patchmon-agent" ]]; then
warning "Go agent binary not found at /usr/local/bin/patchmon-agent"
warning "Configuration files were created manually"
return 0
fi
# Configure credentials
if ! /usr/local/bin/patchmon-agent config set-credentials "$API_ID" "$API_KEY"; then
warning "Failed to configure credentials via CLI, but files were created manually"
fi
success "Go agent configured"
}
# Test Go agent
test_go_agent() {
info "Testing Go agent..."
# Check if the Go agent binary exists
if [[ ! -f "/usr/local/bin/patchmon-agent" ]]; then
warning "Go agent binary not found - skipping tests"
return 0
fi
# Wait a bit for service to fully start
sleep 2
# Test service status
if systemctl is-active --quiet patchmon-agent.service; then
success "PatchMon agent service is running"
else
warning "PatchMon agent service is not running"
fi
# Test configuration
if /usr/local/bin/patchmon-agent config show >/dev/null 2>&1; then
success "Go agent configuration test passed"
else
warning "Go agent configuration test failed, but continuing..."
fi
# Test connectivity (ping test)
info "Testing connectivity to PatchMon server..."
if /usr/local/bin/patchmon-agent ping >/dev/null 2>&1; then
success "Go agent connectivity test passed - server is reachable"
else
warning "Go agent connectivity test failed - server may be unreachable"
fi
}
# Clean up temporary directory
cleanup_temp_directory() {
info "Cleaning up temporary files..."
local temp_dir="/etc/patchmon/tmp"
if [[ -d "$temp_dir" ]]; then
rm -rf "$temp_dir"
success "Temporary directory cleaned up"
fi
}
# Clean up legacy files (only after successful verification)
cleanup_legacy_files() {
info "Cleaning up legacy files..."
# Remove legacy script
if [[ -f "/usr/local/bin/patchmon-agent.sh" ]]; then
rm -f "/usr/local/bin/patchmon-agent.sh"
success "Removed legacy script"
fi
# Remove legacy credentials file
if [[ -f "$CREDENTIALS_FILE" ]]; then
rm -f "$CREDENTIALS_FILE"
success "Removed legacy credentials file"
fi
# Remove legacy config file (if it exists)
if [[ -f "/etc/patchmon/config" ]]; then
rm -f "/etc/patchmon/config"
success "Removed legacy config file"
fi
}
# Show migration summary
show_migration_summary() {
echo ""
echo "=========================================="
echo "PatchMon Agent Migration Complete!"
echo "=========================================="
echo ""
echo "✅ Successfully migrated from bash agent to Go agent"
echo ""
echo "What was done:"
echo " • Converted credentials to YAML format"
echo " • Created Go agent configuration"
echo " • Downloaded and installed Go agent binary"
echo " • Installed and started systemd service"
echo " • Verified service is running and connected"
echo " • Removed legacy cron entries"
echo " • Cleaned up legacy files"
echo ""
echo "Next steps:"
echo " • The Go agent runs as a service, no cron needed"
echo " • Use: patchmon-agent serve (to run as service)"
echo " • Use: patchmon-agent report (for one-time report)"
echo " • Use: patchmon-agent --help (for all commands)"
echo ""
echo "Monitoring commands:"
echo " • Check status: systemctl status patchmon-agent"
echo " • View logs: tail -f /etc/patchmon/logs/patchmon-agent.log"
echo " • Run diagnostics: patchmon-agent diagnostics"
echo ""
echo "Configuration files:"
echo " • Config: /etc/patchmon/config.yml"
echo " • Credentials: /etc/patchmon/credentials.yml"
echo " • Logs: /etc/patchmon/logs/patchmon-agent.log"
echo ""
}
# Post-migration verification
post_migration_check() {
echo ""
echo "=========================================="
echo "Post-Migration Verification"
echo "=========================================="
echo ""
# Check if patchmon-agent is running
info "Checking if patchmon-agent is running..."
if pgrep -f "patchmon-agent serve" >/dev/null 2>&1; then
success "PatchMon agent is running"
else
warning "PatchMon agent is not running (this is normal if not started as service)"
info "To start as service: patchmon-agent serve"
fi
# Check WebSocket connection (if agent is running)
if pgrep -f "patchmon-agent serve" >/dev/null 2>&1; then
info "Checking WebSocket connection..."
if /usr/local/bin/patchmon-agent ping >/dev/null 2>&1; then
success "WebSocket connection is active"
else
warning "WebSocket connection test failed"
fi
else
info "Skipping WebSocket check (agent not running)"
fi
# Run diagnostics
info "Running system diagnostics..."
echo ""
if [[ -f "/usr/local/bin/patchmon-agent" ]]; then
if /usr/local/bin/patchmon-agent diagnostics >/dev/null 2>&1; then
success "Diagnostics completed successfully"
echo ""
echo "Full diagnostics output:"
echo "----------------------------------------"
/usr/local/bin/patchmon-agent diagnostics
echo "----------------------------------------"
else
warning "Diagnostics failed to run"
fi
else
warning "Go agent binary not found - cannot run diagnostics"
fi
echo ""
echo "=========================================="
echo "Migration Verification Complete!"
echo "=========================================="
echo ""
success "Thank you for using PatchMon Agent!"
echo ""
}
# Main migration function
perform_migration() {
info "Starting PatchMon Agent migration from bash to Go..."
echo ""
# Load legacy credentials
load_legacy_credentials
# Convert credentials
convert_credentials_to_yaml
# Create Go agent config
create_go_agent_config
# Download Go agent
local temp_binary=$(download_go_agent)
# Install Go agent binary and service
install_go_agent "$temp_binary"
# Configure Go agent
configure_go_agent
# Test Go agent (including service and connectivity)
test_go_agent
# Only proceed with cleanup if tests pass
if systemctl is-active --quiet patchmon-agent.service && /usr/local/bin/patchmon-agent ping >/dev/null 2>&1; then
success "Go agent is running and connected - proceeding with cleanup"
# Remove cron entries
remove_cron_entries
# Clean up legacy files
cleanup_legacy_files
# Clean up temporary directory
cleanup_temp_directory
# Show summary
show_migration_summary
# Run post-migration verification
post_migration_check
success "Migration completed successfully!"
else
warning "Go agent verification failed - skipping cleanup to preserve legacy files"
warning "You may need to manually clean up legacy files after fixing the Go agent"
# Show summary anyway
show_migration_summary
fi
# Exit here to prevent the legacy script from continuing
exit 0
}
# Handle command line arguments
case "$1" in
"migrate")
check_root
perform_migration
;;
"test")
check_root
load_legacy_credentials
test_go_agent
;;
"update-agent")
# This is called by legacy agents during update
check_root
perform_migration
;;
*)
# If no arguments provided, check if we're being executed by a legacy agent
# Legacy agents will call this script directly during update
if [[ -f "$CREDENTIALS_FILE" ]]; then
info "Detected legacy agent execution - starting migration..."
check_root
perform_migration
else
echo "PatchMon Agent Migration Script v$AGENT_VERSION"
echo "Usage: $0 {migrate|test|update-agent}"
echo ""
echo "Commands:"
echo " migrate - Perform full migration from bash to Go agent"
echo " test - Test Go agent after migration"
echo " update-agent - Called by legacy agents during update"
echo ""
echo "This script should be executed by the legacy agent during update."
exit 1
fi
;;
esac

View File

@@ -6,6 +6,7 @@ PM_DB_CONN_WAIT_INTERVAL=2
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_USER=your-redis-username-here
REDIS_PASSWORD=your-redis-password-here
REDIS_DB=0

View File

@@ -179,7 +179,7 @@ model settings {
updated_at DateTime
update_interval Int @default(60)
auto_update Boolean @default(false)
github_repo_url String @default("git@github.com:9technologygroup/patchmon.net.git")
github_repo_url String @default("https://github.com/PatchMon/PatchMon.git")
ssh_key_path String?
repository_type String @default("public")
last_update_check DateTime?

View File

@@ -14,7 +14,7 @@ const {
const router = express.Router();
const prisma = new PrismaClient();
// Secure endpoint to download the agent binary (requires API authentication)
// Secure endpoint to download the agent script/binary (requires API authentication)
router.get("/agent/download", async (req, res) => {
try {
// Verify API credentials
@@ -34,21 +34,124 @@ router.get("/agent/download", async (req, res) => {
return res.status(401).json({ error: "Invalid API credentials" });
}
const fs = require("node:fs");
const path = require("node:path");
// Check if this is a legacy agent (bash script) requesting update
// Legacy agents will have agent_version < 1.3.0
const isLegacyAgent =
host.agent_version &&
(host.agent_version.startsWith("1.2.") ||
host.agent_version.startsWith("1.1.") ||
host.agent_version.startsWith("1.0."));
if (isLegacyAgent) {
// Serve migration script for legacy agents
const migrationScriptPath = path.join(
__dirname,
"../../../agents/patchmon-agent.sh",
);
if (!fs.existsSync(migrationScriptPath)) {
return res.status(404).json({ error: "Migration script not found" });
}
// Set appropriate headers for script download
res.setHeader("Content-Type", "text/plain");
res.setHeader(
"Content-Disposition",
'attachment; filename="patchmon-agent.sh"',
);
// Stream the migration script
const fileStream = fs.createReadStream(migrationScriptPath);
fileStream.pipe(res);
fileStream.on("error", (error) => {
console.error("Migration script stream error:", error);
if (!res.headersSent) {
res.status(500).json({ error: "Failed to stream migration script" });
}
});
} else {
// Serve Go binary for new agents
const architecture = req.query.arch || "amd64";
// Validate architecture
const validArchitectures = ["amd64", "386", "arm64", "arm"];
if (!validArchitectures.includes(architecture)) {
return res.status(400).json({
error: "Invalid architecture. Must be one of: amd64, 386, arm64, arm",
});
}
const binaryName = `patchmon-agent-linux-${architecture}`;
const binaryPath = path.join(__dirname, "../../../agents", binaryName);
if (!fs.existsSync(binaryPath)) {
return res.status(404).json({
error: `Agent binary not found for architecture: ${architecture}`,
});
}
// Set appropriate headers for binary download
res.setHeader("Content-Type", "application/octet-stream");
res.setHeader(
"Content-Disposition",
`attachment; filename="${binaryName}"`,
);
// Stream the binary file
const fileStream = fs.createReadStream(binaryPath);
fileStream.pipe(res);
fileStream.on("error", (error) => {
console.error("Binary stream error:", error);
if (!res.headersSent) {
res.status(500).json({ error: "Failed to stream agent binary" });
}
});
}
} catch (error) {
console.error("Agent download error:", error);
res.status(500).json({ error: "Failed to serve agent" });
}
});
// Endpoint to download Go agent binary (for migration script)
router.get("/agent/go-binary", async (req, res) => {
try {
// Verify API credentials
const apiId = req.headers["x-api-id"];
const apiKey = req.headers["x-api-key"];
if (!apiId || !apiKey) {
return res.status(401).json({ error: "API credentials required" });
}
// Validate API credentials
const host = await prisma.hosts.findUnique({
where: { api_id: apiId },
});
if (!host || host.api_key !== apiKey) {
return res.status(401).json({ error: "Invalid API credentials" });
}
const fs = require("node:fs");
const path = require("node:path");
// Get architecture parameter (default to amd64)
const architecture = req.query.arch || "amd64";
// Validate architecture
const validArchitectures = ["amd64", "386", "arm64"];
const validArchitectures = ["amd64", "386", "arm64", "arm"];
if (!validArchitectures.includes(architecture)) {
return res.status(400).json({
error: "Invalid architecture. Must be one of: amd64, 386, arm64",
error: "Invalid architecture. Must be one of: amd64, 386, arm64, arm",
});
}
// Serve agent binary directly from file system
const fs = require("node:fs");
const path = require("node:path");
const binaryName = `patchmon-agent-linux-${architecture}`;
const binaryPath = path.join(__dirname, "../../../agents", binaryName);
@@ -76,8 +179,8 @@ router.get("/agent/download", async (req, res) => {
}
});
} catch (error) {
console.error("Agent download error:", error);
res.status(500).json({ error: "Failed to serve agent binary" });
console.error("Go binary download error:", error);
res.status(500).json({ error: "Failed to serve Go agent binary" });
}
});
@@ -763,91 +866,6 @@ router.post(
},
);
// TODO: Admin endpoint to bulk update host groups - needs to be rewritten for many-to-many relationship
// router.put(
// "/bulk/group",
// authenticateToken,
// requireManageHosts,
// [
// body("hostIds").isArray().withMessage("Host IDs must be an array"),
// body("hostIds.*")
// .isLength({ min: 1 })
// .withMessage("Each host ID must be provided"),
// body("hostGroupId").optional(),
// ],
// async (req, res) => {
// try {
// const errors = validationResult(req);
// if (!errors.isEmpty()) {
// return res.status(400).json({ errors: errors.array() });
// }
// const { hostIds, hostGroupId } = req.body;
// // If hostGroupId is provided, verify the group exists
// if (hostGroupId) {
// const hostGroup = await prisma.host_groups.findUnique({
// where: { id: hostGroupId },
// });
// if (!hostGroup) {
// return res.status(400).json({ error: "Host group not found" });
// }
// }
// // Check if all hosts exist
// const existingHosts = await prisma.hosts.findMany({
// where: { id: { in: hostIds } },
// select: { id: true, friendly_name: true },
// });
// if (existingHosts.length !== hostIds.length) {
// const foundIds = existingHosts.map((h) => h.id);
// const missingIds = hostIds.filter((id) => !foundIds.includes(id));
// return res.status(400).json({
// error: "Some hosts not found",
// missingHostIds: missingIds,
// });
// }
// // Bulk update host groups
// const updateResult = await prisma.hosts.updateMany({
// where: { id: { in: hostIds } },
// data: {
// host_group_id: hostGroupId || null,
// updated_at: new Date(),
// },
// });
// // Get updated hosts with group information
// const updatedHosts = await prisma.hosts.findMany({
// where: { id: { in: hostIds } },
// select: {
// id: true,
// friendly_name: true,
// host_groups: {
// select: {
// id: true,
// name: true,
// color: true,
// },
// },
// },
// });
// res.json({
// message: `Successfully updated ${updateResult.count} host${updateResult.count !== 1 ? "s" : ""}`,
// updatedCount: updateResult.count,
// hosts: updatedHosts,
// });
// } catch (error) {
// console.error("Bulk host group update error:", error);
// res.status(500).json({ error: "Failed to update host groups" });
// }
// },
// );
// Admin endpoint to bulk update host groups (many-to-many)
router.put(
"/bulk/groups",
authenticateToken,

View File

@@ -6,7 +6,7 @@ const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
// Default GitHub repository URL
const DEFAULT_GITHUB_REPO = "https://github.com/patchMon/patchmon";
const DEFAULT_GITHUB_REPO = "https://github.com/PatchMon/PatchMon.git";
const router = express.Router();

View File

@@ -339,9 +339,7 @@ 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",
);
app.use(
cors({
@@ -564,6 +562,15 @@ app.use((err, _req, res, _next) => {
if (process.env.ENABLE_LOGGING === "true") {
logger.error(err.stack);
}
// Special handling for CORS errors - always include the message
if (err.message?.includes("Not allowed by CORS")) {
return res.status(500).json({
error: "Something went wrong!",
message: err.message, // Always include CORS error message
});
}
res.status(500).json({
error: "Something went wrong!",
message: process.env.NODE_ENV === "development" ? err.message : undefined,

View File

@@ -21,7 +21,7 @@ class GitHubUpdateCheck {
try {
// Get settings
const settings = await prisma.settings.findFirst();
const DEFAULT_GITHUB_REPO = "https://github.com/patchMon/patchmon";
const DEFAULT_GITHUB_REPO = "https://github.com/PatchMon/PatchMon.git";
const repoUrl = settings?.githubRepoUrl || DEFAULT_GITHUB_REPO;
let owner, repo;

View File

@@ -5,6 +5,7 @@ const redisConnection = {
host: process.env.REDIS_HOST || "localhost",
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_PASSWORD || undefined,
username: process.env.REDIS_USER || undefined,
db: parseInt(process.env.REDIS_DB, 10) || 0,
retryDelayOnFailover: 100,
maxRetriesPerRequest: null, // BullMQ requires this to be null

View File

@@ -1 +1,3 @@
**/env.example
**/.env
**/.env.*

View File

@@ -1,3 +1,19 @@
# Change 3 Passwords in this file:
# Generate passwords with 'openssl rand -hex 64'
#
# 1. The database password in the environment variable POSTGRES_PASSWORD
# 2. The redis password in the command redis-server --requirepass your-redis-password-here
# 3. The jwt secret in the environment variable JWT_SECRET
#
#
# Change 2 URL areas in this file:
# 1. Setup your CORS_ORIGIN to what url you will use for accessing PatchMon frontend url
# 2. Setup your SERVER_PROTOCOL, SERVER_HOST and SERVER_PORT to what you will use for linux agents to access PatchMon
#
# This is generally the same as your CORS_ORIGIN url , in some cases it might be different - SERVER_* variables are used in the scripts for Server connection.
# You can also change this in the front-end but in the case of docker-compose - it is overwritten by the variables set here.
name: patchmon
services:
@@ -7,7 +23,7 @@ services:
environment:
POSTGRES_DB: patchmon_db
POSTGRES_USER: patchmon_user
POSTGRES_PASSWORD: # CREATE A STRONG PASSWORD AND PUT IT HERE
POSTGRES_PASSWORD: # CREATE A STRONG DB PASSWORD AND PUT IT HERE
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
@@ -19,11 +35,11 @@ services:
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass your-redis-password-here
command: redis-server --requirepass your-redis-password-here # CHANGE THIS TO YOUR REDIS PASSWORD
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "your-redis-password-here", "ping"]
test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "your-redis-password-here", "ping"] # CHANGE THIS TO YOUR REDIS PASSWORD
interval: 3s
timeout: 5s
retries: 7
@@ -35,7 +51,7 @@ services:
environment:
LOG_LEVEL: info
DATABASE_URL: postgresql://patchmon_user:REPLACE_YOUR_POSTGRES_PASSWORD_HERE@database:5432/patchmon_db
JWT_SECRET: # CREATE A STRONG SECRET AND PUT IT HERE - Generate with 'openssl rand -hex 64'
JWT_SECRET: # CREATE A STRONG SECRET AND PUT IT HERE
SERVER_PROTOCOL: http
SERVER_HOST: localhost
SERVER_PORT: 3000

View File

@@ -41,7 +41,7 @@ server {
# Preserve original client IP through proxy chain
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
# CORS headers for API calls
# 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;
@@ -77,10 +77,10 @@ server {
proxy_request_buffering off;
proxy_max_temp_file_size 0;
# CORS headers for SSE
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
# CORS headers for SSE - commented out to let backend handle CORS
# add_header Access-Control-Allow-Origin * always;
# add_header Access-Control-Allow-Methods "GET, 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,35 +0,0 @@
# Redis Configuration for PatchMon Production
# Security settings
# requirepass ${REDIS_PASSWORD} # Disabled - using command-line password instead
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command DEBUG ""
rename-command CONFIG "CONFIG_DISABLED"
# Memory management
maxmemory 256mb
maxmemory-policy allkeys-lru
# Persistence settings
save 900 1
save 300 10
save 60 10000
# Logging
loglevel notice
logfile ""
# Network security
bind 127.0.0.1
protected-mode yes
# Performance tuning
tcp-keepalive 300
timeout 0
# Disable dangerous commands
rename-command SHUTDOWN "SHUTDOWN_DISABLED"
rename-command KEYS ""
rename-command MONITOR ""
rename-command SLAVEOF ""
rename-command REPLICAOF ""

View File

@@ -2,6 +2,7 @@ import { AlertCircle, CheckCircle, Shield, UserPlus } from "lucide-react";
import { useId, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { isCorsError } from "../utils/api";
const FirstTimeAdminSetup = () => {
const { login, setAuthState } = useAuth();
@@ -121,11 +122,39 @@ const FirstTimeAdminSetup = () => {
}, 2000);
}
} else {
setError(data.error || "Failed to create admin user");
// Handle HTTP error responses (like 500 CORS errors)
console.log("HTTP error response:", response.status, data);
// Check if this is a CORS error based on the response data
if (
data.message?.includes("Not allowed by CORS") ||
data.message?.includes("CORS") ||
data.error?.includes("CORS")
) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else {
setError(data.error || "Failed to create admin user");
}
}
} catch (error) {
console.error("Setup error:", error);
setError("Network error. Please try again.");
// Check for CORS/network errors first
if (isCorsError(error)) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else if (
error.name === "TypeError" &&
error.message?.includes("Failed to fetch")
) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else {
setError("Network error. Please try again.");
}
} finally {
setIsLoading(false);
}

View File

@@ -7,6 +7,7 @@ import {
} from "react";
import { flushSync } from "react-dom";
import { AUTH_PHASES, isAuthPhase } from "../constants/authPhases";
import { isCorsError } from "../utils/api";
const AuthContext = createContext();
@@ -120,9 +121,50 @@ export const AuthProvider = ({ children }) => {
return { success: true };
} else {
// Handle HTTP error responses (like 500 CORS errors)
console.log("HTTP error response:", response.status, data);
// Check if this is a CORS error based on the response data
if (
data.message?.includes("Not allowed by CORS") ||
data.message?.includes("CORS") ||
data.error?.includes("CORS")
) {
return {
success: false,
error:
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
};
}
return { success: false, error: data.error || "Login failed" };
}
} catch {
} catch (error) {
console.log("Login error:", error);
console.log("Error response:", error.response);
console.log("Error message:", error.message);
// Check for CORS/network errors first
if (isCorsError(error)) {
return {
success: false,
error:
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
};
}
// Check for other network errors
if (
error.name === "TypeError" &&
error.message?.includes("Failed to fetch")
) {
return {
success: false,
error:
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
};
}
return { success: false, error: "Network error occurred" };
}
};
@@ -167,9 +209,46 @@ export const AuthProvider = ({ children }) => {
localStorage.setItem("user", JSON.stringify(data.user));
return { success: true, user: data.user };
} else {
// Handle HTTP error responses (like 500 CORS errors)
console.log("HTTP error response:", response.status, data);
// Check if this is a CORS error based on the response data
if (
data.message?.includes("Not allowed by CORS") ||
data.message?.includes("CORS") ||
data.error?.includes("CORS")
) {
return {
success: false,
error:
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
};
}
return { success: false, error: data.error || "Update failed" };
}
} catch {
} catch (error) {
// Check for CORS/network errors first
if (isCorsError(error)) {
return {
success: false,
error:
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
};
}
// Check for other network errors
if (
error.name === "TypeError" &&
error.message?.includes("Failed to fetch")
) {
return {
success: false,
error:
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
};
}
return { success: false, error: "Network error occurred" };
}
};
@@ -190,12 +269,49 @@ export const AuthProvider = ({ children }) => {
if (response.ok) {
return { success: true };
} else {
// Handle HTTP error responses (like 500 CORS errors)
console.log("HTTP error response:", response.status, data);
// Check if this is a CORS error based on the response data
if (
data.message?.includes("Not allowed by CORS") ||
data.message?.includes("CORS") ||
data.error?.includes("CORS")
) {
return {
success: false,
error:
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
};
}
return {
success: false,
error: data.error || "Password change failed",
};
}
} catch {
} catch (error) {
// Check for CORS/network errors first
if (isCorsError(error)) {
return {
success: false,
error:
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
};
}
// Check for other network errors
if (
error.name === "TypeError" &&
error.message?.includes("Failed to fetch")
) {
return {
success: false,
error:
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
};
}
return { success: false, error: "Network error occurred" };
}
};

View File

@@ -13,7 +13,7 @@ import { useEffect, useId, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { authAPI } from "../utils/api";
import { authAPI, isCorsError } from "../utils/api";
const Login = () => {
const usernameId = useId();
@@ -82,7 +82,21 @@ const Login = () => {
setError(result.error || "Login failed");
}
} catch (err) {
setError(err.response?.data?.error || "Login failed");
// Check for CORS/network errors first
if (isCorsError(err)) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else if (
err.name === "TypeError" &&
err.message?.includes("Failed to fetch")
) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else {
setError(err.response?.data?.error || "Login failed");
}
} finally {
setIsLoading(false);
}
@@ -112,12 +126,25 @@ const Login = () => {
}
} catch (err) {
console.error("Signup error:", err);
const errorMessage =
err.response?.data?.error ||
(err.response?.data?.errors && err.response.data.errors.length > 0
? err.response.data.errors.map((e) => e.msg).join(", ")
: err.message || "Signup failed");
setError(errorMessage);
if (isCorsError(err)) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else if (
err.name === "TypeError" &&
err.message?.includes("Failed to fetch")
) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else {
const errorMessage =
err.response?.data?.error ||
(err.response?.data?.errors && err.response.data.errors.length > 0
? err.response.data.errors.map((e) => e.msg).join(", ")
: err.message || "Signup failed");
setError(errorMessage);
}
} finally {
setIsLoading(false);
}
@@ -146,9 +173,22 @@ const Login = () => {
}
} catch (err) {
console.error("TFA verification error:", err);
const errorMessage =
err.response?.data?.error || err.message || "TFA verification failed";
setError(errorMessage);
if (isCorsError(err)) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else if (
err.name === "TypeError" &&
err.message?.includes("Failed to fetch")
) {
setError(
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
);
} else {
const errorMessage =
err.response?.data?.error || err.message || "TFA verification failed";
setError(errorMessage);
}
// Clear the token input for security
setTfaData({ token: "" });
} finally {

View File

@@ -26,7 +26,7 @@ import { useEffect, useId, useState } from "react";
import { useAuth } from "../contexts/AuthContext";
import { useTheme } from "../contexts/ThemeContext";
import { tfaAPI } from "../utils/api";
import { isCorsError, tfaAPI } from "../utils/api";
const Profile = () => {
const usernameId = useId();
@@ -88,8 +88,15 @@ const Profile = () => {
text: result.error || "Failed to update profile",
});
}
} catch {
setMessage({ type: "error", text: "Network error occurred" });
} catch (error) {
if (isCorsError(error)) {
setMessage({
type: "error",
text: "CORS_ORIGIN mismatch - please set your URL in your environment variable",
});
} else {
setMessage({ type: "error", text: "Network error occurred" });
}
} finally {
setIsLoading(false);
}
@@ -133,8 +140,15 @@ const Profile = () => {
text: result.error || "Failed to change password",
});
}
} catch {
setMessage({ type: "error", text: "Network error occurred" });
} catch (error) {
if (isCorsError(error)) {
setMessage({
type: "error",
text: "CORS_ORIGIN mismatch - please set your URL in your environment variable",
});
} else {
setMessage({ type: "error", text: "Network error occurred" });
}
} finally {
setIsLoading(false);
}

View File

@@ -144,7 +144,7 @@ const Settings = () => {
defaultUserRole: settings.default_user_role || "user",
githubRepoUrl:
settings.github_repo_url ||
"git@github.com:9technologygroup/patchmon.net.git",
"https://github.com/PatchMon/PatchMon.git",
repositoryType: settings.repository_type || "public",
sshKeyPath: settings.ssh_key_path || "",
useCustomSshKey: !!settings.ssh_key_path,

View File

@@ -221,7 +221,82 @@ export const packagesAPI = {
};
// Utility functions
export const isCorsError = (error) => {
// Check for browser-level CORS errors (when request is blocked before reaching server)
if (error.message?.includes("Failed to fetch") && !error.response) {
return true;
}
// Check for TypeError with Failed to fetch (common CORS error pattern)
if (
error.name === "TypeError" &&
error.message?.includes("Failed to fetch")
) {
return true;
}
// Check for backend CORS errors that get converted to 500 by proxy
if (error.response?.status === 500) {
// Check if the error message contains CORS-related text
if (
error.message?.includes("Not allowed by CORS") ||
error.message?.includes("CORS") ||
error.message?.includes("cors")
) {
return true;
}
// Check if the response data contains CORS error information
if (
error.response?.data?.error?.includes("CORS") ||
error.response?.data?.error?.includes("cors") ||
error.response?.data?.message?.includes("CORS") ||
error.response?.data?.message?.includes("cors") ||
error.response?.data?.message?.includes("Not allowed by CORS")
) {
return true;
}
// Check for specific CORS error patterns from backend logs
if (
error.message?.includes("origin") &&
error.message?.includes("callback")
) {
return true;
}
// Check if this is likely a CORS error based on context
// If we're accessing from localhost but CORS_ORIGIN is set to fabio, this is likely CORS
const currentOrigin = window.location.origin;
if (
currentOrigin === "http://localhost:3000" &&
error.config?.url?.includes("/api/")
) {
// This is likely a CORS error when accessing from localhost
return true;
}
}
// Check for CORS-related errors
return (
error.message?.includes("CORS") ||
error.message?.includes("cors") ||
error.message?.includes("Access to fetch") ||
error.message?.includes("blocked by CORS policy") ||
error.message?.includes("Cross-Origin Request Blocked") ||
error.message?.includes("NetworkError when attempting to fetch resource") ||
error.message?.includes("ERR_BLOCKED_BY_CLIENT") ||
error.message?.includes("ERR_NETWORK") ||
error.message?.includes("ERR_CONNECTION_REFUSED")
);
};
export const formatError = (error) => {
// Check for CORS-related errors
if (isCorsError(error)) {
return "CORS_ORIGIN mismatch - please set your URL in your environment variable";
}
if (error.response?.data?.message) {
return error.response.data.message;
}

169
setup.sh
View File

@@ -436,6 +436,57 @@ generate_jwt_secret() {
openssl rand -base64 64 | tr -d "=+/" | cut -c1-50
}
# Generate Redis password
generate_redis_password() {
openssl rand -base64 32 | tr -d "=+/" | cut -c1-25
}
# Find next available Redis database
find_next_redis_db() {
print_info "Finding next available Redis database..."
# Start from database 0 and keep checking until we find an empty one
local db_num=0
local max_attempts=16 # Redis default is 16 databases
while [ $db_num -lt $max_attempts ]; do
# Test if database is empty
local key_count
local redis_output
# Try to get database size
redis_output=$(redis-cli -h localhost -p 6379 -n "$db_num" DBSIZE 2>&1)
# Check for errors
if echo "$redis_output" | grep -q "ERR"; then
if echo "$redis_output" | grep -q "invalid DB index"; then
print_warning "Reached maximum database limit at database $db_num"
break
else
print_error "Error checking database $db_num: $redis_output"
return 1
fi
fi
key_count="$redis_output"
# If database is empty, use it
if [ "$key_count" = "0" ]; then
print_status "Found available Redis database: $db_num (empty)"
echo "$db_num"
return 0
fi
print_info "Database $db_num has $key_count keys, checking next..."
db_num=$((db_num + 1))
done
print_warning "No available Redis databases found (checked 0-$max_attempts)"
print_info "Using database 0 (may have existing data)"
echo "0"
return 0
}
# Initialize instance variables
init_instance_vars() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] init_instance_vars function started" >> "$DEBUG_LOG"
@@ -467,6 +518,12 @@ init_instance_vars() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Generating JWT secret..." >> "$DEBUG_LOG"
JWT_SECRET=$(generate_jwt_secret)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Generating Redis password..." >> "$DEBUG_LOG"
REDIS_PASSWORD=$(generate_redis_password)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Finding next available Redis database..." >> "$DEBUG_LOG"
REDIS_DB=$(find_next_redis_db)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Generating random backend port..." >> "$DEBUG_LOG"
# Generate random backend port (3001-3999)
@@ -584,6 +641,104 @@ install_redis() {
fi
}
# Configure Redis with user authentication
configure_redis() {
print_info "Configuring Redis with user authentication..."
# Check if Redis is running
if ! systemctl is-active --quiet redis-server; then
print_error "Redis is not running. Please start Redis first."
return 1
fi
# Generate Redis username based on instance
REDIS_USER="patchmon_${DB_SAFE_NAME}"
# Generate separate user password (more secure than reusing admin password)
REDIS_USER_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
print_info "Creating Redis user: $REDIS_USER for database $REDIS_DB"
# Create Redis configuration backup
if [ -f /etc/redis/redis.conf ]; then
cp /etc/redis/redis.conf /etc/redis/redis.conf.backup.$(date +%Y%m%d_%H%M%S)
print_info "Created Redis configuration backup"
fi
# Configure Redis with admin password first
print_info "Setting Redis admin password"
# Add password configuration to redis.conf
if ! grep -q "^requirepass" /etc/redis/redis.conf; then
echo "requirepass $REDIS_PASSWORD" >> /etc/redis/redis.conf
print_status "Added admin password configuration to Redis"
else
# Update existing password
sed -i "s/^requirepass.*/requirepass $REDIS_PASSWORD/" /etc/redis/redis.conf
print_status "Updated Redis admin password configuration"
fi
# Restart Redis to apply admin password
print_info "Restarting Redis to apply admin password configuration..."
systemctl restart redis-server
# Wait for Redis to start
sleep 3
# Test admin connection
if ! redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASSWORD" --no-auth-warning ping > /dev/null 2>&1; then
print_error "Failed to configure Redis admin password"
return 1
fi
print_status "Redis admin password configuration successful"
# Create Redis user with ACL
print_info "Creating Redis ACL user: $REDIS_USER"
# Create user with password and permissions - capture output for error handling
local acl_result
acl_result=$(redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASSWORD" --no-auth-warning ACL SETUSER "$REDIS_USER" on ">${REDIS_USER_PASSWORD}" ~* +@all 2>&1)
if [ "$acl_result" = "OK" ]; then
print_status "Redis user '$REDIS_USER' created successfully"
# Verify user was actually created
local verify_result
verify_result=$(redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASSWORD" --no-auth-warning ACL GETUSER "$REDIS_USER" 2>&1)
if [ "$verify_result" = "(nil)" ]; then
print_error "User creation reported OK but user does not exist"
return 1
fi
else
print_error "Failed to create Redis user: $acl_result"
return 1
fi
# Test user connection
print_info "Testing Redis user connection..."
if redis-cli -h 127.0.0.1 -p 6379 --user "$REDIS_USER" --pass "$REDIS_USER_PASSWORD" --no-auth-warning -n "$REDIS_DB" ping > /dev/null 2>&1; then
print_status "Redis user connection test successful"
else
print_error "Redis user connection test failed"
return 1
fi
# Mark the selected database as in-use
redis-cli -h 127.0.0.1 -p 6379 --user "$REDIS_USER" --pass "$REDIS_USER_PASSWORD" --no-auth-warning -n "$REDIS_DB" SET "patchmon:initialized" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /dev/null
print_status "Marked Redis database $REDIS_DB as in-use"
# Update .env with the USER PASSWORD, not admin password
echo "REDIS_USER=$REDIS_USER" >> .env
echo "REDIS_PASSWORD=$REDIS_USER_PASSWORD" >> .env
echo "REDIS_DB=$REDIS_DB" >> .env
print_status "Redis user password: $REDIS_USER_PASSWORD"
return 0
}
# Install nginx
install_nginx() {
print_info "Installing nginx..."
@@ -875,8 +1030,9 @@ AGENT_RATE_LIMIT_MAX=1000
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
REDIS_USER=$REDIS_USER
REDIS_PASSWORD=$REDIS_PASSWORD
REDIS_DB=$REDIS_DB
# Logging
LOG_LEVEL=info
@@ -1379,8 +1535,9 @@ Database Information:
Redis Information:
- Host: localhost
- Port: 6379
- Password: (none - Redis runs without authentication)
- Database: 0
- User: $REDIS_USER
- Password: $REDIS_PASSWORD
- Database: $REDIS_DB
Networking:
- Backend Port: $BACKEND_PORT
@@ -1533,6 +1690,9 @@ deploy_instance() {
echo -e "${YELLOW}Database Name: $DB_NAME${NC}"
echo -e "${YELLOW}Database User: $DB_USER${NC}"
echo -e "${YELLOW}Database Password: $DB_PASS${NC}"
echo -e "${YELLOW}Redis User: $REDIS_USER${NC}"
echo -e "${YELLOW}Redis Password: $REDIS_PASSWORD${NC}"
echo -e "${YELLOW}Redis Database: $REDIS_DB${NC}"
echo -e "${YELLOW}JWT Secret: $JWT_SECRET${NC}"
echo -e "${YELLOW}Backend Port: $BACKEND_PORT${NC}"
echo -e "${YELLOW}Instance User: $INSTANCE_USER${NC}"
@@ -1543,6 +1703,7 @@ deploy_instance() {
install_nodejs
install_postgresql
install_redis
configure_redis
install_nginx
# Only install certbot if SSL is enabled

249
tools/setup-redis.sh Executable file
View File

@@ -0,0 +1,249 @@
#!/bin/bash
# redis-setup.sh - Redis Database and User Setup for PatchMon
# This script creates a dedicated Redis database and user for a PatchMon instance
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default Redis connection details
REDIS_HOST=${REDIS_HOST:-"localhost"}
REDIS_PORT=${REDIS_PORT:-6379}
REDIS_ADMIN_PASSWORD=${REDIS_ADMIN_PASSWORD:-""}
echo -e "${BLUE}🔧 PatchMon Redis Setup${NC}"
echo "=================================="
# Function to generate random strings
generate_random_string() {
local length=${1:-16}
openssl rand -base64 $length | tr -d "=+/" | cut -c1-$length
}
# Function to check if Redis is accessible
check_redis_connection() {
echo -e "${YELLOW}📡 Checking Redis connection...${NC}"
if [ -n "$REDIS_ADMIN_PASSWORD" ]; then
# With password
if redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_ADMIN_PASSWORD" --no-auth-warning ping > /dev/null 2>&1; then
echo -e "${GREEN}✅ Redis connection successful${NC}"
return 0
else
echo -e "${RED}❌ Cannot connect to Redis with password${NC}"
echo "Please ensure Redis is running and the admin password is correct"
return 1
fi
else
# Without password
if redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ping > /dev/null 2>&1; then
echo -e "${GREEN}✅ Redis connection successful${NC}"
return 0
else
echo -e "${RED}❌ Cannot connect to Redis${NC}"
echo "Please ensure Redis is running"
return 1
fi
fi
}
# Function to find next available database number
find_next_db() {
echo -e "${YELLOW}🔍 Finding next available database...${NC}" >&2
# Start from database 0 and keep checking until we find an empty one
local db_num=0
local max_attempts=100 # Safety limit to prevent infinite loop
while [ $db_num -lt $max_attempts ]; do
# Test if database is empty
local key_count
local redis_output
if [ -n "$REDIS_ADMIN_PASSWORD" ]; then
# With password
redis_output=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_ADMIN_PASSWORD" --no-auth-warning -n "$db_num" DBSIZE 2>&1)
else
# Without password
redis_output=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -n "$db_num" DBSIZE 2>&1)
fi
# Check for authentication errors
if echo "$redis_output" | grep -q "NOAUTH"; then
echo -e "${RED}❌ Authentication required but REDIS_ADMIN_PASSWORD not set${NC}" >&2
echo -e "${YELLOW}💡 Please set REDIS_ADMIN_PASSWORD environment variable:${NC}" >&2
echo -e "${YELLOW} export REDIS_ADMIN_PASSWORD='your_password'${NC}" >&2
echo -e "${YELLOW} Or run: REDIS_ADMIN_PASSWORD='your_password' ./setup-redis.sh${NC}" >&2
exit 1
fi
# Check for other errors
if echo "$redis_output" | grep -q "ERR"; then
if echo "$redis_output" | grep -q "invalid DB index"; then
echo -e "${RED}❌ Reached maximum database limit at database $db_num${NC}" >&2
echo -e "${YELLOW}💡 Redis is configured with $db_num databases maximum.${NC}" >&2
echo -e "${YELLOW}💡 Increase 'databases' setting in redis.conf or clean up unused databases.${NC}" >&2
exit 1
else
echo -e "${RED}❌ Error checking database $db_num: $redis_output${NC}" >&2
exit 1
fi
fi
key_count="$redis_output"
# If database is empty, use it
if [ "$key_count" = "0" ]; then
echo -e "${GREEN}✅ Found available database: $db_num (empty)${NC}" >&2
echo "$db_num"
return
fi
echo -e "${BLUE} Database $db_num has $key_count keys, checking next...${NC}" >&2
db_num=$((db_num + 1))
done
echo -e "${RED}❌ No available databases found (checked 0-$max_attempts)${NC}" >&2
echo -e "${YELLOW}💡 All checked databases are in use. Consider cleaning up unused databases.${NC}" >&2
exit 1
}
# Function to create Redis user
create_redis_user() {
local username="$1"
local password="$2"
local db_num="$3"
echo -e "${YELLOW}👤 Creating Redis user: $username for database $db_num${NC}"
# Create user with password and permissions
# Note: >password syntax is for Redis ACL, we need to properly escape it
local result
if [ -n "$REDIS_ADMIN_PASSWORD" ]; then
# With password
result=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_ADMIN_PASSWORD" --no-auth-warning ACL SETUSER "$username" on ">${password}" ~* +@all 2>&1)
else
# Without password
result=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ACL SETUSER "$username" on ">${password}" ~* +@all 2>&1)
fi
if [ $? -eq 0 ] && [ "$result" = "OK" ]; then
echo -e "${GREEN}✅ Redis user '$username' created successfully for database $db_num${NC}"
# Verify user was created
if [ -n "$REDIS_ADMIN_PASSWORD" ]; then
local verify=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_ADMIN_PASSWORD" --no-auth-warning ACL GETUSER "$username" 2>&1)
else
local verify=$(redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ACL GETUSER "$username" 2>&1)
fi
if [ "$verify" = "(nil)" ]; then
echo -e "${RED}❌ User creation reported OK but user does not exist${NC}"
return 1
fi
return 0
else
echo -e "${RED}❌ Failed to create Redis user${NC}"
echo -e "${RED}Error: $result${NC}"
return 1
fi
}
# Function to test user connection
test_user_connection() {
local username="$1"
local password="$2"
local db_num="$3"
echo -e "${YELLOW}🧪 Testing user connection...${NC}"
if redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" --user "$username" --pass "$password" --no-auth-warning -n "$db_num" ping > /dev/null 2>&1; then
echo -e "${GREEN}✅ User connection test successful${NC}"
return 0
else
echo -e "${RED}❌ User connection test failed${NC}"
return 1
fi
}
# Function to mark database as in-use
mark_database_in_use() {
local db_num="$1"
echo -e "${YELLOW}📝 Marking database as in-use...${NC}"
if [ -n "$REDIS_ADMIN_PASSWORD" ]; then
redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_ADMIN_PASSWORD" --no-auth-warning -n "$db_num" SET "patchmon:initialized" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /dev/null
else
redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -n "$db_num" SET "patchmon:initialized" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /dev/null
fi
if [ $? -eq 0 ]; then
echo -e "${GREEN}✅ Database marked as in-use${NC}"
return 0
else
echo -e "${RED}❌ Failed to mark database${NC}"
return 1
fi
}
# Main execution
main() {
# Check Redis connection
if ! check_redis_connection; then
exit 1
fi
# Generate random credentials
USERNAME="patchmon_$(generate_random_string 8)"
PASSWORD=$(generate_random_string 32)
DB_NUM=$(find_next_db)
echo ""
echo -e "${BLUE}📋 Generated Configuration:${NC}"
echo "Username: $USERNAME"
echo "Password: $PASSWORD"
echo "Database: $DB_NUM"
echo ""
# Create Redis user
if ! create_redis_user "$USERNAME" "$PASSWORD" "$DB_NUM"; then
exit 1
fi
# Test user connection
if ! test_user_connection "$USERNAME" "$PASSWORD" "$DB_NUM"; then
exit 1
fi
# Mark database as in-use to prevent reuse on next run
if ! mark_database_in_use "$DB_NUM"; then
exit 1
fi
# Output .env configuration
echo ""
echo -e "${GREEN}🎉 Redis setup completed successfully!${NC}"
echo ""
echo -e "${BLUE}📄 Add these lines to your .env file:${NC}"
echo "=================================="
echo "REDIS_HOST=$REDIS_HOST"
echo "REDIS_PORT=$REDIS_PORT"
echo "REDIS_USER=$USERNAME"
echo "REDIS_PASSWORD=$PASSWORD"
echo "REDIS_DB=$DB_NUM"
echo "=================================="
echo ""
echo -e "${YELLOW}💡 Copy the configuration above to your .env file${NC}"
}
# Run main function
main "$@"