mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-11 09:27:30 +00:00
Compare commits
3 Commits
feature/ap
...
renovate/b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ba6ae9f25 | ||
|
|
427743b81e | ||
|
|
a4922b4e54 |
@@ -1,32 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/bash
|
||||||
# PatchMon Agent Installation Script
|
|
||||||
# This script requires bash for full functionality
|
|
||||||
# Usage: curl -s {PATCHMON_URL}/api/v1/hosts/install -H "X-API-ID: {API_ID}" -H "X-API-KEY: {API_KEY}" | sh
|
|
||||||
|
|
||||||
# Check if bash is available, if not try to install it (for Alpine Linux)
|
|
||||||
if ! command -v bash >/dev/null 2>&1; then
|
|
||||||
if command -v apk >/dev/null 2>&1; then
|
|
||||||
echo "Installing bash for script compatibility..."
|
|
||||||
apk add --no-cache bash >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If bash is available and we're not already running in bash, switch to bash
|
|
||||||
# When piped, we can't re-execute easily, so we'll continue with sh
|
|
||||||
# but ensure bash is available for bash-specific features
|
|
||||||
if command -v bash >/dev/null 2>&1 && [ -z "${BASH_VERSION:-}" ]; then
|
|
||||||
# Check if we're being piped (stdin is not a terminal)
|
|
||||||
if [ -t 0 ]; then
|
|
||||||
# Direct execution, re-execute with bash
|
|
||||||
exec bash "$0" "$@"
|
|
||||||
exit $?
|
|
||||||
fi
|
|
||||||
# When piped, we continue with sh but bash is now available
|
|
||||||
# The script will use bash-specific features which should work if bash is installed
|
|
||||||
fi
|
|
||||||
|
|
||||||
# PatchMon Agent Installation Script
|
# PatchMon Agent Installation Script
|
||||||
# Usage: curl -s {PATCHMON_URL}/api/v1/hosts/install -H "X-API-ID: {API_ID}" -H "X-API-KEY: {API_KEY}" | sh
|
# Usage: curl -s {PATCHMON_URL}/api/v1/hosts/install -H "X-API-ID: {API_ID}" -H "X-API-KEY: {API_KEY}" | bash
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -61,7 +36,7 @@ warning() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Check if running as root
|
# Check if running as root
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
error "This script must be run as root (use sudo)"
|
error "This script must be run as root (use sudo)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -70,8 +45,8 @@ verify_datetime() {
|
|||||||
info "🕐 Verifying system datetime and timezone..."
|
info "🕐 Verifying system datetime and timezone..."
|
||||||
|
|
||||||
# Get current system time
|
# Get current system time
|
||||||
system_time=$(date)
|
local system_time=$(date)
|
||||||
timezone=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Unknown")
|
local timezone=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "Unknown")
|
||||||
|
|
||||||
# Display current datetime info
|
# Display current datetime info
|
||||||
echo ""
|
echo ""
|
||||||
@@ -81,17 +56,14 @@ verify_datetime() {
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check if we can read from stdin (interactive terminal)
|
# Check if we can read from stdin (interactive terminal)
|
||||||
if [ -t 0 ]; then
|
if [[ -t 0 ]]; then
|
||||||
# Interactive terminal - ask user
|
# Interactive terminal - ask user
|
||||||
printf "Does this date/time look correct to you? (y/N): "
|
read -p "Does this date/time look correct to you? (y/N): " -r response
|
||||||
read -r response
|
if [[ "$response" =~ ^[Yy]$ ]]; then
|
||||||
case "$response" in
|
success "✅ Date/time verification passed"
|
||||||
[Yy]*)
|
echo ""
|
||||||
success "✅ Date/time verification passed"
|
return 0
|
||||||
echo ""
|
else
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${RED}❌ Date/time verification failed${NC}"
|
echo -e "${RED}❌ Date/time verification failed${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -100,10 +72,9 @@ verify_datetime() {
|
|||||||
echo " sudo timedatectl set-timezone 'America/New_York' # or your timezone"
|
echo " sudo timedatectl set-timezone 'America/New_York' # or your timezone"
|
||||||
echo " sudo timedatectl list-timezones # to see available timezones"
|
echo " sudo timedatectl list-timezones # to see available timezones"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}ℹ️ After fixing the date/time, re-run this installation script.${NC}"
|
echo -e "${BLUE}ℹ️ After fixing the date/time, re-run this installation script.${NC}"
|
||||||
error "Installation cancelled - please fix date/time and re-run"
|
error "Installation cancelled - please fix date/time and re-run"
|
||||||
;;
|
fi
|
||||||
esac
|
|
||||||
else
|
else
|
||||||
# Non-interactive (piped from curl) - show warning and continue
|
# Non-interactive (piped from curl) - show warning and continue
|
||||||
echo -e "${YELLOW}⚠️ Non-interactive installation detected${NC}"
|
echo -e "${YELLOW}⚠️ Non-interactive installation detected${NC}"
|
||||||
@@ -150,9 +121,9 @@ cleanup_old_files
|
|||||||
# Generate or retrieve machine ID
|
# Generate or retrieve machine ID
|
||||||
get_machine_id() {
|
get_machine_id() {
|
||||||
# Try multiple sources for machine ID
|
# Try multiple sources for machine ID
|
||||||
if [ -f /etc/machine-id ]; then
|
if [[ -f /etc/machine-id ]]; then
|
||||||
cat /etc/machine-id
|
cat /etc/machine-id
|
||||||
elif [ -f /var/lib/dbus/machine-id ]; then
|
elif [[ -f /var/lib/dbus/machine-id ]]; then
|
||||||
cat /var/lib/dbus/machine-id
|
cat /var/lib/dbus/machine-id
|
||||||
else
|
else
|
||||||
# Fallback: generate from hardware info (less ideal but works)
|
# Fallback: generate from hardware info (less ideal but works)
|
||||||
@@ -161,12 +132,12 @@ get_machine_id() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Parse arguments from environment (passed via HTTP headers)
|
# Parse arguments from environment (passed via HTTP headers)
|
||||||
if [ -z "$PATCHMON_URL" ] || [ -z "$API_ID" ] || [ -z "$API_KEY" ]; then
|
if [[ -z "$PATCHMON_URL" ]] || [[ -z "$API_ID" ]] || [[ -z "$API_KEY" ]]; then
|
||||||
error "Missing required parameters. This script should be called via the PatchMon web interface."
|
error "Missing required parameters. This script should be called via the PatchMon web interface."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Auto-detect architecture if not explicitly set
|
# Auto-detect architecture if not explicitly set
|
||||||
if [ -z "$ARCHITECTURE" ]; then
|
if [[ -z "$ARCHITECTURE" ]]; then
|
||||||
arch_raw=$(uname -m 2>/dev/null || echo "unknown")
|
arch_raw=$(uname -m 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
# Map architecture to supported values
|
# Map architecture to supported values
|
||||||
@@ -191,16 +162,13 @@ if [ -z "$ARCHITECTURE" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Validate architecture
|
# Validate architecture
|
||||||
if [ "$ARCHITECTURE" != "amd64" ] && [ "$ARCHITECTURE" != "386" ] && [ "$ARCHITECTURE" != "arm64" ] && [ "$ARCHITECTURE" != "arm" ]; then
|
if [[ "$ARCHITECTURE" != "amd64" && "$ARCHITECTURE" != "386" && "$ARCHITECTURE" != "arm64" && "$ARCHITECTURE" != "arm" ]]; then
|
||||||
error "Invalid architecture '$ARCHITECTURE'. Must be one of: amd64, 386, arm64, arm"
|
error "Invalid architecture '$ARCHITECTURE'. Must be one of: amd64, 386, arm64, arm"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if --force flag is set (for bypassing broken packages)
|
# Check if --force flag is set (for bypassing broken packages)
|
||||||
FORCE_INSTALL="${FORCE_INSTALL:-false}"
|
FORCE_INSTALL="${FORCE_INSTALL:-false}"
|
||||||
case "$*" in
|
if [[ "$*" == *"--force"* ]] || [[ "$FORCE_INSTALL" == "true" ]]; then
|
||||||
*"--force"*) FORCE_INSTALL="true" ;;
|
|
||||||
esac
|
|
||||||
if [ "$FORCE_INSTALL" = "true" ]; then
|
|
||||||
FORCE_INSTALL="true"
|
FORCE_INSTALL="true"
|
||||||
warning "⚠️ Force mode enabled - will bypass broken packages"
|
warning "⚠️ Force mode enabled - will bypass broken packages"
|
||||||
fi
|
fi
|
||||||
@@ -256,7 +224,7 @@ install_apt_packages() {
|
|||||||
# Build apt-get command based on force mode
|
# Build apt-get command based on force mode
|
||||||
local apt_cmd="apt-get install ${missing_packages[*]} -y"
|
local apt_cmd="apt-get install ${missing_packages[*]} -y"
|
||||||
|
|
||||||
if [ "$FORCE_INSTALL" = "true" ]; then
|
if [[ "$FORCE_INSTALL" == "true" ]]; then
|
||||||
info "Using force mode - bypassing broken packages..."
|
info "Using force mode - bypassing broken packages..."
|
||||||
apt_cmd="$apt_cmd -o APT::Get::Fix-Broken=false -o DPkg::Options::=\"--force-confold\" -o DPkg::Options::=\"--force-confdef\""
|
apt_cmd="$apt_cmd -o APT::Get::Fix-Broken=false -o DPkg::Options::=\"--force-confold\" -o DPkg::Options::=\"--force-confdef\""
|
||||||
fi
|
fi
|
||||||
@@ -272,7 +240,7 @@ install_apt_packages() {
|
|||||||
local all_ok=true
|
local all_ok=true
|
||||||
for pkg in "${packages[@]}"; do
|
for pkg in "${packages[@]}"; do
|
||||||
if ! command_exists "$pkg"; then
|
if ! command_exists "$pkg"; then
|
||||||
if [ "$FORCE_INSTALL" = "true" ]; then
|
if [[ "$FORCE_INSTALL" == "true" ]]; then
|
||||||
error "Critical dependency '$pkg' is not available even with --force. Please install manually."
|
error "Critical dependency '$pkg' is not available even with --force. Please install manually."
|
||||||
else
|
else
|
||||||
error "Critical dependency '$pkg' is not available. Try again with --force flag or install manually: apt-get install $pkg"
|
error "Critical dependency '$pkg' is not available. Try again with --force flag or install manually: apt-get install $pkg"
|
||||||
@@ -311,7 +279,7 @@ install_yum_dnf_packages() {
|
|||||||
|
|
||||||
info "Need to install: ${missing_packages[*]}"
|
info "Need to install: ${missing_packages[*]}"
|
||||||
|
|
||||||
if [ "$pkg_manager" = "yum" ]; then
|
if [[ "$pkg_manager" == "yum" ]]; then
|
||||||
yum install -y "${missing_packages[@]}"
|
yum install -y "${missing_packages[@]}"
|
||||||
else
|
else
|
||||||
dnf install -y "${missing_packages[@]}"
|
dnf install -y "${missing_packages[@]}"
|
||||||
@@ -397,7 +365,7 @@ install_apk_packages() {
|
|||||||
local all_ok=true
|
local all_ok=true
|
||||||
for pkg in "${packages[@]}"; do
|
for pkg in "${packages[@]}"; do
|
||||||
if ! command_exists "$pkg"; then
|
if ! command_exists "$pkg"; then
|
||||||
if [ "$FORCE_INSTALL" = "true" ]; then
|
if [[ "$FORCE_INSTALL" == "true" ]]; then
|
||||||
error "Critical dependency '$pkg' is not available even with --force. Please install manually."
|
error "Critical dependency '$pkg' is not available even with --force. Please install manually."
|
||||||
else
|
else
|
||||||
error "Critical dependency '$pkg' is not available. Try again with --force flag or install manually: apk add $pkg"
|
error "Critical dependency '$pkg' is not available. Try again with --force flag or install manually: apk add $pkg"
|
||||||
@@ -423,7 +391,7 @@ if command -v apt-get >/dev/null 2>&1; then
|
|||||||
|
|
||||||
# Check for broken packages
|
# Check for broken packages
|
||||||
if dpkg -l | grep -q "^iH\|^iF" 2>/dev/null; then
|
if dpkg -l | grep -q "^iH\|^iF" 2>/dev/null; then
|
||||||
if [ "$FORCE_INSTALL" = "true" ]; then
|
if [[ "$FORCE_INSTALL" == "true" ]]; then
|
||||||
warning "Detected broken packages on system - force mode will work around them"
|
warning "Detected broken packages on system - force mode will work around them"
|
||||||
else
|
else
|
||||||
warning "⚠️ Broken packages detected on system"
|
warning "⚠️ Broken packages detected on system"
|
||||||
@@ -478,7 +446,7 @@ echo ""
|
|||||||
info "📁 Setting up configuration directory..."
|
info "📁 Setting up configuration directory..."
|
||||||
|
|
||||||
# Check if configuration directory already exists
|
# Check if configuration directory already exists
|
||||||
if [ -d "/etc/patchmon" ]; then
|
if [[ -d "/etc/patchmon" ]]; then
|
||||||
warning "⚠️ Configuration directory already exists at /etc/patchmon"
|
warning "⚠️ Configuration directory already exists at /etc/patchmon"
|
||||||
warning "⚠️ Preserving existing configuration files"
|
warning "⚠️ Preserving existing configuration files"
|
||||||
|
|
||||||
@@ -495,8 +463,8 @@ fi
|
|||||||
# Check if agent is already configured and working (before we overwrite anything)
|
# Check if agent is already configured and working (before we overwrite anything)
|
||||||
info "🔍 Checking if agent is already configured..."
|
info "🔍 Checking if agent is already configured..."
|
||||||
|
|
||||||
if [ -f /etc/patchmon/config.yml ] && [ -f /etc/patchmon/credentials.yml ]; then
|
if [[ -f /etc/patchmon/config.yml ]] && [[ -f /etc/patchmon/credentials.yml ]]; then
|
||||||
if [ -f /usr/local/bin/patchmon-agent ]; then
|
if [[ -f /usr/local/bin/patchmon-agent ]]; then
|
||||||
info "📋 Found existing agent configuration"
|
info "📋 Found existing agent configuration"
|
||||||
info "🧪 Testing existing configuration with ping..."
|
info "🧪 Testing existing configuration with ping..."
|
||||||
|
|
||||||
@@ -527,7 +495,7 @@ fi
|
|||||||
info "🔐 Creating configuration files..."
|
info "🔐 Creating configuration files..."
|
||||||
|
|
||||||
# Check if config file already exists
|
# Check if config file already exists
|
||||||
if [ -f "/etc/patchmon/config.yml" ]; then
|
if [[ -f "/etc/patchmon/config.yml" ]]; then
|
||||||
warning "⚠️ Config file already exists at /etc/patchmon/config.yml"
|
warning "⚠️ Config file already exists at /etc/patchmon/config.yml"
|
||||||
warning "⚠️ Moving existing file out of the way for fresh installation"
|
warning "⚠️ Moving existing file out of the way for fresh installation"
|
||||||
|
|
||||||
@@ -540,7 +508,7 @@ if [ -f "/etc/patchmon/config.yml" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if credentials file already exists
|
# Check if credentials file already exists
|
||||||
if [ -f "/etc/patchmon/credentials.yml" ]; then
|
if [[ -f "/etc/patchmon/credentials.yml" ]]; then
|
||||||
warning "⚠️ Credentials file already exists at /etc/patchmon/credentials.yml"
|
warning "⚠️ Credentials file already exists at /etc/patchmon/credentials.yml"
|
||||||
warning "⚠️ Moving existing file out of the way for fresh installation"
|
warning "⚠️ Moving existing file out of the way for fresh installation"
|
||||||
|
|
||||||
@@ -553,7 +521,7 @@ if [ -f "/etc/patchmon/credentials.yml" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up old credentials file if it exists (from previous installations)
|
# Clean up old credentials file if it exists (from previous installations)
|
||||||
if [ -f "/etc/patchmon/credentials" ]; then
|
if [[ -f "/etc/patchmon/credentials" ]]; then
|
||||||
warning "⚠️ Found old credentials file, removing it..."
|
warning "⚠️ Found old credentials file, removing it..."
|
||||||
rm -f /etc/patchmon/credentials
|
rm -f /etc/patchmon/credentials
|
||||||
info "📋 Removed old credentials file"
|
info "📋 Removed old credentials file"
|
||||||
@@ -589,7 +557,7 @@ info "📥 Downloading PatchMon agent binary..."
|
|||||||
BINARY_NAME="patchmon-agent-linux-${ARCHITECTURE}"
|
BINARY_NAME="patchmon-agent-linux-${ARCHITECTURE}"
|
||||||
|
|
||||||
# Check if agent binary already exists
|
# Check if agent binary already exists
|
||||||
if [ -f "/usr/local/bin/patchmon-agent" ]; then
|
if [[ -f "/usr/local/bin/patchmon-agent" ]]; then
|
||||||
warning "⚠️ Agent binary already exists at /usr/local/bin/patchmon-agent"
|
warning "⚠️ Agent binary already exists at /usr/local/bin/patchmon-agent"
|
||||||
warning "⚠️ Moving existing file out of the way for fresh installation"
|
warning "⚠️ Moving existing file out of the way for fresh installation"
|
||||||
|
|
||||||
@@ -602,7 +570,7 @@ if [ -f "/usr/local/bin/patchmon-agent" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up old shell script if it exists (from previous installations)
|
# Clean up old shell script if it exists (from previous installations)
|
||||||
if [ -f "/usr/local/bin/patchmon-agent.sh" ]; then
|
if [[ -f "/usr/local/bin/patchmon-agent.sh" ]]; then
|
||||||
warning "⚠️ Found old shell script agent, removing it..."
|
warning "⚠️ Found old shell script agent, removing it..."
|
||||||
rm -f /usr/local/bin/patchmon-agent.sh
|
rm -f /usr/local/bin/patchmon-agent.sh
|
||||||
info "📋 Removed old shell script agent"
|
info "📋 Removed old shell script agent"
|
||||||
@@ -628,7 +596,7 @@ info "📁 Setting up log directory..."
|
|||||||
mkdir -p /etc/patchmon/logs
|
mkdir -p /etc/patchmon/logs
|
||||||
|
|
||||||
# Handle existing log files
|
# Handle existing log files
|
||||||
if [ -f "/etc/patchmon/logs/patchmon-agent.log" ]; then
|
if [[ -f "/etc/patchmon/logs/patchmon-agent.log" ]]; then
|
||||||
warning "⚠️ Existing log file found at /etc/patchmon/logs/patchmon-agent.log"
|
warning "⚠️ Existing log file found at /etc/patchmon/logs/patchmon-agent.log"
|
||||||
warning "⚠️ Rotating log file for fresh start"
|
warning "⚠️ Rotating log file for fresh start"
|
||||||
|
|
||||||
@@ -645,26 +613,23 @@ else
|
|||||||
error "❌ Failed to validate API credentials or reach server"
|
error "❌ Failed to validate API credentials or reach server"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 5: Setup service for WebSocket connection
|
# Step 5: Setup systemd service for WebSocket connection
|
||||||
# Note: The service will automatically send an initial report on startup (see serve.go)
|
# Note: The service will automatically send an initial report on startup (see serve.go)
|
||||||
# Detect init system and create appropriate service
|
info "🔧 Setting up systemd service..."
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
|
||||||
# Systemd is available
|
# Stop and disable existing service if it exists
|
||||||
info "🔧 Setting up systemd service..."
|
if systemctl is-active --quiet patchmon-agent.service 2>/dev/null; then
|
||||||
|
warning "⚠️ Stopping existing PatchMon agent service..."
|
||||||
# Stop and disable existing service if it exists
|
systemctl stop patchmon-agent.service
|
||||||
if systemctl is-active --quiet patchmon-agent.service 2>/dev/null; then
|
fi
|
||||||
warning "⚠️ Stopping existing PatchMon agent service..."
|
|
||||||
systemctl stop patchmon-agent.service
|
if systemctl is-enabled --quiet patchmon-agent.service 2>/dev/null; then
|
||||||
fi
|
warning "⚠️ Disabling existing PatchMon agent service..."
|
||||||
|
systemctl disable patchmon-agent.service
|
||||||
if systemctl is-enabled --quiet patchmon-agent.service 2>/dev/null; then
|
fi
|
||||||
warning "⚠️ Disabling existing PatchMon agent service..."
|
|
||||||
systemctl disable patchmon-agent.service
|
# Create systemd service file
|
||||||
fi
|
cat > /etc/systemd/system/patchmon-agent.service << EOF
|
||||||
|
|
||||||
# Create systemd service file
|
|
||||||
cat > /etc/systemd/system/patchmon-agent.service << EOF
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=PatchMon Agent Service
|
Description=PatchMon Agent Service
|
||||||
After=network.target
|
After=network.target
|
||||||
@@ -686,105 +651,25 @@ SyslogIdentifier=patchmon-agent
|
|||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Clean up old crontab entries if they exist (from previous installations)
|
|
||||||
if crontab -l 2>/dev/null | grep -q "patchmon-agent"; then
|
|
||||||
warning "⚠️ Found old crontab entries, removing them..."
|
|
||||||
crontab -l 2>/dev/null | grep -v "patchmon-agent" | crontab -
|
|
||||||
info "📋 Removed old crontab entries"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Reload systemd and enable/start the service
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable patchmon-agent.service
|
|
||||||
systemctl start patchmon-agent.service
|
|
||||||
|
|
||||||
# Check if service started successfully
|
|
||||||
if systemctl is-active --quiet patchmon-agent.service; then
|
|
||||||
success "✅ PatchMon Agent service started successfully"
|
|
||||||
info "🔗 WebSocket connection established"
|
|
||||||
else
|
|
||||||
warning "⚠️ Service may have failed to start. Check status with: systemctl status patchmon-agent"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SERVICE_TYPE="systemd"
|
|
||||||
elif [ -d /etc/init.d ] && command -v rc-service >/dev/null 2>&1; then
|
|
||||||
# OpenRC is available (Alpine Linux)
|
|
||||||
info "🔧 Setting up OpenRC service..."
|
|
||||||
|
|
||||||
# Stop and disable existing service if it exists
|
|
||||||
if rc-service patchmon-agent status >/dev/null 2>&1; then
|
|
||||||
warning "⚠️ Stopping existing PatchMon agent service..."
|
|
||||||
rc-service patchmon-agent stop
|
|
||||||
fi
|
|
||||||
|
|
||||||
if rc-update show default 2>/dev/null | grep -q "patchmon-agent"; then
|
|
||||||
warning "⚠️ Disabling existing PatchMon agent service..."
|
|
||||||
rc-update del patchmon-agent default
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create OpenRC service file
|
|
||||||
cat > /etc/init.d/patchmon-agent << 'EOF'
|
|
||||||
#!/sbin/openrc-run
|
|
||||||
|
|
||||||
name="patchmon-agent"
|
# Clean up old crontab entries if they exist (from previous installations)
|
||||||
description="PatchMon Agent Service"
|
if crontab -l 2>/dev/null | grep -q "patchmon-agent"; then
|
||||||
command="/usr/local/bin/patchmon-agent"
|
warning "⚠️ Found old crontab entries, removing them..."
|
||||||
command_args="serve"
|
crontab -l 2>/dev/null | grep -v "patchmon-agent" | crontab -
|
||||||
command_user="root"
|
info "📋 Removed old crontab entries"
|
||||||
pidfile="/var/run/patchmon-agent.pid"
|
fi
|
||||||
command_background="yes"
|
|
||||||
working_dir="/etc/patchmon"
|
|
||||||
|
|
||||||
depend() {
|
# Reload systemd and enable/start the service
|
||||||
need net
|
systemctl daemon-reload
|
||||||
after net
|
systemctl enable patchmon-agent.service
|
||||||
}
|
systemctl start patchmon-agent.service
|
||||||
EOF
|
|
||||||
|
# Check if service started successfully
|
||||||
chmod +x /etc/init.d/patchmon-agent
|
if systemctl is-active --quiet patchmon-agent.service; then
|
||||||
|
success "✅ PatchMon Agent service started successfully"
|
||||||
# Clean up old crontab entries if they exist (from previous installations)
|
|
||||||
if crontab -l 2>/dev/null | grep -q "patchmon-agent"; then
|
|
||||||
warning "⚠️ Found old crontab entries, removing them..."
|
|
||||||
crontab -l 2>/dev/null | grep -v "patchmon-agent" | crontab -
|
|
||||||
info "📋 Removed old crontab entries"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Enable and start the service
|
|
||||||
rc-update add patchmon-agent default
|
|
||||||
rc-service patchmon-agent start
|
|
||||||
|
|
||||||
# Check if service started successfully
|
|
||||||
if rc-service patchmon-agent status >/dev/null 2>&1; then
|
|
||||||
success "✅ PatchMon Agent service started successfully"
|
|
||||||
info "🔗 WebSocket connection established"
|
|
||||||
else
|
|
||||||
warning "⚠️ Service may have failed to start. Check status with: rc-service patchmon-agent status"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SERVICE_TYPE="openrc"
|
|
||||||
else
|
|
||||||
# No init system detected, use crontab as fallback
|
|
||||||
warning "⚠️ No init system detected (systemd or OpenRC). Using crontab for service management."
|
|
||||||
|
|
||||||
# Clean up old crontab entries if they exist
|
|
||||||
if crontab -l 2>/dev/null | grep -q "patchmon-agent"; then
|
|
||||||
warning "⚠️ Found old crontab entries, removing them..."
|
|
||||||
crontab -l 2>/dev/null | grep -v "patchmon-agent" | crontab -
|
|
||||||
info "📋 Removed old crontab entries"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add crontab entry to run the agent
|
|
||||||
(crontab -l 2>/dev/null; echo "@reboot /usr/local/bin/patchmon-agent serve >/dev/null 2>&1") | crontab -
|
|
||||||
info "📋 Added crontab entry for PatchMon agent"
|
|
||||||
|
|
||||||
# Start the agent manually
|
|
||||||
/usr/local/bin/patchmon-agent serve >/dev/null 2>&1 &
|
|
||||||
success "✅ PatchMon Agent started in background"
|
|
||||||
info "🔗 WebSocket connection established"
|
info "🔗 WebSocket connection established"
|
||||||
|
else
|
||||||
SERVICE_TYPE="crontab"
|
warning "⚠️ Service may have failed to start. Check status with: systemctl status patchmon-agent"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Installation complete
|
# Installation complete
|
||||||
@@ -795,20 +680,14 @@ echo " • Configuration directory: /etc/patchmon"
|
|||||||
echo " • Agent binary installed: /usr/local/bin/patchmon-agent"
|
echo " • Agent binary installed: /usr/local/bin/patchmon-agent"
|
||||||
echo " • Architecture: $ARCHITECTURE"
|
echo " • Architecture: $ARCHITECTURE"
|
||||||
echo " • Dependencies installed: jq, curl, bc"
|
echo " • Dependencies installed: jq, curl, bc"
|
||||||
if [ "$SERVICE_TYPE" = "systemd" ]; then
|
echo " • Systemd service configured and running"
|
||||||
echo " • Systemd service configured and running"
|
|
||||||
elif [ "$SERVICE_TYPE" = "openrc" ]; then
|
|
||||||
echo " • OpenRC service configured and running"
|
|
||||||
else
|
|
||||||
echo " • Service configured via crontab"
|
|
||||||
fi
|
|
||||||
echo " • API credentials configured and tested"
|
echo " • API credentials configured and tested"
|
||||||
echo " • WebSocket connection established"
|
echo " • WebSocket connection established"
|
||||||
echo " • Logs directory: /etc/patchmon/logs"
|
echo " • Logs directory: /etc/patchmon/logs"
|
||||||
|
|
||||||
# Check for moved files and show them
|
# Check for moved files and show them
|
||||||
MOVED_FILES=$(ls /etc/patchmon/credentials.yml.backup.* /etc/patchmon/config.yml.backup.* /usr/local/bin/patchmon-agent.backup.* /etc/patchmon/logs/patchmon-agent.log.old.* /usr/local/bin/patchmon-agent.sh.backup.* /etc/patchmon/credentials.backup.* 2>/dev/null || true)
|
MOVED_FILES=$(ls /etc/patchmon/credentials.yml.backup.* /etc/patchmon/config.yml.backup.* /usr/local/bin/patchmon-agent.backup.* /etc/patchmon/logs/patchmon-agent.log.old.* /usr/local/bin/patchmon-agent.sh.backup.* /etc/patchmon/credentials.backup.* 2>/dev/null || true)
|
||||||
if [ -n "$MOVED_FILES" ]; then
|
if [[ -n "$MOVED_FILES" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}📋 Files Moved for Fresh Installation:${NC}"
|
echo -e "${YELLOW}📋 Files Moved for Fresh Installation:${NC}"
|
||||||
echo "$MOVED_FILES" | while read -r moved_file; do
|
echo "$MOVED_FILES" | while read -r moved_file; do
|
||||||
@@ -823,17 +702,8 @@ echo -e "${BLUE}🔧 Management Commands:${NC}"
|
|||||||
echo " • Test connection: /usr/local/bin/patchmon-agent ping"
|
echo " • Test connection: /usr/local/bin/patchmon-agent ping"
|
||||||
echo " • Manual report: /usr/local/bin/patchmon-agent report"
|
echo " • Manual report: /usr/local/bin/patchmon-agent report"
|
||||||
echo " • Check status: /usr/local/bin/patchmon-agent diagnostics"
|
echo " • Check status: /usr/local/bin/patchmon-agent diagnostics"
|
||||||
if [ "$SERVICE_TYPE" = "systemd" ]; then
|
echo " • Service status: systemctl status patchmon-agent"
|
||||||
echo " • Service status: systemctl status patchmon-agent"
|
echo " • Service logs: journalctl -u patchmon-agent -f"
|
||||||
echo " • Service logs: journalctl -u patchmon-agent -f"
|
echo " • Restart service: systemctl restart patchmon-agent"
|
||||||
echo " • Restart service: systemctl restart patchmon-agent"
|
|
||||||
elif [ "$SERVICE_TYPE" = "openrc" ]; then
|
|
||||||
echo " • Service status: rc-service patchmon-agent status"
|
|
||||||
echo " • Service logs: tail -f /etc/patchmon/logs/patchmon-agent.log"
|
|
||||||
echo " • Restart service: rc-service patchmon-agent restart"
|
|
||||||
else
|
|
||||||
echo " • Service logs: tail -f /etc/patchmon/logs/patchmon-agent.log"
|
|
||||||
echo " • Restart service: pkill -f 'patchmon-agent serve' && /usr/local/bin/patchmon-agent serve &"
|
|
||||||
fi
|
|
||||||
echo ""
|
echo ""
|
||||||
success "✅ Your system is now being monitored by PatchMon!"
|
success "✅ Your system is now being monitored by PatchMon!"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"@bull-board/express": "^6.13.1",
|
"@bull-board/express": "^6.13.1",
|
||||||
"@prisma/client": "^6.1.0",
|
"@prisma/client": "^6.1.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^3.0.0",
|
||||||
"bullmq": "^5.61.0",
|
"bullmq": "^5.61.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
"prisma": "^6.1.0"
|
"prisma": "^6.1.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "auto_enrollment_tokens" ADD COLUMN "scopes" JSONB;
|
|
||||||
|
|
||||||
@@ -288,7 +288,6 @@ model auto_enrollment_tokens {
|
|||||||
last_used_at DateTime?
|
last_used_at DateTime?
|
||||||
expires_at DateTime?
|
expires_at DateTime?
|
||||||
metadata Json?
|
metadata Json?
|
||||||
scopes Json?
|
|
||||||
users users? @relation(fields: [created_by_user_id], references: [id], onDelete: SetNull)
|
users users? @relation(fields: [created_by_user_id], references: [id], onDelete: SetNull)
|
||||||
host_groups host_groups? @relation(fields: [default_host_group_id], references: [id], onDelete: SetNull)
|
host_groups host_groups? @relation(fields: [default_host_group_id], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
const { getPrismaClient } = require("../config/prisma");
|
|
||||||
const bcrypt = require("bcryptjs");
|
|
||||||
|
|
||||||
const prisma = getPrismaClient();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware factory to authenticate API tokens using Basic Auth
|
|
||||||
* @param {string} integrationType - The expected integration type (e.g., "api", "gethomepage")
|
|
||||||
* @returns {Function} Express middleware function
|
|
||||||
*/
|
|
||||||
const authenticateApiToken = (integrationType) => {
|
|
||||||
return async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const authHeader = req.headers.authorization;
|
|
||||||
|
|
||||||
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
|
||||||
return res
|
|
||||||
.status(401)
|
|
||||||
.json({ error: "Missing or invalid authorization header" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode base64 credentials
|
|
||||||
const base64Credentials = authHeader.split(" ")[1];
|
|
||||||
const credentials = Buffer.from(base64Credentials, "base64").toString(
|
|
||||||
"ascii",
|
|
||||||
);
|
|
||||||
const [apiKey, apiSecret] = credentials.split(":");
|
|
||||||
|
|
||||||
if (!apiKey || !apiSecret) {
|
|
||||||
return res.status(401).json({ error: "Invalid credentials format" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the token in database
|
|
||||||
const token = await prisma.auto_enrollment_tokens.findUnique({
|
|
||||||
where: { token_key: apiKey },
|
|
||||||
include: {
|
|
||||||
users: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
role: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
console.log(`API key not found: ${apiKey}`);
|
|
||||||
return res.status(401).json({ error: "Invalid API key" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if token is active
|
|
||||||
if (!token.is_active) {
|
|
||||||
return res.status(401).json({ error: "API key is disabled" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if token has expired
|
|
||||||
if (token.expires_at && new Date(token.expires_at) < new Date()) {
|
|
||||||
return res.status(401).json({ error: "API key has expired" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if token is for the expected integration type
|
|
||||||
if (token.metadata?.integration_type !== integrationType) {
|
|
||||||
return res.status(401).json({ error: "Invalid API key type" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the secret
|
|
||||||
const isValidSecret = await bcrypt.compare(apiSecret, token.token_secret);
|
|
||||||
if (!isValidSecret) {
|
|
||||||
return res.status(401).json({ error: "Invalid API secret" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check IP restrictions if any
|
|
||||||
if (token.allowed_ip_ranges && token.allowed_ip_ranges.length > 0) {
|
|
||||||
const clientIp = req.ip || req.connection.remoteAddress;
|
|
||||||
const forwardedFor = req.headers["x-forwarded-for"];
|
|
||||||
const realIp = req.headers["x-real-ip"];
|
|
||||||
|
|
||||||
// Get the actual client IP (considering proxies)
|
|
||||||
const actualClientIp = forwardedFor
|
|
||||||
? forwardedFor.split(",")[0].trim()
|
|
||||||
: realIp || clientIp;
|
|
||||||
|
|
||||||
const isAllowedIp = token.allowed_ip_ranges.some((range) => {
|
|
||||||
// Simple IP range check (can be enhanced for CIDR support)
|
|
||||||
return actualClientIp.startsWith(range) || actualClientIp === range;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isAllowedIp) {
|
|
||||||
console.log(
|
|
||||||
`IP validation failed. Client IP: ${actualClientIp}, Allowed ranges: ${token.allowed_ip_ranges.join(", ")}`,
|
|
||||||
);
|
|
||||||
return res.status(403).json({ error: "IP address not allowed" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update last used timestamp
|
|
||||||
await prisma.auto_enrollment_tokens.update({
|
|
||||||
where: { id: token.id },
|
|
||||||
data: { last_used_at: new Date() },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attach token info to request
|
|
||||||
req.apiToken = token;
|
|
||||||
next();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("API key authentication error:", error);
|
|
||||||
res.status(500).json({ error: "Authentication failed" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = { authenticateApiToken };
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
/**
|
|
||||||
* Middleware factory to validate API token scopes
|
|
||||||
* Only applies to tokens with metadata.integration_type === "api"
|
|
||||||
* @param {string} resource - The resource being accessed (e.g., "host")
|
|
||||||
* @param {string} action - The action being performed (e.g., "get", "put", "patch", "update", "delete")
|
|
||||||
* @returns {Function} Express middleware function
|
|
||||||
*/
|
|
||||||
const requireApiScope = (resource, action) => {
|
|
||||||
return async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const token = req.apiToken;
|
|
||||||
|
|
||||||
// If no token attached, this should have been caught by auth middleware
|
|
||||||
if (!token) {
|
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only validate scopes for API type tokens
|
|
||||||
if (token.metadata?.integration_type !== "api") {
|
|
||||||
// For non-API tokens, skip scope validation
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if token has scopes field
|
|
||||||
if (!token.scopes || typeof token.scopes !== "object") {
|
|
||||||
console.warn(
|
|
||||||
`API token ${token.token_key} missing scopes field for ${resource}:${action}`,
|
|
||||||
);
|
|
||||||
return res.status(403).json({
|
|
||||||
error: "Access denied",
|
|
||||||
message: "This API key does not have the required permissions",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if resource exists in scopes
|
|
||||||
if (!token.scopes[resource]) {
|
|
||||||
console.warn(
|
|
||||||
`API token ${token.token_key} missing resource ${resource} for ${action}`,
|
|
||||||
);
|
|
||||||
return res.status(403).json({
|
|
||||||
error: "Access denied",
|
|
||||||
message: `This API key does not have access to ${resource}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if action exists in resource scopes
|
|
||||||
if (!Array.isArray(token.scopes[resource])) {
|
|
||||||
console.warn(
|
|
||||||
`API token ${token.token_key} has invalid scopes structure for ${resource}`,
|
|
||||||
);
|
|
||||||
return res.status(403).json({
|
|
||||||
error: "Access denied",
|
|
||||||
message: "Invalid API key permissions configuration",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!token.scopes[resource].includes(action)) {
|
|
||||||
console.warn(
|
|
||||||
`API token ${token.token_key} missing action ${action} for resource ${resource}`,
|
|
||||||
);
|
|
||||||
return res.status(403).json({
|
|
||||||
error: "Access denied",
|
|
||||||
message: `This API key does not have permission to ${action} ${resource}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scope validation passed
|
|
||||||
next();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Scope validation error:", error);
|
|
||||||
res.status(500).json({ error: "Scope validation failed" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = { requireApiScope };
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
const express = require("express");
|
|
||||||
const { getPrismaClient } = require("../config/prisma");
|
|
||||||
const { authenticateApiToken } = require("../middleware/apiAuth");
|
|
||||||
const { requireApiScope } = require("../middleware/apiScope");
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
const prisma = getPrismaClient();
|
|
||||||
|
|
||||||
// Helper function to check if a string is a valid UUID
|
|
||||||
const isUUID = (str) => {
|
|
||||||
const uuidRegex =
|
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
||||||
return uuidRegex.test(str);
|
|
||||||
};
|
|
||||||
|
|
||||||
// GET /api/v1/api/hosts - List hosts with IP and groups
|
|
||||||
router.get(
|
|
||||||
"/hosts",
|
|
||||||
authenticateApiToken("api"),
|
|
||||||
requireApiScope("host", "get"),
|
|
||||||
async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { hostgroup } = req.query;
|
|
||||||
|
|
||||||
let whereClause = {};
|
|
||||||
let filterValues = [];
|
|
||||||
|
|
||||||
// Parse hostgroup filter (comma-separated names or UUIDs)
|
|
||||||
if (hostgroup) {
|
|
||||||
filterValues = hostgroup.split(",").map((g) => g.trim());
|
|
||||||
|
|
||||||
// Separate UUIDs from names
|
|
||||||
const uuidFilters = [];
|
|
||||||
const nameFilters = [];
|
|
||||||
|
|
||||||
for (const value of filterValues) {
|
|
||||||
if (isUUID(value)) {
|
|
||||||
uuidFilters.push(value);
|
|
||||||
} else {
|
|
||||||
nameFilters.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find host group IDs from names
|
|
||||||
const groupIds = [...uuidFilters];
|
|
||||||
|
|
||||||
if (nameFilters.length > 0) {
|
|
||||||
const groups = await prisma.host_groups.findMany({
|
|
||||||
where: {
|
|
||||||
name: {
|
|
||||||
in: nameFilters,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add found group IDs
|
|
||||||
groupIds.push(...groups.map((g) => g.id));
|
|
||||||
|
|
||||||
// Check if any name filters didn't match
|
|
||||||
const foundNames = groups.map((g) => g.name);
|
|
||||||
const notFoundNames = nameFilters.filter(
|
|
||||||
(name) => !foundNames.includes(name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (notFoundNames.length > 0) {
|
|
||||||
console.warn(`Host groups not found: ${notFoundNames.join(", ")}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter hosts by group memberships
|
|
||||||
if (groupIds.length > 0) {
|
|
||||||
whereClause = {
|
|
||||||
host_group_memberships: {
|
|
||||||
some: {
|
|
||||||
host_group_id: {
|
|
||||||
in: groupIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// No valid groups found, return empty result
|
|
||||||
return res.json({
|
|
||||||
hosts: [],
|
|
||||||
total: 0,
|
|
||||||
filtered_by_groups: filterValues,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query hosts with groups
|
|
||||||
const hosts = await prisma.hosts.findMany({
|
|
||||||
where: whereClause,
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
friendly_name: true,
|
|
||||||
hostname: true,
|
|
||||||
ip: true,
|
|
||||||
host_group_memberships: {
|
|
||||||
include: {
|
|
||||||
host_groups: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
friendly_name: "asc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Format response
|
|
||||||
const formattedHosts = hosts.map((host) => ({
|
|
||||||
id: host.id,
|
|
||||||
friendly_name: host.friendly_name,
|
|
||||||
hostname: host.hostname,
|
|
||||||
ip: host.ip,
|
|
||||||
host_groups: host.host_group_memberships.map((membership) => ({
|
|
||||||
id: membership.host_groups.id,
|
|
||||||
name: membership.host_groups.name,
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
hosts: formattedHosts,
|
|
||||||
total: formattedHosts.length,
|
|
||||||
filtered_by_groups: filterValues.length > 0 ? filterValues : undefined,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching hosts:", error);
|
|
||||||
res.status(500).json({ error: "Failed to fetch hosts" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -125,10 +125,6 @@ router.post(
|
|||||||
.optional({ nullable: true, checkFalsy: true })
|
.optional({ nullable: true, checkFalsy: true })
|
||||||
.isISO8601()
|
.isISO8601()
|
||||||
.withMessage("Invalid date format"),
|
.withMessage("Invalid date format"),
|
||||||
body("scopes")
|
|
||||||
.optional()
|
|
||||||
.isObject()
|
|
||||||
.withMessage("Scopes must be an object"),
|
|
||||||
],
|
],
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -144,7 +140,6 @@ router.post(
|
|||||||
default_host_group_id,
|
default_host_group_id,
|
||||||
expires_at,
|
expires_at,
|
||||||
metadata = {},
|
metadata = {},
|
||||||
scopes,
|
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// Validate host group if provided
|
// Validate host group if provided
|
||||||
@@ -158,32 +153,6 @@ router.post(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate scopes for API tokens
|
|
||||||
if (metadata.integration_type === "api" && scopes) {
|
|
||||||
// Validate scopes structure
|
|
||||||
if (typeof scopes !== "object" || scopes === null) {
|
|
||||||
return res.status(400).json({ error: "Scopes must be an object" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate each resource in scopes
|
|
||||||
for (const [resource, actions] of Object.entries(scopes)) {
|
|
||||||
if (!Array.isArray(actions)) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: `Scopes for resource "${resource}" must be an array of actions`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate action names
|
|
||||||
for (const action of actions) {
|
|
||||||
if (typeof action !== "string") {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: `All actions in scopes must be strings`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { token_key, token_secret } = generate_auto_enrollment_token();
|
const { token_key, token_secret } = generate_auto_enrollment_token();
|
||||||
const hashed_secret = await bcrypt.hash(token_secret, 10);
|
const hashed_secret = await bcrypt.hash(token_secret, 10);
|
||||||
|
|
||||||
@@ -199,7 +168,6 @@ router.post(
|
|||||||
default_host_group_id: default_host_group_id || null,
|
default_host_group_id: default_host_group_id || null,
|
||||||
expires_at: expires_at ? new Date(expires_at) : null,
|
expires_at: expires_at ? new Date(expires_at) : null,
|
||||||
metadata: { integration_type: "proxmox-lxc", ...metadata },
|
metadata: { integration_type: "proxmox-lxc", ...metadata },
|
||||||
scopes: metadata.integration_type === "api" ? scopes || null : null,
|
|
||||||
updated_at: new Date(),
|
updated_at: new Date(),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
@@ -233,7 +201,6 @@ router.post(
|
|||||||
default_host_group: token.host_groups,
|
default_host_group: token.host_groups,
|
||||||
created_by: token.users,
|
created_by: token.users,
|
||||||
expires_at: token.expires_at,
|
expires_at: token.expires_at,
|
||||||
scopes: token.scopes,
|
|
||||||
},
|
},
|
||||||
warning: "⚠️ Save the token_secret now - it cannot be retrieved later!",
|
warning: "⚠️ Save the token_secret now - it cannot be retrieved later!",
|
||||||
});
|
});
|
||||||
@@ -265,7 +232,6 @@ router.get(
|
|||||||
created_at: true,
|
created_at: true,
|
||||||
default_host_group_id: true,
|
default_host_group_id: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
scopes: true,
|
|
||||||
host_groups: {
|
host_groups: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -348,10 +314,6 @@ router.patch(
|
|||||||
body("max_hosts_per_day").optional().isInt({ min: 1, max: 1000 }),
|
body("max_hosts_per_day").optional().isInt({ min: 1, max: 1000 }),
|
||||||
body("allowed_ip_ranges").optional().isArray(),
|
body("allowed_ip_ranges").optional().isArray(),
|
||||||
body("expires_at").optional().isISO8601(),
|
body("expires_at").optional().isISO8601(),
|
||||||
body("scopes")
|
|
||||||
.optional()
|
|
||||||
.isObject()
|
|
||||||
.withMessage("Scopes must be an object"),
|
|
||||||
],
|
],
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -361,16 +323,6 @@ router.patch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { tokenId } = req.params;
|
const { tokenId } = req.params;
|
||||||
|
|
||||||
// First, get the existing token to check its integration type
|
|
||||||
const existing_token = await prisma.auto_enrollment_tokens.findUnique({
|
|
||||||
where: { id: tokenId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!existing_token) {
|
|
||||||
return res.status(404).json({ error: "Token not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const update_data = { updated_at: new Date() };
|
const update_data = { updated_at: new Date() };
|
||||||
|
|
||||||
if (req.body.is_active !== undefined)
|
if (req.body.is_active !== undefined)
|
||||||
@@ -382,41 +334,6 @@ router.patch(
|
|||||||
if (req.body.expires_at !== undefined)
|
if (req.body.expires_at !== undefined)
|
||||||
update_data.expires_at = new Date(req.body.expires_at);
|
update_data.expires_at = new Date(req.body.expires_at);
|
||||||
|
|
||||||
// Handle scopes updates for API tokens only
|
|
||||||
if (req.body.scopes !== undefined) {
|
|
||||||
if (existing_token.metadata?.integration_type === "api") {
|
|
||||||
// Validate scopes structure
|
|
||||||
const scopes = req.body.scopes;
|
|
||||||
if (typeof scopes !== "object" || scopes === null) {
|
|
||||||
return res.status(400).json({ error: "Scopes must be an object" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate each resource in scopes
|
|
||||||
for (const [resource, actions] of Object.entries(scopes)) {
|
|
||||||
if (!Array.isArray(actions)) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: `Scopes for resource "${resource}" must be an array of actions`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate action names
|
|
||||||
for (const action of actions) {
|
|
||||||
if (typeof action !== "string") {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: `All actions in scopes must be strings`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update_data.scopes = scopes;
|
|
||||||
} else {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: "Scopes can only be updated for API integration tokens",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await prisma.auto_enrollment_tokens.update({
|
const token = await prisma.auto_enrollment_tokens.update({
|
||||||
where: { id: tokenId },
|
where: { id: tokenId },
|
||||||
data: update_data,
|
data: update_data,
|
||||||
|
|||||||
@@ -1,12 +1,113 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const { getPrismaClient } = require("../config/prisma");
|
const { getPrismaClient } = require("../config/prisma");
|
||||||
const { authenticateApiToken } = require("../middleware/apiAuth");
|
const bcrypt = require("bcryptjs");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const prisma = getPrismaClient();
|
const prisma = getPrismaClient();
|
||||||
|
|
||||||
|
// Middleware to authenticate API key
|
||||||
|
const authenticateApiKey = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
||||||
|
return res
|
||||||
|
.status(401)
|
||||||
|
.json({ error: "Missing or invalid authorization header" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 credentials
|
||||||
|
const base64Credentials = authHeader.split(" ")[1];
|
||||||
|
const credentials = Buffer.from(base64Credentials, "base64").toString(
|
||||||
|
"ascii",
|
||||||
|
);
|
||||||
|
const [apiKey, apiSecret] = credentials.split(":");
|
||||||
|
|
||||||
|
if (!apiKey || !apiSecret) {
|
||||||
|
return res.status(401).json({ error: "Invalid credentials format" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the token in database
|
||||||
|
const token = await prisma.auto_enrollment_tokens.findUnique({
|
||||||
|
where: { token_key: apiKey },
|
||||||
|
include: {
|
||||||
|
users: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.log(`API key not found: ${apiKey}`);
|
||||||
|
return res.status(401).json({ error: "Invalid API key" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is active
|
||||||
|
if (!token.is_active) {
|
||||||
|
return res.status(401).json({ error: "API key is disabled" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token has expired
|
||||||
|
if (token.expires_at && new Date(token.expires_at) < new Date()) {
|
||||||
|
return res.status(401).json({ error: "API key has expired" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is for gethomepage integration
|
||||||
|
if (token.metadata?.integration_type !== "gethomepage") {
|
||||||
|
return res.status(401).json({ error: "Invalid API key type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the secret
|
||||||
|
const isValidSecret = await bcrypt.compare(apiSecret, token.token_secret);
|
||||||
|
if (!isValidSecret) {
|
||||||
|
return res.status(401).json({ error: "Invalid API secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check IP restrictions if any
|
||||||
|
if (token.allowed_ip_ranges && token.allowed_ip_ranges.length > 0) {
|
||||||
|
const clientIp = req.ip || req.connection.remoteAddress;
|
||||||
|
const forwardedFor = req.headers["x-forwarded-for"];
|
||||||
|
const realIp = req.headers["x-real-ip"];
|
||||||
|
|
||||||
|
// Get the actual client IP (considering proxies)
|
||||||
|
const actualClientIp = forwardedFor
|
||||||
|
? forwardedFor.split(",")[0].trim()
|
||||||
|
: realIp || clientIp;
|
||||||
|
|
||||||
|
const isAllowedIp = token.allowed_ip_ranges.some((range) => {
|
||||||
|
// Simple IP range check (can be enhanced for CIDR support)
|
||||||
|
return actualClientIp.startsWith(range) || actualClientIp === range;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isAllowedIp) {
|
||||||
|
console.log(
|
||||||
|
`IP validation failed. Client IP: ${actualClientIp}, Allowed ranges: ${token.allowed_ip_ranges.join(", ")}`,
|
||||||
|
);
|
||||||
|
return res.status(403).json({ error: "IP address not allowed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last used timestamp
|
||||||
|
await prisma.auto_enrollment_tokens.update({
|
||||||
|
where: { id: token.id },
|
||||||
|
data: { last_used_at: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach token info to request
|
||||||
|
req.apiToken = token;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API key authentication error:", error);
|
||||||
|
res.status(500).json({ error: "Authentication failed" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Get homepage widget statistics
|
// Get homepage widget statistics
|
||||||
router.get("/stats", authenticateApiToken("gethomepage"), async (_req, res) => {
|
router.get("/stats", authenticateApiKey, async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
// Get total hosts count
|
// Get total hosts count
|
||||||
const totalHosts = await prisma.hosts.count({
|
const totalHosts = await prisma.hosts.count({
|
||||||
@@ -134,7 +235,7 @@ router.get("/stats", authenticateApiToken("gethomepage"), async (_req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Health check endpoint for the API
|
// Health check endpoint for the API
|
||||||
router.get("/health", authenticateApiToken("gethomepage"), async (req, res) => {
|
router.get("/health", authenticateApiKey, async (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: "ok",
|
status: "ok",
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ const wsRoutes = require("./routes/wsRoutes");
|
|||||||
const agentVersionRoutes = require("./routes/agentVersionRoutes");
|
const agentVersionRoutes = require("./routes/agentVersionRoutes");
|
||||||
const metricsRoutes = require("./routes/metricsRoutes");
|
const metricsRoutes = require("./routes/metricsRoutes");
|
||||||
const userPreferencesRoutes = require("./routes/userPreferencesRoutes");
|
const userPreferencesRoutes = require("./routes/userPreferencesRoutes");
|
||||||
const apiHostsRoutes = require("./routes/apiHostsRoutes");
|
|
||||||
const { initSettings } = require("./services/settingsService");
|
const { initSettings } = require("./services/settingsService");
|
||||||
const { queueManager } = require("./services/automation");
|
const { queueManager } = require("./services/automation");
|
||||||
const { authenticateToken, requireAdmin } = require("./middleware/auth");
|
const { authenticateToken, requireAdmin } = require("./middleware/auth");
|
||||||
@@ -481,7 +480,6 @@ app.use(`/api/${apiVersion}/ws`, wsRoutes);
|
|||||||
app.use(`/api/${apiVersion}/agent`, agentVersionRoutes);
|
app.use(`/api/${apiVersion}/agent`, agentVersionRoutes);
|
||||||
app.use(`/api/${apiVersion}/metrics`, metricsRoutes);
|
app.use(`/api/${apiVersion}/metrics`, metricsRoutes);
|
||||||
app.use(`/api/${apiVersion}/user/preferences`, userPreferencesRoutes);
|
app.use(`/api/${apiVersion}/user/preferences`, userPreferencesRoutes);
|
||||||
app.use(`/api/${apiVersion}/api`, authLimiter, apiHostsRoutes);
|
|
||||||
|
|
||||||
// Bull Board - will be populated after queue manager initializes
|
// Bull Board - will be populated after queue manager initializes
|
||||||
let bullBoardRouter = null;
|
let bullBoardRouter = null;
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ const Integrations = () => {
|
|||||||
const [host_groups, setHostGroups] = useState([]);
|
const [host_groups, setHostGroups] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [show_create_modal, setShowCreateModal] = useState(false);
|
const [show_create_modal, setShowCreateModal] = useState(false);
|
||||||
const [show_edit_modal, setShowEditModal] = useState(false);
|
|
||||||
const [edit_token, setEditToken] = useState(null);
|
|
||||||
const [new_token, setNewToken] = useState(null);
|
const [new_token, setNewToken] = useState(null);
|
||||||
const [show_secret, setShowSecret] = useState(false);
|
const [show_secret, setShowSecret] = useState(false);
|
||||||
const [server_url, setServerUrl] = useState("");
|
const [server_url, setServerUrl] = useState("");
|
||||||
@@ -42,9 +40,6 @@ const Integrations = () => {
|
|||||||
default_host_group_id: "",
|
default_host_group_id: "",
|
||||||
allowed_ip_ranges: "",
|
allowed_ip_ranges: "",
|
||||||
expires_at: "",
|
expires_at: "",
|
||||||
scopes: {
|
|
||||||
host: [],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [copy_success, setCopySuccess] = useState({});
|
const [copy_success, setCopySuccess] = useState({});
|
||||||
@@ -59,25 +54,6 @@ const Integrations = () => {
|
|||||||
setActiveTab(tabName);
|
setActiveTab(tabName);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggle_scope_action = (resource, action) => {
|
|
||||||
setFormData((prev) => {
|
|
||||||
const current_scopes = prev.scopes || { [resource]: [] };
|
|
||||||
const resource_scopes = current_scopes[resource] || [];
|
|
||||||
|
|
||||||
const updated_scopes = resource_scopes.includes(action)
|
|
||||||
? resource_scopes.filter((a) => a !== action)
|
|
||||||
: [...resource_scopes, action];
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
scopes: {
|
|
||||||
...current_scopes,
|
|
||||||
[resource]: updated_scopes,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Only run on mount
|
// biome-ignore lint/correctness/useExhaustiveDependencies: Only run on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load_tokens();
|
load_tokens();
|
||||||
@@ -120,14 +96,6 @@ const Integrations = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Determine integration type based on active tab
|
|
||||||
let integration_type = "proxmox-lxc";
|
|
||||||
if (activeTab === "gethomepage") {
|
|
||||||
integration_type = "gethomepage";
|
|
||||||
} else if (activeTab === "api") {
|
|
||||||
integration_type = "api";
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
token_name: form_data.token_name,
|
token_name: form_data.token_name,
|
||||||
max_hosts_per_day: Number.parseInt(form_data.max_hosts_per_day, 10),
|
max_hosts_per_day: Number.parseInt(form_data.max_hosts_per_day, 10),
|
||||||
@@ -135,7 +103,8 @@ const Integrations = () => {
|
|||||||
? form_data.allowed_ip_ranges.split(",").map((ip) => ip.trim())
|
? form_data.allowed_ip_ranges.split(",").map((ip) => ip.trim())
|
||||||
: [],
|
: [],
|
||||||
metadata: {
|
metadata: {
|
||||||
integration_type: integration_type,
|
integration_type:
|
||||||
|
activeTab === "gethomepage" ? "gethomepage" : "proxmox-lxc",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,11 +116,6 @@ const Integrations = () => {
|
|||||||
data.expires_at = form_data.expires_at;
|
data.expires_at = form_data.expires_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add scopes for API credentials
|
|
||||||
if (activeTab === "api" && form_data.scopes) {
|
|
||||||
data.scopes = form_data.scopes;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.post("/auto-enrollment/tokens", data);
|
const response = await api.post("/auto-enrollment/tokens", data);
|
||||||
setNewToken(response.data.token);
|
setNewToken(response.data.token);
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
@@ -164,9 +128,6 @@ const Integrations = () => {
|
|||||||
default_host_group_id: "",
|
default_host_group_id: "",
|
||||||
allowed_ip_ranges: "",
|
allowed_ip_ranges: "",
|
||||||
expires_at: "",
|
expires_at: "",
|
||||||
scopes: {
|
|
||||||
host: [],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create token:", error);
|
console.error("Failed to create token:", error);
|
||||||
@@ -207,69 +168,6 @@ const Integrations = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const open_edit_modal = (token) => {
|
|
||||||
setEditToken(token);
|
|
||||||
setFormData({
|
|
||||||
token_name: token.token_name,
|
|
||||||
max_hosts_per_day: token.max_hosts_per_day || 100,
|
|
||||||
default_host_group_id: token.default_host_group_id || "",
|
|
||||||
allowed_ip_ranges: token.allowed_ip_ranges?.join(", ") || "",
|
|
||||||
expires_at: token.expires_at
|
|
||||||
? new Date(token.expires_at).toISOString().slice(0, 16)
|
|
||||||
: "",
|
|
||||||
scopes: token.scopes || { host: [] },
|
|
||||||
});
|
|
||||||
setShowEditModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const update_token = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = {
|
|
||||||
allowed_ip_ranges: form_data.allowed_ip_ranges
|
|
||||||
? form_data.allowed_ip_ranges.split(",").map((ip) => ip.trim())
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add expiration if provided
|
|
||||||
if (form_data.expires_at) {
|
|
||||||
data.expires_at = form_data.expires_at;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add scopes for API credentials
|
|
||||||
if (
|
|
||||||
edit_token?.metadata?.integration_type === "api" &&
|
|
||||||
form_data.scopes
|
|
||||||
) {
|
|
||||||
data.scopes = form_data.scopes;
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.patch(`/auto-enrollment/tokens/${edit_token.id}`, data);
|
|
||||||
setShowEditModal(false);
|
|
||||||
setEditToken(null);
|
|
||||||
load_tokens();
|
|
||||||
|
|
||||||
// Reset form
|
|
||||||
setFormData({
|
|
||||||
token_name: "",
|
|
||||||
max_hosts_per_day: 100,
|
|
||||||
default_host_group_id: "",
|
|
||||||
allowed_ip_ranges: "",
|
|
||||||
expires_at: "",
|
|
||||||
scopes: {
|
|
||||||
host: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update token:", error);
|
|
||||||
const error_message = error.response?.data?.errors
|
|
||||||
? error.response.data.errors.map((e) => e.msg).join(", ")
|
|
||||||
: error.response?.data?.error || "Failed to update token";
|
|
||||||
alert(error_message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copy_to_clipboard = async (text, key) => {
|
const copy_to_clipboard = async (text, key) => {
|
||||||
// Check if Clipboard API is available
|
// Check if Clipboard API is available
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
@@ -358,17 +256,6 @@ const Integrations = () => {
|
|||||||
>
|
>
|
||||||
GetHomepage
|
GetHomepage
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleTabChange("api")}
|
|
||||||
className={`px-6 py-3 text-sm font-medium ${
|
|
||||||
activeTab === "api"
|
|
||||||
? "text-primary-600 dark:text-primary-400 border-b-2 border-primary-500 bg-primary-50 dark:bg-primary-900/20"
|
|
||||||
: "text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300 hover:bg-secondary-50 dark:hover:bg-secondary-700/50"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
API
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleTabChange("docker")}
|
onClick={() => handleTabChange("docker")}
|
||||||
@@ -849,214 +736,6 @@ const Integrations = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* API Tab */}
|
|
||||||
{activeTab === "api" && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Header with New Credential Button */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-primary-100 dark:bg-primary-900 rounded-lg flex items-center justify-center">
|
|
||||||
<Server className="h-5 w-5 text-primary-600 dark:text-primary-400" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
|
|
||||||
API Credentials
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-secondary-600 dark:text-secondary-400">
|
|
||||||
Manage API credentials for programmatic access to
|
|
||||||
PatchMon data
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowCreateModal(true)}
|
|
||||||
className="btn-primary flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
New Credential
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* API Credentials List */}
|
|
||||||
{loading ? (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" />
|
|
||||||
</div>
|
|
||||||
) : tokens.filter(
|
|
||||||
(token) => token.metadata?.integration_type === "api",
|
|
||||||
).length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-secondary-600 dark:text-secondary-400">
|
|
||||||
<p>No API credentials created yet.</p>
|
|
||||||
<p className="text-sm mt-2">
|
|
||||||
Create a credential to enable programmatic access to
|
|
||||||
PatchMon.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{tokens
|
|
||||||
.filter(
|
|
||||||
(token) => token.metadata?.integration_type === "api",
|
|
||||||
)
|
|
||||||
.map((token) => (
|
|
||||||
<div
|
|
||||||
key={token.id}
|
|
||||||
className="border border-secondary-200 dark:border-secondary-600 rounded-lg p-4 hover:border-primary-300 dark:hover:border-primary-700 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<h4 className="font-medium text-secondary-900 dark:text-white">
|
|
||||||
{token.token_name}
|
|
||||||
</h4>
|
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
|
||||||
API
|
|
||||||
</span>
|
|
||||||
{token.is_active ? (
|
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
|
||||||
Active
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-secondary-100 text-secondary-800 dark:bg-secondary-700 dark:text-secondary-200">
|
|
||||||
Inactive
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 space-y-1 text-sm text-secondary-600 dark:text-secondary-400">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="font-mono text-xs bg-secondary-100 dark:bg-secondary-700 px-2 py-1 rounded">
|
|
||||||
{token.token_key}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
copy_to_clipboard(
|
|
||||||
token.token_key,
|
|
||||||
`key-${token.id}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="text-primary-600 hover:text-primary-700 dark:text-primary-400"
|
|
||||||
>
|
|
||||||
{copy_success[`key-${token.id}`] ? (
|
|
||||||
<CheckCircle className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{token.scopes && (
|
|
||||||
<p>
|
|
||||||
Scopes:{" "}
|
|
||||||
{Object.entries(token.scopes)
|
|
||||||
.map(
|
|
||||||
([resource, actions]) =>
|
|
||||||
`${resource}: ${Array.isArray(actions) ? actions.join(", ") : actions}`,
|
|
||||||
)
|
|
||||||
.join(" | ")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{token.allowed_ip_ranges?.length > 0 && (
|
|
||||||
<p>
|
|
||||||
Allowed IPs:{" "}
|
|
||||||
{token.allowed_ip_ranges.join(", ")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p>Created: {format_date(token.created_at)}</p>
|
|
||||||
{token.last_used_at && (
|
|
||||||
<p>
|
|
||||||
Last Used: {format_date(token.last_used_at)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{token.expires_at && (
|
|
||||||
<p>
|
|
||||||
Expires: {format_date(token.expires_at)}
|
|
||||||
{new Date(token.expires_at) <
|
|
||||||
new Date() && (
|
|
||||||
<span className="ml-2 text-red-600 dark:text-red-400">
|
|
||||||
(Expired)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => open_edit_modal(token)}
|
|
||||||
className="px-3 py-1 text-sm rounded bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-300"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
toggle_token_active(token.id, token.is_active)
|
|
||||||
}
|
|
||||||
className={`px-3 py-1 text-sm rounded ${
|
|
||||||
token.is_active
|
|
||||||
? "bg-secondary-100 text-secondary-700 hover:bg-secondary-200 dark:bg-secondary-700 dark:text-secondary-300"
|
|
||||||
: "bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900 dark:text-green-300"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{token.is_active ? "Disable" : "Enable"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
delete_token(token.id, token.token_name)
|
|
||||||
}
|
|
||||||
className="text-red-600 hover:text-red-800 dark:text-red-400 p-2"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Documentation Section */}
|
|
||||||
<div className="bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-semibold text-primary-900 dark:text-primary-200 mb-4">
|
|
||||||
Using API Credentials
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4 text-sm text-primary-800 dark:text-primary-300">
|
|
||||||
<p>
|
|
||||||
API credentials allow you to programmatically access
|
|
||||||
PatchMon data using Basic Authentication.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold mb-2">
|
|
||||||
Example cURL Request:
|
|
||||||
</p>
|
|
||||||
<div className="bg-primary-100 dark:bg-primary-900/40 p-3 rounded border border-primary-200 dark:border-primary-700 font-mono text-xs overflow-x-auto">
|
|
||||||
curl -u "YOUR_API_KEY:YOUR_API_SECRET" \<br />
|
|
||||||
{server_url}/api/v1/api/hosts
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold mb-2">
|
|
||||||
Query Hosts by Group:
|
|
||||||
</p>
|
|
||||||
<div className="bg-primary-100 dark:bg-primary-900/40 p-3 rounded border border-primary-200 dark:border-primary-700 font-mono text-xs overflow-x-auto">
|
|
||||||
curl -u "YOUR_API_KEY:YOUR_API_SECRET" \<br />
|
|
||||||
"{server_url}
|
|
||||||
/api/v1/api/hosts?hostgroup=Production,Development"
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs">
|
|
||||||
<strong>💡 Tip:</strong> You can filter by host group
|
|
||||||
names or UUIDs. Multiple groups can be specified as a
|
|
||||||
comma-separated list.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Docker Tab */}
|
{/* Docker Tab */}
|
||||||
{activeTab === "docker" && (
|
{activeTab === "docker" && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -1206,9 +885,7 @@ const Integrations = () => {
|
|||||||
<h2 className="text-xl font-bold text-secondary-900 dark:text-white">
|
<h2 className="text-xl font-bold text-secondary-900 dark:text-white">
|
||||||
{activeTab === "gethomepage"
|
{activeTab === "gethomepage"
|
||||||
? "Create GetHomepage API Key"
|
? "Create GetHomepage API Key"
|
||||||
: activeTab === "api"
|
: "Create Auto-Enrollment Token"}
|
||||||
? "Create API Credential"
|
|
||||||
: "Create Auto-Enrollment Token"}
|
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1234,9 +911,7 @@ const Integrations = () => {
|
|||||||
placeholder={
|
placeholder={
|
||||||
activeTab === "gethomepage"
|
activeTab === "gethomepage"
|
||||||
? "e.g., GetHomepage Widget"
|
? "e.g., GetHomepage Widget"
|
||||||
: activeTab === "api"
|
: "e.g., Proxmox Production"
|
||||||
? "e.g., Ansible Inventory"
|
|
||||||
: "e.g., Proxmox Production"
|
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
|
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
|
||||||
/>
|
/>
|
||||||
@@ -1295,56 +970,6 @@ const Integrations = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "api" && (
|
|
||||||
<div className="block">
|
|
||||||
<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
|
||||||
Scopes *
|
|
||||||
</span>
|
|
||||||
<div className="border border-secondary-300 dark:border-secondary-600 rounded-md p-4 bg-secondary-50 dark:bg-secondary-900">
|
|
||||||
<div className="mb-3">
|
|
||||||
<p className="text-xs font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
|
|
||||||
Host Permissions
|
|
||||||
</p>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{["get", "put", "patch", "update", "delete"].map(
|
|
||||||
(action) => (
|
|
||||||
<label
|
|
||||||
key={action}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={
|
|
||||||
form_data.scopes?.host?.includes(action) ||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
onChange={() =>
|
|
||||||
toggle_scope_action("host", action)
|
|
||||||
}
|
|
||||||
className="rounded border-secondary-300 dark:border-secondary-600 text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-400"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-secondary-700 dark:text-secondary-300 uppercase">
|
|
||||||
{action}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
{action === "get" && "- Read host data"}
|
|
||||||
{action === "put" && "- Replace host data"}
|
|
||||||
{action === "patch" && "- Update host data"}
|
|
||||||
{action === "update" && "- Modify host data"}
|
|
||||||
{action === "delete" && "- Delete hosts"}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Select the permissions this API credential should have
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
||||||
Allowed IP Addresses (Optional)
|
Allowed IP Addresses (Optional)
|
||||||
@@ -1413,9 +1038,7 @@ const Integrations = () => {
|
|||||||
<h2 className="text-lg font-bold text-secondary-900 dark:text-white">
|
<h2 className="text-lg font-bold text-secondary-900 dark:text-white">
|
||||||
{activeTab === "gethomepage"
|
{activeTab === "gethomepage"
|
||||||
? "API Key Created Successfully"
|
? "API Key Created Successfully"
|
||||||
: activeTab === "api"
|
: "Token Created Successfully"}
|
||||||
? "API Credential Created Successfully"
|
|
||||||
: "Token Created Successfully"}
|
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -1538,103 +1161,6 @@ const Integrations = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeTab === "api" && new_token.scopes && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<div className="block text-xs font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
|
||||||
Granted Scopes
|
|
||||||
</div>
|
|
||||||
<div className="bg-secondary-50 dark:bg-secondary-900 border border-secondary-300 dark:border-secondary-600 rounded-md p-3">
|
|
||||||
{Object.entries(new_token.scopes).map(
|
|
||||||
([resource, actions]) => (
|
|
||||||
<div key={resource} className="text-sm">
|
|
||||||
<span className="font-semibold text-secondary-800 dark:text-secondary-200 capitalize">
|
|
||||||
{resource}:
|
|
||||||
</span>{" "}
|
|
||||||
<span className="text-secondary-600 dark:text-secondary-400">
|
|
||||||
{Array.isArray(actions)
|
|
||||||
? actions.join(", ").toUpperCase()
|
|
||||||
: actions}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === "api" && (
|
|
||||||
<div className="mt-6">
|
|
||||||
<div className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
|
||||||
Usage Examples
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-secondary-600 dark:text-secondary-400 mb-2">
|
|
||||||
Basic cURL request:
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={`curl -u "${new_token.token_key}:${new_token.token_secret}" ${server_url}/api/v1/api/hosts`}
|
|
||||||
readOnly
|
|
||||||
className="flex-1 px-3 py-2 text-xs border border-secondary-300 dark:border-secondary-600 rounded-md bg-secondary-50 dark:bg-secondary-900 text-secondary-900 dark:text-white font-mono"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
copy_to_clipboard(
|
|
||||||
`curl -u "${new_token.token_key}:${new_token.token_secret}" ${server_url}/api/v1/api/hosts`,
|
|
||||||
"api-curl-basic",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="btn-primary p-2"
|
|
||||||
title="Copy cURL command"
|
|
||||||
>
|
|
||||||
{copy_success["api-curl-basic"] ? (
|
|
||||||
<CheckCircle className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-secondary-600 dark:text-secondary-400 mb-2">
|
|
||||||
Filter by host group:
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={`curl -u "${new_token.token_key}:${new_token.token_secret}" "${server_url}/api/v1/api/hosts?hostgroup=Production"`}
|
|
||||||
readOnly
|
|
||||||
className="flex-1 px-3 py-2 text-xs border border-secondary-300 dark:border-secondary-600 rounded-md bg-secondary-50 dark:bg-secondary-900 text-secondary-900 dark:text-white font-mono"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
copy_to_clipboard(
|
|
||||||
`curl -u "${new_token.token_key}:${new_token.token_secret}" "${server_url}/api/v1/api/hosts?hostgroup=Production"`,
|
|
||||||
"api-curl-filter",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="btn-primary p-2"
|
|
||||||
title="Copy cURL command"
|
|
||||||
>
|
|
||||||
{copy_success["api-curl-filter"] ? (
|
|
||||||
<CheckCircle className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-secondary-500 dark:text-secondary-400 mt-3">
|
|
||||||
💡 Replace "Production" with your host group name or UUID
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === "proxmox" && (
|
{activeTab === "proxmox" && (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<div className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
<div className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
||||||
@@ -1845,154 +1371,6 @@ const Integrations = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Edit API Credential Modal */}
|
|
||||||
{show_edit_modal && edit_token && (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className="bg-white dark:bg-secondary-800 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
|
||||||
<h2 className="text-xl font-bold text-secondary-900 dark:text-white">
|
|
||||||
Edit API Credential
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setShowEditModal(false);
|
|
||||||
setEditToken(null);
|
|
||||||
}}
|
|
||||||
className="text-secondary-400 hover:text-secondary-600 dark:hover:text-secondary-200"
|
|
||||||
>
|
|
||||||
<X className="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={update_token} className="space-y-4">
|
|
||||||
<div className="block">
|
|
||||||
<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
|
||||||
Token Name
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={form_data.token_name}
|
|
||||||
readOnly
|
|
||||||
disabled
|
|
||||||
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-secondary-100 dark:bg-secondary-900 text-secondary-500 dark:text-secondary-400"
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Token name cannot be changed
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{edit_token?.metadata?.integration_type === "api" && (
|
|
||||||
<div className="block">
|
|
||||||
<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
|
|
||||||
Scopes
|
|
||||||
</span>
|
|
||||||
<div className="border border-secondary-300 dark:border-secondary-600 rounded-md p-4 bg-secondary-50 dark:bg-secondary-900">
|
|
||||||
<div className="mb-3">
|
|
||||||
<p className="text-xs font-semibold text-secondary-700 dark:text-secondary-300 mb-2">
|
|
||||||
Host Permissions
|
|
||||||
</p>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{["get", "put", "patch", "update", "delete"].map(
|
|
||||||
(action) => (
|
|
||||||
<label
|
|
||||||
key={action}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={
|
|
||||||
form_data.scopes?.host?.includes(action) ||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
onChange={() =>
|
|
||||||
toggle_scope_action("host", action)
|
|
||||||
}
|
|
||||||
className="rounded border-secondary-300 dark:border-secondary-600 text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-400"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-secondary-700 dark:text-secondary-300 uppercase">
|
|
||||||
{action}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
{action === "get" && "- Read host data"}
|
|
||||||
{action === "put" && "- Replace host data"}
|
|
||||||
{action === "patch" && "- Update host data"}
|
|
||||||
{action === "update" && "- Modify host data"}
|
|
||||||
{action === "delete" && "- Delete hosts"}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Update the permissions for this API credential
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<label className="block">
|
|
||||||
<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
|
||||||
Allowed IP Addresses (Optional)
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={form_data.allowed_ip_ranges}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({
|
|
||||||
...form_data,
|
|
||||||
allowed_ip_ranges: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="e.g., 192.168.1.100, 10.0.0.50"
|
|
||||||
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
|
||||||
Comma-separated list of IP addresses allowed to use this
|
|
||||||
token
|
|
||||||
</p>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="block">
|
|
||||||
<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
|
|
||||||
Expiration Date (Optional)
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="datetime-local"
|
|
||||||
value={form_data.expires_at}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({ ...form_data, expires_at: e.target.value })
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="flex gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex-1 btn-primary py-2 px-4 rounded-md"
|
|
||||||
>
|
|
||||||
Update Credential
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setShowEditModal(false);
|
|
||||||
setEditToken(null);
|
|
||||||
}}
|
|
||||||
className="flex-1 bg-secondary-100 dark:bg-secondary-700 text-secondary-700 dark:text-secondary-300 py-2 px-4 rounded-md hover:bg-secondary-200 dark:hover:bg-secondary-600"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
171
package-lock.json
generated
171
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "patchmon",
|
"name": "patchmon",
|
||||||
"version": "1.3.2",
|
"version": "1.3.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "patchmon",
|
"name": "patchmon",
|
||||||
"version": "1.3.2",
|
"version": "1.3.3",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"backend",
|
"backend",
|
||||||
@@ -23,14 +23,14 @@
|
|||||||
},
|
},
|
||||||
"backend": {
|
"backend": {
|
||||||
"name": "patchmon-backend",
|
"name": "patchmon-backend",
|
||||||
"version": "1.3.2",
|
"version": "1.3.3",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^6.13.1",
|
"@bull-board/api": "^6.13.1",
|
||||||
"@bull-board/express": "^6.13.1",
|
"@bull-board/express": "^6.13.1",
|
||||||
"@prisma/client": "^6.1.0",
|
"@prisma/client": "^6.1.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^3.0.0",
|
||||||
"bullmq": "^5.61.0",
|
"bullmq": "^5.61.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
"prisma": "^6.1.0"
|
"prisma": "^6.1.0"
|
||||||
},
|
},
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
},
|
},
|
||||||
"frontend": {
|
"frontend": {
|
||||||
"name": "patchmon-frontend",
|
"name": "patchmon-frontend",
|
||||||
"version": "1.3.2",
|
"version": "1.3.3",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
@@ -134,6 +134,7 @@
|
|||||||
"version": "7.28.4",
|
"version": "7.28.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.3",
|
"@babel/generator": "^7.28.3",
|
||||||
@@ -547,6 +548,7 @@
|
|||||||
"node_modules/@bull-board/ui": {
|
"node_modules/@bull-board/ui": {
|
||||||
"version": "6.13.1",
|
"version": "6.13.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "6.13.1"
|
"@bull-board/api": "6.13.1"
|
||||||
}
|
}
|
||||||
@@ -580,6 +582,7 @@
|
|||||||
"node_modules/@dnd-kit/core": {
|
"node_modules/@dnd-kit/core": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/accessibility": "^3.1.1",
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -988,9 +991,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/bcryptjs": {
|
"node_modules/@types/bcryptjs": {
|
||||||
"version": "2.4.6",
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==",
|
||||||
|
"deprecated": "This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "*"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
@@ -1020,6 +1029,7 @@
|
|||||||
"version": "18.3.24",
|
"version": "18.3.24",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@@ -1183,8 +1193,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bcryptjs": {
|
"node_modules/bcryptjs": {
|
||||||
"version": "2.4.3",
|
"version": "3.0.3",
|
||||||
"license": "MIT"
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
@@ -1267,6 +1282,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.3",
|
"baseline-browser-mapping": "^2.8.3",
|
||||||
"caniuse-lite": "^1.0.30001741",
|
"caniuse-lite": "^1.0.30001741",
|
||||||
@@ -1456,6 +1472,7 @@
|
|||||||
"node_modules/chart.js": {
|
"node_modules/chart.js": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kurkle/color": "^0.3.0"
|
"@kurkle/color": "^0.3.0"
|
||||||
},
|
},
|
||||||
@@ -2030,6 +2047,7 @@
|
|||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.2",
|
"version": "4.21.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
@@ -2795,6 +2813,76 @@
|
|||||||
"lefthook-windows-x64": "1.13.5"
|
"lefthook-windows-x64": "1.13.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lefthook-darwin-arm64": {
|
||||||
|
"version": "1.13.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/lefthook-darwin-arm64/-/lefthook-darwin-arm64-1.13.5.tgz",
|
||||||
|
"integrity": "sha512-BYt5CnAOXasVCS6i+A4ljUo9xru/B5uMFD6EWHhs3R26jGF7mBSDxM3ErzXTUaJRTP0kQI/XBmgqBryBqoqZOQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/lefthook-darwin-x64": {
|
||||||
|
"version": "1.13.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/lefthook-darwin-x64/-/lefthook-darwin-x64-1.13.5.tgz",
|
||||||
|
"integrity": "sha512-ZDtLBzvI5e26C/RZ4irOHpELTd22x9lDTgF2+eCYcnrBWOkB7800V8tuAvBybsLGvg6JwKjFxn+NTRNZnCC2hw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/lefthook-freebsd-arm64": {
|
||||||
|
"version": "1.13.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/lefthook-freebsd-arm64/-/lefthook-freebsd-arm64-1.13.5.tgz",
|
||||||
|
"integrity": "sha512-uQ/kQZSSedw74aGCpsfOPN4yVt3klg8grOP6gHQOCRUMv5oK/Lj3pe1PylpTuuhxWORWRzkauPMot26J0OZZdA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/lefthook-freebsd-x64": {
|
||||||
|
"version": "1.13.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/lefthook-freebsd-x64/-/lefthook-freebsd-x64-1.13.5.tgz",
|
||||||
|
"integrity": "sha512-6czek8XagVrI7ExURawkfrfX40Qjc/wktc8bLq/iXfRlmdvKDMrx2FrA82mDfEVCAEz+tTvkteK1TfR3icYF3Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/lefthook-linux-arm64": {
|
||||||
|
"version": "1.13.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/lefthook-linux-arm64/-/lefthook-linux-arm64-1.13.5.tgz",
|
||||||
|
"integrity": "sha512-MjWtiuW1br+rpTtgG1KGV53mSGtL5MWQwgafYzrFleJ89fKb86F4TD/4mVNzk5thmZ+HVPZw9bRZGUHFBnNJWg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/lefthook-linux-x64": {
|
"node_modules/lefthook-linux-x64": {
|
||||||
"version": "1.13.5",
|
"version": "1.13.5",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
@@ -2807,6 +2895,62 @@
|
|||||||
"linux"
|
"linux"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/lefthook-openbsd-arm64": {
|
||||||
|
"version": "1.13.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/lefthook-openbsd-arm64/-/lefthook-openbsd-arm64-1.13.5.tgz",
|
||||||
|
"integrity": "sha512-lYXrWf0/hBrwtG8ceaHq886bcqRKh3Lfv+jZJs+ykMLB6L/kaqk8tA4V2NHWydQ5h56o45ugs/580nMz36ZdRg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/lefthook-openbsd-x64": {
|
||||||
|
"version": "1.13.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/lefthook-openbsd-x64/-/lefthook-openbsd-x64-1.13.5.tgz",
|
||||||
|
"integrity": "sha512-Ba1JrsRbfan4WKd8Q7gUhTxCUuppXzirDObd3JxpLRSLxA47yxhjMv7KByDunRDTvzTgsXoykZI6mPupkc1JiQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/lefthook-windows-arm64": {
|
||||||
|
"version": "1.13.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/lefthook-windows-arm64/-/lefthook-windows-arm64-1.13.5.tgz",
|
||||||
|
"integrity": "sha512-Y/CpmEIb0hlFe+kTT/efWgX6+/gUTp5NItTF+gmUrY1/G/bTLIxdIRS7WpodVM0MEN24sOrQVTSi9DN9FvGoGg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/lefthook-windows-x64": {
|
||||||
|
"version": "1.13.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/lefthook-windows-x64/-/lefthook-windows-x64-1.13.5.tgz",
|
||||||
|
"integrity": "sha512-WJBqGNBlFJnunRwy12QyaDHdGULtostPqpYSZSS4boFJDY0lP5qtz9lAGmJ49aA5GQ19jrnDjGLwVPFiwIqksQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3419,6 +3563,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -3548,6 +3693,7 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/config": "6.16.2",
|
"@prisma/config": "6.16.2",
|
||||||
"@prisma/engines": "6.16.2"
|
"@prisma/engines": "6.16.2"
|
||||||
@@ -3737,6 +3883,7 @@
|
|||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -3755,6 +3902,7 @@
|
|||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -4472,6 +4620,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -4624,6 +4773,7 @@
|
|||||||
"version": "7.1.7",
|
"version": "7.1.7",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -4713,6 +4863,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user