From 5004e062b4f2f09cba8b481d0dc31b87b2fe126f Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Sat, 18 Oct 2025 16:14:09 +0100 Subject: [PATCH] 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 --- .dockerignore | 34 +++ backend/env.example | 1 + backend/prisma/schema.prisma | 2 +- backend/src/routes/versionRoutes.js | 2 +- backend/src/server.js | 13 +- .../services/automation/githubUpdateCheck.js | 2 +- .../src/services/automation/shared/redis.js | 1 + docker/backend.Dockerfile.dockerignore | 2 + docker/docker-compose.yml | 24 +- docker/nginx.conf.template | 10 +- docker/redis.conf | 35 --- .../src/components/FirstTimeAdminSetup.jsx | 33 ++- frontend/src/contexts/AuthContext.jsx | 122 ++++++++- frontend/src/pages/Login.jsx | 62 ++++- frontend/src/pages/Profile.jsx | 24 +- frontend/src/pages/Settings.jsx | 2 +- frontend/src/utils/api.js | 75 ++++++ setup-redis.sh | 234 ++++++++++++++++++ 18 files changed, 606 insertions(+), 72 deletions(-) create mode 100644 .dockerignore delete mode 100644 docker/redis.conf create mode 100755 setup-redis.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..159029f --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/backend/env.example b/backend/env.example index 10d9ac3..7a36ec9 100644 --- a/backend/env.example +++ b/backend/env.example @@ -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 diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 81230ba..241b3f6 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -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? diff --git a/backend/src/routes/versionRoutes.js b/backend/src/routes/versionRoutes.js index 550f151..0fcaded 100644 --- a/backend/src/routes/versionRoutes.js +++ b/backend/src/routes/versionRoutes.js @@ -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(); diff --git a/backend/src/server.js b/backend/src/server.js index 121b312..5f631ce 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -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, diff --git a/backend/src/services/automation/githubUpdateCheck.js b/backend/src/services/automation/githubUpdateCheck.js index a9cdd0f..9725918 100644 --- a/backend/src/services/automation/githubUpdateCheck.js +++ b/backend/src/services/automation/githubUpdateCheck.js @@ -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; diff --git a/backend/src/services/automation/shared/redis.js b/backend/src/services/automation/shared/redis.js index 62aa644..d5afccf 100644 --- a/backend/src/services/automation/shared/redis.js +++ b/backend/src/services/automation/shared/redis.js @@ -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 diff --git a/docker/backend.Dockerfile.dockerignore b/docker/backend.Dockerfile.dockerignore index 523b439..c1e6080 100644 --- a/docker/backend.Dockerfile.dockerignore +++ b/docker/backend.Dockerfile.dockerignore @@ -1 +1,3 @@ **/env.example +**/.env +**/.env.* diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3c9660b..cac66de 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -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 diff --git a/docker/nginx.conf.template b/docker/nginx.conf.template index dc96ca2..8dc395a 100644 --- a/docker/nginx.conf.template +++ b/docker/nginx.conf.template @@ -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') { diff --git a/docker/redis.conf b/docker/redis.conf deleted file mode 100644 index b0b7640..0000000 --- a/docker/redis.conf +++ /dev/null @@ -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 "" diff --git a/frontend/src/components/FirstTimeAdminSetup.jsx b/frontend/src/components/FirstTimeAdminSetup.jsx index 09e1f97..61599f7 100644 --- a/frontend/src/components/FirstTimeAdminSetup.jsx +++ b/frontend/src/components/FirstTimeAdminSetup.jsx @@ -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); } diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index b7d7cc9..5e32dd7 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -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" }; } }; diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 49fc5ed..629718d 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -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 { diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx index fa13720..568f548 100644 --- a/frontend/src/pages/Profile.jsx +++ b/frontend/src/pages/Profile.jsx @@ -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); } diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 60539f8..d7b3406 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -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, diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index f105a37..24740fb 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -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; } diff --git a/setup-redis.sh b/setup-redis.sh new file mode 100755 index 0000000..7b139da --- /dev/null +++ b/setup-redis.sh @@ -0,0 +1,234 @@ +#!/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:-"redispass1"} + +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 + if [ -n "$REDIS_ADMIN_PASSWORD" ]; then + # With password + redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_ADMIN_PASSWORD" --no-auth-warning ACL SETUSER "$username" on ">${password}" ~* +@all > /dev/null + else + # Without password + redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ACL SETUSER "$username" on ">${password}" ~* +@all > /dev/null + fi + + if [ $? -eq 0 ]; then + echo -e "${GREEN}โœ… Redis user '$username' created successfully for database $db_num${NC}" + return 0 + else + echo -e "${RED}โŒ Failed to create Redis user${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 "$@"