mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-22 23:32:03 +00:00
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
This commit is contained in:
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
|
@@ -6,6 +6,7 @@ PM_DB_CONN_WAIT_INTERVAL=2
|
|||||||
# Redis Configuration
|
# Redis Configuration
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
REDIS_USER=your-redis-username-here
|
||||||
REDIS_PASSWORD=your-redis-password-here
|
REDIS_PASSWORD=your-redis-password-here
|
||||||
REDIS_DB=0
|
REDIS_DB=0
|
||||||
|
|
||||||
|
@@ -179,7 +179,7 @@ model settings {
|
|||||||
updated_at DateTime
|
updated_at DateTime
|
||||||
update_interval Int @default(60)
|
update_interval Int @default(60)
|
||||||
auto_update Boolean @default(false)
|
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?
|
ssh_key_path String?
|
||||||
repository_type String @default("public")
|
repository_type String @default("public")
|
||||||
last_update_check DateTime?
|
last_update_check DateTime?
|
||||||
|
@@ -6,7 +6,7 @@ const { PrismaClient } = require("@prisma/client");
|
|||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
// Default GitHub repository URL
|
// 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();
|
const router = express.Router();
|
||||||
|
|
||||||
|
@@ -339,9 +339,7 @@ const parseOrigins = (val) =>
|
|||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const allowedOrigins = parseOrigins(
|
const allowedOrigins = parseOrigins(
|
||||||
process.env.CORS_ORIGINS ||
|
process.env.CORS_ORIGINS || process.env.CORS_ORIGIN || "http://fabio:3000",
|
||||||
process.env.CORS_ORIGIN ||
|
|
||||||
"http://localhost:3000",
|
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
@@ -564,6 +562,15 @@ app.use((err, _req, res, _next) => {
|
|||||||
if (process.env.ENABLE_LOGGING === "true") {
|
if (process.env.ENABLE_LOGGING === "true") {
|
||||||
logger.error(err.stack);
|
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({
|
res.status(500).json({
|
||||||
error: "Something went wrong!",
|
error: "Something went wrong!",
|
||||||
message: process.env.NODE_ENV === "development" ? err.message : undefined,
|
message: process.env.NODE_ENV === "development" ? err.message : undefined,
|
||||||
|
@@ -21,7 +21,7 @@ class GitHubUpdateCheck {
|
|||||||
try {
|
try {
|
||||||
// Get settings
|
// Get settings
|
||||||
const settings = await prisma.settings.findFirst();
|
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;
|
const repoUrl = settings?.githubRepoUrl || DEFAULT_GITHUB_REPO;
|
||||||
let owner, repo;
|
let owner, repo;
|
||||||
|
|
||||||
|
@@ -5,6 +5,7 @@ const redisConnection = {
|
|||||||
host: process.env.REDIS_HOST || "localhost",
|
host: process.env.REDIS_HOST || "localhost",
|
||||||
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
|
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
|
||||||
password: process.env.REDIS_PASSWORD || undefined,
|
password: process.env.REDIS_PASSWORD || undefined,
|
||||||
|
username: process.env.REDIS_USER || undefined,
|
||||||
db: parseInt(process.env.REDIS_DB, 10) || 0,
|
db: parseInt(process.env.REDIS_DB, 10) || 0,
|
||||||
retryDelayOnFailover: 100,
|
retryDelayOnFailover: 100,
|
||||||
maxRetriesPerRequest: null, // BullMQ requires this to be null
|
maxRetriesPerRequest: null, // BullMQ requires this to be null
|
||||||
|
@@ -1 +1,3 @@
|
|||||||
**/env.example
|
**/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
|
name: patchmon
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -7,7 +23,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: patchmon_db
|
POSTGRES_DB: patchmon_db
|
||||||
POSTGRES_USER: patchmon_user
|
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:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -19,11 +35,11 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: unless-stopped
|
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:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
healthcheck:
|
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
|
interval: 3s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 7
|
retries: 7
|
||||||
@@ -35,7 +51,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
LOG_LEVEL: info
|
LOG_LEVEL: info
|
||||||
DATABASE_URL: postgresql://patchmon_user:REPLACE_YOUR_POSTGRES_PASSWORD_HERE@database:5432/patchmon_db
|
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_PROTOCOL: http
|
||||||
SERVER_HOST: localhost
|
SERVER_HOST: localhost
|
||||||
SERVER_PORT: 3000
|
SERVER_PORT: 3000
|
||||||
|
@@ -41,7 +41,7 @@ server {
|
|||||||
# Preserve original client IP through proxy chain
|
# Preserve original client IP through proxy chain
|
||||||
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
|
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-Origin * always;
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" 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;
|
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_request_buffering off;
|
||||||
proxy_max_temp_file_size 0;
|
proxy_max_temp_file_size 0;
|
||||||
|
|
||||||
# CORS headers for SSE
|
# CORS headers for SSE - commented out to let backend handle CORS
|
||||||
add_header Access-Control-Allow-Origin * always;
|
# add_header Access-Control-Allow-Origin * always;
|
||||||
add_header Access-Control-Allow-Methods "GET, OPTIONS" 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;
|
# 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
|
# Handle preflight requests
|
||||||
if ($request_method = 'OPTIONS') {
|
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 { useId, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
import { isCorsError } from "../utils/api";
|
||||||
|
|
||||||
const FirstTimeAdminSetup = () => {
|
const FirstTimeAdminSetup = () => {
|
||||||
const { login, setAuthState } = useAuth();
|
const { login, setAuthState } = useAuth();
|
||||||
@@ -121,11 +122,39 @@ const FirstTimeAdminSetup = () => {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error("Setup error:", 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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { flushSync } from "react-dom";
|
import { flushSync } from "react-dom";
|
||||||
import { AUTH_PHASES, isAuthPhase } from "../constants/authPhases";
|
import { AUTH_PHASES, isAuthPhase } from "../constants/authPhases";
|
||||||
|
import { isCorsError } from "../utils/api";
|
||||||
|
|
||||||
const AuthContext = createContext();
|
const AuthContext = createContext();
|
||||||
|
|
||||||
@@ -120,9 +121,50 @@ export const AuthProvider = ({ children }) => {
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} else {
|
} 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" };
|
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" };
|
return { success: false, error: "Network error occurred" };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -167,9 +209,46 @@ export const AuthProvider = ({ children }) => {
|
|||||||
localStorage.setItem("user", JSON.stringify(data.user));
|
localStorage.setItem("user", JSON.stringify(data.user));
|
||||||
return { success: true, user: data.user };
|
return { success: true, user: data.user };
|
||||||
} else {
|
} 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" };
|
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" };
|
return { success: false, error: "Network error occurred" };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -190,12 +269,49 @@ export const AuthProvider = ({ children }) => {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} else {
|
} 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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: data.error || "Password change failed",
|
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" };
|
return { success: false, error: "Network error occurred" };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -13,7 +13,7 @@ import { useEffect, useId, useState } from "react";
|
|||||||
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { authAPI } from "../utils/api";
|
import { authAPI, isCorsError } from "../utils/api";
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const usernameId = useId();
|
const usernameId = useId();
|
||||||
@@ -82,7 +82,21 @@ const Login = () => {
|
|||||||
setError(result.error || "Login failed");
|
setError(result.error || "Login failed");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -112,12 +126,25 @@ const Login = () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Signup error:", err);
|
console.error("Signup error:", err);
|
||||||
const errorMessage =
|
if (isCorsError(err)) {
|
||||||
err.response?.data?.error ||
|
setError(
|
||||||
(err.response?.data?.errors && err.response.data.errors.length > 0
|
"CORS_ORIGIN mismatch - please set your URL in your environment variable",
|
||||||
? err.response.data.errors.map((e) => e.msg).join(", ")
|
);
|
||||||
: err.message || "Signup failed");
|
} else if (
|
||||||
setError(errorMessage);
|
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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -146,9 +173,22 @@ const Login = () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("TFA verification error:", err);
|
console.error("TFA verification error:", err);
|
||||||
const errorMessage =
|
if (isCorsError(err)) {
|
||||||
err.response?.data?.error || err.message || "TFA verification failed";
|
setError(
|
||||||
setError(errorMessage);
|
"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
|
// Clear the token input for security
|
||||||
setTfaData({ token: "" });
|
setTfaData({ token: "" });
|
||||||
} finally {
|
} finally {
|
||||||
|
@@ -26,7 +26,7 @@ import { useEffect, useId, useState } from "react";
|
|||||||
|
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { useTheme } from "../contexts/ThemeContext";
|
import { useTheme } from "../contexts/ThemeContext";
|
||||||
import { tfaAPI } from "../utils/api";
|
import { isCorsError, tfaAPI } from "../utils/api";
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const usernameId = useId();
|
const usernameId = useId();
|
||||||
@@ -88,8 +88,15 @@ const Profile = () => {
|
|||||||
text: result.error || "Failed to update profile",
|
text: result.error || "Failed to update profile",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
setMessage({ type: "error", text: "Network error occurred" });
|
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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
@@ -133,8 +140,15 @@ const Profile = () => {
|
|||||||
text: result.error || "Failed to change password",
|
text: result.error || "Failed to change password",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
setMessage({ type: "error", text: "Network error occurred" });
|
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 {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
@@ -144,7 +144,7 @@ const Settings = () => {
|
|||||||
defaultUserRole: settings.default_user_role || "user",
|
defaultUserRole: settings.default_user_role || "user",
|
||||||
githubRepoUrl:
|
githubRepoUrl:
|
||||||
settings.github_repo_url ||
|
settings.github_repo_url ||
|
||||||
"git@github.com:9technologygroup/patchmon.net.git",
|
"https://github.com/PatchMon/PatchMon.git",
|
||||||
repositoryType: settings.repository_type || "public",
|
repositoryType: settings.repository_type || "public",
|
||||||
sshKeyPath: settings.ssh_key_path || "",
|
sshKeyPath: settings.ssh_key_path || "",
|
||||||
useCustomSshKey: !!settings.ssh_key_path,
|
useCustomSshKey: !!settings.ssh_key_path,
|
||||||
|
@@ -221,7 +221,82 @@ export const packagesAPI = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Utility functions
|
// 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) => {
|
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) {
|
if (error.response?.data?.message) {
|
||||||
return error.response.data.message;
|
return error.response.data.message;
|
||||||
}
|
}
|
||||||
|
234
setup-redis.sh
Executable file
234
setup-redis.sh
Executable file
@@ -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 "$@"
|
Reference in New Issue
Block a user