mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-02 21:13:45 +00:00
Compare commits
10 Commits
52c8ba6b03
...
4b35fc9ab9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b35fc9ab9 | ||
|
|
191a1afada | ||
|
|
175f10b8b7 | ||
|
|
080bcbe22e | ||
|
|
3175ed79a5 | ||
|
|
fba6d0ede5 | ||
|
|
54a5012012 | ||
|
|
5004e062b4 | ||
|
|
44d52a5536 | ||
|
|
c328123bd3 |
34
.dockerignore
Normal file
34
.dockerignore
Normal 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
BIN
agents/patchmon-agent-linux-arm
Executable file
Binary file not shown.
Binary file not shown.
555
agents/patchmon-agent.sh
Executable file
555
agents/patchmon-agent.sh
Executable 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
**/env.example
|
||||
**/.env
|
||||
**/.env.*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 ""
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
169
setup.sh
@@ -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
249
tools/setup-redis.sh
Executable 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 "$@"
|
||||
Reference in New Issue
Block a user