mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-23 16:13:57 +00:00
Compare commits
2 Commits
v1.3.0
...
c98203a997
Author | SHA1 | Date | |
---|---|---|---|
|
c98203a997 | ||
|
37c8f5fa76 |
@@ -1,23 +1,29 @@
|
|||||||
# Database Configuration
|
# Database Configuration
|
||||||
DATABASE_URL="postgresql://patchmon_user:p@tchm0n_p@55@localhost:5432/patchmon_db"
|
DATABASE_URL="postgresql://patchmon_user:your-password-here@localhost:5432/patchmon_db"
|
||||||
PM_DB_CONN_MAX_ATTEMPTS=30
|
PM_DB_CONN_MAX_ATTEMPTS=30
|
||||||
PM_DB_CONN_WAIT_INTERVAL=2
|
PM_DB_CONN_WAIT_INTERVAL=2
|
||||||
|
|
||||||
# Redis Configuration
|
# JWT Configuration
|
||||||
REDIS_HOST=localhost
|
JWT_SECRET=your-secure-random-secret-key-change-this-in-production
|
||||||
REDIS_PORT=6379
|
JWT_EXPIRES_IN=1h
|
||||||
REDIS_USER=your-redis-username-here
|
JWT_REFRESH_EXPIRES_IN=7d
|
||||||
REDIS_PASSWORD=your-redis-password-here
|
|
||||||
REDIS_DB=0
|
|
||||||
|
|
||||||
# Server Configuration
|
# Server Configuration
|
||||||
PORT=3001
|
PORT=3001
|
||||||
NODE_ENV=development
|
NODE_ENV=production
|
||||||
|
|
||||||
# API Configuration
|
# API Configuration
|
||||||
API_VERSION=v1
|
API_VERSION=v1
|
||||||
|
|
||||||
|
# CORS Configuration
|
||||||
CORS_ORIGIN=http://localhost:3000
|
CORS_ORIGIN=http://localhost:3000
|
||||||
|
|
||||||
|
# Session Configuration
|
||||||
|
SESSION_INACTIVITY_TIMEOUT_MINUTES=30
|
||||||
|
|
||||||
|
# User Configuration
|
||||||
|
DEFAULT_USER_ROLE=user
|
||||||
|
|
||||||
# Rate Limiting (times in milliseconds)
|
# Rate Limiting (times in milliseconds)
|
||||||
RATE_LIMIT_WINDOW_MS=900000
|
RATE_LIMIT_WINDOW_MS=900000
|
||||||
RATE_LIMIT_MAX=5000
|
RATE_LIMIT_MAX=5000
|
||||||
@@ -26,20 +32,18 @@ AUTH_RATE_LIMIT_MAX=500
|
|||||||
AGENT_RATE_LIMIT_WINDOW_MS=60000
|
AGENT_RATE_LIMIT_WINDOW_MS=60000
|
||||||
AGENT_RATE_LIMIT_MAX=1000
|
AGENT_RATE_LIMIT_MAX=1000
|
||||||
|
|
||||||
|
# Redis Configuration
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_USER=your-redis-username-here
|
||||||
|
REDIS_PASSWORD=your-redis-password-here
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
ENABLE_LOGGING=true
|
ENABLE_LOGGING=true
|
||||||
|
|
||||||
# User Registration
|
# TFA Configuration (optional - used if TFA is enabled)
|
||||||
DEFAULT_USER_ROLE=user
|
|
||||||
|
|
||||||
# JWT Configuration
|
|
||||||
JWT_SECRET=your-secure-random-secret-key-change-this-in-production
|
|
||||||
JWT_EXPIRES_IN=1h
|
|
||||||
JWT_REFRESH_EXPIRES_IN=7d
|
|
||||||
SESSION_INACTIVITY_TIMEOUT_MINUTES=30
|
|
||||||
|
|
||||||
# TFA Configuration
|
|
||||||
TFA_REMEMBER_ME_EXPIRES_IN=30d
|
TFA_REMEMBER_ME_EXPIRES_IN=30d
|
||||||
TFA_MAX_REMEMBER_SESSIONS=5
|
TFA_MAX_REMEMBER_SESSIONS=5
|
||||||
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3
|
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3
|
||||||
|
10
frontend/env.example
Normal file
10
frontend/env.example
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Frontend Environment Configuration
|
||||||
|
# This file is used by Vite during build and runtime
|
||||||
|
|
||||||
|
# API URL - Update this to match your backend server
|
||||||
|
VITE_API_URL=http://localhost:3001/api/v1
|
||||||
|
|
||||||
|
# Application Metadata
|
||||||
|
VITE_APP_NAME=PatchMon
|
||||||
|
VITE_APP_VERSION=1.3.0
|
||||||
|
|
635
setup.sh
635
setup.sh
@@ -651,10 +651,11 @@ configure_redis() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate Redis username based on instance
|
# Generate Redis username based on instance (global variable for use in create_env_files)
|
||||||
REDIS_USER="patchmon_${DB_SAFE_NAME}"
|
REDIS_USER="patchmon_${DB_SAFE_NAME}"
|
||||||
|
|
||||||
# Generate separate user password (more secure than reusing admin password)
|
# Generate separate user password (more secure than reusing admin password)
|
||||||
|
# This will be stored in the .env file for the application to use
|
||||||
REDIS_USER_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
|
REDIS_USER_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
|
||||||
|
|
||||||
print_info "Creating Redis user: $REDIS_USER for database $REDIS_DB"
|
print_info "Creating Redis user: $REDIS_USER for database $REDIS_DB"
|
||||||
@@ -761,12 +762,9 @@ configure_redis() {
|
|||||||
redis-cli -h 127.0.0.1 -p 6379 --user "$REDIS_USER" --pass "$REDIS_USER_PASSWORD" --no-auth-warning -n "$REDIS_DB" SET "patchmon:initialized" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /dev/null
|
redis-cli -h 127.0.0.1 -p 6379 --user "$REDIS_USER" --pass "$REDIS_USER_PASSWORD" --no-auth-warning -n "$REDIS_DB" SET "patchmon:initialized" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /dev/null
|
||||||
print_status "Marked Redis database $REDIS_DB as in-use"
|
print_status "Marked Redis database $REDIS_DB as in-use"
|
||||||
|
|
||||||
# Update .env with the USER PASSWORD, not admin password
|
# Note: Redis credentials will be written to .env by create_env_files() function
|
||||||
echo "REDIS_USER=$REDIS_USER" >> .env
|
print_status "Redis user '$REDIS_USER' configured successfully"
|
||||||
echo "REDIS_PASSWORD=$REDIS_USER_PASSWORD" >> .env
|
print_info "Redis credentials will be saved to backend/.env"
|
||||||
echo "REDIS_DB=$REDIS_DB" >> .env
|
|
||||||
|
|
||||||
print_status "Redis user password: $REDIS_USER_PASSWORD"
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -1063,12 +1061,17 @@ AGENT_RATE_LIMIT_MAX=1000
|
|||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
REDIS_USER=$REDIS_USER
|
REDIS_USER=$REDIS_USER
|
||||||
REDIS_PASSWORD=$REDIS_PASSWORD
|
REDIS_PASSWORD=$REDIS_USER_PASSWORD
|
||||||
REDIS_DB=$REDIS_DB
|
REDIS_DB=$REDIS_DB
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
ENABLE_LOGGING=true
|
ENABLE_LOGGING=true
|
||||||
|
|
||||||
|
# TFA Configuration
|
||||||
|
TFA_REMEMBER_ME_EXPIRES_IN=30d
|
||||||
|
TFA_MAX_REMEMBER_SESSIONS=5
|
||||||
|
TFA_SUSPICIOUS_ACTIVITY_THRESHOLD=3
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Frontend .env
|
# Frontend .env
|
||||||
@@ -1130,88 +1133,90 @@ EOF
|
|||||||
print_status "Systemd service created: $SERVICE_NAME (running as $INSTANCE_USER)"
|
print_status "Systemd service created: $SERVICE_NAME (running as $INSTANCE_USER)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Setup nginx configuration
|
# Unified nginx configuration generator
|
||||||
setup_nginx() {
|
generate_nginx_config() {
|
||||||
print_info "Setting up nginx configuration..."
|
local fqdn="$1"
|
||||||
log_message "Setting up nginx configuration for $FQDN"
|
local app_dir="$2"
|
||||||
|
local backend_port="$3"
|
||||||
|
local ssl_enabled="$4" # "true" or "false"
|
||||||
|
local config_file="/etc/nginx/sites-available/$fqdn"
|
||||||
|
|
||||||
if [ "$USE_LETSENCRYPT" = "true" ]; then
|
print_info "Generating nginx configuration for $fqdn (SSL: $ssl_enabled)"
|
||||||
# HTTP-only config first for Certbot challenge
|
|
||||||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
if [ "$ssl_enabled" = "true" ]; then
|
||||||
|
# SSL Configuration
|
||||||
|
cat > "$config_file" << EOF
|
||||||
|
# HTTP to HTTPS redirect
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name $FQDN;
|
server_name $fqdn;
|
||||||
|
|
||||||
|
# Let's Encrypt challenge location
|
||||||
location /.well-known/acme-challenge/ {
|
location /.well-known/acme-challenge/ {
|
||||||
root /var/www/html;
|
root /var/www/html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Redirect all other traffic to HTTPS
|
||||||
location / {
|
location / {
|
||||||
return 301 https://\$server_name\$request_uri;
|
return 301 https://\$server_name\$request_uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
|
||||||
else
|
# HTTPS server block
|
||||||
# HTTP-only configuration for local hosting
|
|
||||||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 443 ssl http2;
|
||||||
server_name $FQDN;
|
server_name $fqdn;
|
||||||
|
|
||||||
|
# SSL Configuration
|
||||||
|
ssl_certificate /etc/letsencrypt/live/$fqdn/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/$fqdn/privkey.pem;
|
||||||
|
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||||
|
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||||
|
|
||||||
|
# Security headers (applied to all responses)
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||||
|
add_header X-Frame-Options DENY always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
location / {
|
location / {
|
||||||
root $APP_DIR/frontend/dist;
|
root $app_dir/frontend/dist;
|
||||||
try_files \$uri \$uri/ /index.html;
|
try_files \$uri \$uri/ /index.html;
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header X-Frame-Options DENY;
|
|
||||||
add_header X-Content-Type-Options nosniff;
|
|
||||||
add_header X-XSS-Protection "1; mode=block";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Bull Board proxy
|
# Bull Board proxy
|
||||||
location /bullboard {
|
location /bullboard {
|
||||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
proxy_pass http://127.0.0.1:$backend_port;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
proxy_set_header Host \$host;
|
proxy_set_header Host \$host;
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
proxy_set_header X-Forwarded-Host \$host;
|
proxy_set_header X-Forwarded-Host \$host;
|
||||||
|
proxy_set_header Cookie \$http_cookie;
|
||||||
|
proxy_cache_bypass \$http_upgrade;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_connect_timeout 75s;
|
||||||
|
|
||||||
# CORS headers for Bull Board
|
# Enable cookie passthrough
|
||||||
add_header Access-Control-Allow-Origin * always;
|
proxy_pass_header Set-Cookie;
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
proxy_cookie_path / /;
|
||||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
|
|
||||||
|
# Preserve original client IP
|
||||||
|
proxy_set_header X-Original-Forwarded-For \$http_x_forwarded_for;
|
||||||
|
|
||||||
# Handle preflight requests
|
|
||||||
if (\$request_method = 'OPTIONS') {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# API routes
|
|
||||||
# Bull Board proxy
|
|
||||||
location /bullboard {
|
|
||||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host \$host;
|
|
||||||
|
|
||||||
# CORS headers for Bull Board
|
|
||||||
add_header Access-Control-Allow-Origin * always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
|
|
||||||
|
|
||||||
# Handle preflight requests
|
|
||||||
if (\$request_method = 'OPTIONS') {
|
if (\$request_method = 'OPTIONS') {
|
||||||
return 204;
|
return 204;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# API proxy
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
proxy_pass http://127.0.0.1:$backend_port;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade \$http_upgrade;
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
proxy_set_header Connection 'upgrade';
|
proxy_set_header Connection 'upgrade';
|
||||||
@@ -1222,16 +1227,122 @@ server {
|
|||||||
proxy_cache_bypass \$http_upgrade;
|
proxy_cache_bypass \$http_upgrade;
|
||||||
proxy_read_timeout 300s;
|
proxy_read_timeout 300s;
|
||||||
proxy_connect_timeout 75s;
|
proxy_connect_timeout 75s;
|
||||||
|
|
||||||
|
# Preserve original client IP
|
||||||
|
proxy_set_header X-Original-Forwarded-For \$http_x_forwarded_for;
|
||||||
|
|
||||||
|
if (\$request_method = 'OPTIONS') {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Health check
|
# Static assets caching (exclude Bull Board assets)
|
||||||
|
location ~* ^/(?!bullboard).*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
location /health {
|
location /health {
|
||||||
proxy_pass http://127.0.0.1:$BACKEND_PORT/health;
|
proxy_pass http://127.0.0.1:$backend_port/health;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
# HTTP-only configuration
|
||||||
|
cat > "$config_file" << EOF
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name $fqdn;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options DENY always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
location / {
|
||||||
|
root $app_dir/frontend/dist;
|
||||||
|
try_files \$uri \$uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bull Board proxy
|
||||||
|
location /bullboard {
|
||||||
|
proxy_pass http://127.0.0.1:$backend_port;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host \$host;
|
||||||
|
proxy_set_header Cookie \$http_cookie;
|
||||||
|
proxy_cache_bypass \$http_upgrade;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_connect_timeout 75s;
|
||||||
|
|
||||||
|
# Enable cookie passthrough
|
||||||
|
proxy_pass_header Set-Cookie;
|
||||||
|
proxy_cookie_path / /;
|
||||||
|
|
||||||
|
# Preserve original client IP
|
||||||
|
proxy_set_header X-Original-Forwarded-For \$http_x_forwarded_for;
|
||||||
|
|
||||||
|
if (\$request_method = 'OPTIONS') {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# API proxy
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:$backend_port;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade \$http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host \$host;
|
||||||
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||||
|
proxy_cache_bypass \$http_upgrade;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_connect_timeout 75s;
|
||||||
|
|
||||||
|
# Preserve original client IP
|
||||||
|
proxy_set_header X-Original-Forwarded-For \$http_x_forwarded_for;
|
||||||
|
|
||||||
|
if (\$request_method = 'OPTIONS') {
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static assets caching (exclude Bull Board assets)
|
||||||
|
location ~* ^/(?!bullboard).*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health {
|
||||||
|
proxy_pass http://127.0.0.1:$backend_port/health;
|
||||||
access_log off;
|
access_log off;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
print_status "Nginx configuration generated for $fqdn"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Setup nginx configuration
|
||||||
|
setup_nginx() {
|
||||||
|
print_info "Setting up nginx configuration..."
|
||||||
|
log_message "Setting up nginx configuration for $FQDN"
|
||||||
|
|
||||||
|
# Generate HTTP-only config first (needed for Let's Encrypt challenge if SSL enabled)
|
||||||
|
generate_nginx_config "$FQDN" "$APP_DIR" "$BACKEND_PORT" "false"
|
||||||
|
|
||||||
# Enable site
|
# Enable site
|
||||||
ln -sf "/etc/nginx/sites-available/$FQDN" "/etc/nginx/sites-enabled/"
|
ln -sf "/etc/nginx/sites-available/$FQDN" "/etc/nginx/sites-enabled/"
|
||||||
@@ -1254,96 +1365,10 @@ setup_letsencrypt() {
|
|||||||
|
|
||||||
# Check if a valid certificate already exists
|
# Check if a valid certificate already exists
|
||||||
if certbot certificates 2>/dev/null | grep -q "$FQDN" && certbot certificates 2>/dev/null | grep -A 10 "$FQDN" | grep -q "VALID"; then
|
if certbot certificates 2>/dev/null | grep -q "$FQDN" && certbot certificates 2>/dev/null | grep -A 10 "$FQDN" | grep -q "VALID"; then
|
||||||
print_status "Valid SSL certificate already exists for $FQDN, skipping certificate generation"
|
print_status "Valid SSL certificate already exists for $FQDN"
|
||||||
|
|
||||||
# Update Nginx config with existing HTTPS configuration
|
# Generate SSL config with existing certificate
|
||||||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
generate_nginx_config "$FQDN" "$APP_DIR" "$BACKEND_PORT" "true"
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name $FQDN;
|
|
||||||
return 301 https://\$server_name\$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name $FQDN;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/$FQDN/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/$FQDN/privkey.pem;
|
|
||||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
|
||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
||||||
add_header X-Frame-Options DENY;
|
|
||||||
add_header X-Content-Type-Options nosniff;
|
|
||||||
add_header X-XSS-Protection "1; mode=block";
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
location / {
|
|
||||||
root $APP_DIR/frontend/dist;
|
|
||||||
try_files \$uri \$uri/ /index.html;
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header X-Frame-Options DENY;
|
|
||||||
add_header X-Content-Type-Options nosniff;
|
|
||||||
add_header X-XSS-Protection "1; mode=block";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bull Board proxy
|
|
||||||
location /bullboard {
|
|
||||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host \$host;
|
|
||||||
|
|
||||||
# CORS headers for Bull Board
|
|
||||||
add_header Access-Control-Allow-Origin * always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
|
|
||||||
|
|
||||||
# Handle preflight requests
|
|
||||||
if (\$request_method = 'OPTIONS') {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# API proxy
|
|
||||||
# Bull Board proxy
|
|
||||||
location /bullboard {
|
|
||||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host \$host;
|
|
||||||
|
|
||||||
# CORS headers for Bull Board
|
|
||||||
add_header Access-Control-Allow-Origin * always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
|
|
||||||
|
|
||||||
# Handle preflight requests
|
|
||||||
if (\$request_method = 'OPTIONS') {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade \$http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
||||||
proxy_cache_bypass \$http_upgrade;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Enable the site
|
# Enable the site
|
||||||
ln -sf "/etc/nginx/sites-available/$FQDN" "/etc/nginx/sites-enabled/"
|
ln -sf "/etc/nginx/sites-available/$FQDN" "/etc/nginx/sites-enabled/"
|
||||||
@@ -1362,7 +1387,7 @@ EOF
|
|||||||
|
|
||||||
print_info "No valid certificate found, generating new SSL certificate..."
|
print_info "No valid certificate found, generating new SSL certificate..."
|
||||||
|
|
||||||
# Wait a moment for nginx to be ready
|
# Wait for nginx to be ready
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
# Obtain SSL certificate
|
# Obtain SSL certificate
|
||||||
@@ -1370,100 +1395,17 @@ EOF
|
|||||||
certbot --nginx -d "$FQDN" --non-interactive --agree-tos --email "$EMAIL" --redirect
|
certbot --nginx -d "$FQDN" --non-interactive --agree-tos --email "$EMAIL" --redirect
|
||||||
log_message "SSL certificate obtained successfully"
|
log_message "SSL certificate obtained successfully"
|
||||||
|
|
||||||
# Update Nginx config with full HTTPS configuration
|
# Generate SSL nginx configuration
|
||||||
cat > "/etc/nginx/sites-available/$FQDN" << EOF
|
generate_nginx_config "$FQDN" "$APP_DIR" "$BACKEND_PORT" "true"
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name $FQDN;
|
|
||||||
return 301 https://\$server_name\$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name $FQDN;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/$FQDN/fullchain.pem;
|
# Test and reload nginx
|
||||||
ssl_certificate_key /etc/letsencrypt/live/$FQDN/privkey.pem;
|
if nginx -t; then
|
||||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
systemctl reload nginx
|
||||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
print_status "Nginx configuration updated successfully"
|
||||||
|
else
|
||||||
# Frontend
|
print_error "Nginx configuration test failed"
|
||||||
location / {
|
return 1
|
||||||
root $APP_DIR/frontend/dist;
|
fi
|
||||||
try_files \$uri \$uri/ /index.html;
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header X-Frame-Options DENY;
|
|
||||||
add_header X-Content-Type-Options nosniff;
|
|
||||||
add_header X-XSS-Protection "1; mode=block";
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bull Board proxy
|
|
||||||
location /bullboard {
|
|
||||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host \$host;
|
|
||||||
|
|
||||||
# CORS headers for Bull Board
|
|
||||||
add_header Access-Control-Allow-Origin * always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
|
|
||||||
|
|
||||||
# Handle preflight requests
|
|
||||||
if (\$request_method = 'OPTIONS') {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# API routes
|
|
||||||
# Bull Board proxy
|
|
||||||
location /bullboard {
|
|
||||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host \$host;
|
|
||||||
|
|
||||||
# CORS headers for Bull Board
|
|
||||||
add_header Access-Control-Allow-Origin * always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
|
||||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" always;
|
|
||||||
|
|
||||||
# Handle preflight requests
|
|
||||||
if (\$request_method = 'OPTIONS') {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://127.0.0.1:$BACKEND_PORT;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade \$http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_set_header X-Real-IP \$remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
|
||||||
proxy_cache_bypass \$http_upgrade;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
proxy_connect_timeout 75s;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
location /health {
|
|
||||||
proxy_pass http://127.0.0.1:$BACKEND_PORT/health;
|
|
||||||
access_log off;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
nginx -t
|
|
||||||
nginx -s reload
|
|
||||||
|
|
||||||
# Setup auto-renewal
|
# Setup auto-renewal
|
||||||
echo "0 12 * * * /usr/bin/certbot renew --quiet" | crontab -
|
echo "0 12 * * * /usr/bin/certbot renew --quiet" | crontab -
|
||||||
@@ -2022,6 +1964,190 @@ select_installation_to_update() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Check and update Redis configuration for existing installation
|
||||||
|
update_redis_configuration() {
|
||||||
|
print_info "Checking Redis configuration..."
|
||||||
|
|
||||||
|
# Check if Redis configuration exists in .env
|
||||||
|
if [ -f "$instance_dir/backend/.env" ]; then
|
||||||
|
if grep -q "^REDIS_HOST=" "$instance_dir/backend/.env" && \
|
||||||
|
grep -q "^REDIS_PASSWORD=" "$instance_dir/backend/.env"; then
|
||||||
|
print_status "Redis configuration already exists in .env"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_warning "Redis configuration not found in .env - this is a legacy installation"
|
||||||
|
print_info "Setting up Redis for this instance..."
|
||||||
|
|
||||||
|
# Detect package manager if not already set
|
||||||
|
if [ -z "$PKG_INSTALL" ]; then
|
||||||
|
if command -v apt >/dev/null 2>&1; then
|
||||||
|
PKG_INSTALL="apt install -y"
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
PKG_INSTALL="apt-get install -y"
|
||||||
|
else
|
||||||
|
print_error "No supported package manager found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure Redis is installed and running
|
||||||
|
if ! systemctl is-active --quiet redis-server; then
|
||||||
|
print_info "Installing Redis..."
|
||||||
|
$PKG_INSTALL redis-server
|
||||||
|
systemctl start redis-server
|
||||||
|
systemctl enable redis-server
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate Redis variables for this instance
|
||||||
|
# Extract DB_SAFE_NAME from existing database name
|
||||||
|
DB_SAFE_NAME=$(echo "$DB_NAME" | sed 's/[^a-zA-Z0-9]/_/g')
|
||||||
|
REDIS_USER="patchmon_${DB_SAFE_NAME}"
|
||||||
|
REDIS_USER_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
|
||||||
|
|
||||||
|
# Find available Redis database
|
||||||
|
print_info "Finding available Redis database..."
|
||||||
|
local redis_db=0
|
||||||
|
local max_attempts=16
|
||||||
|
|
||||||
|
while [ $redis_db -lt $max_attempts ]; do
|
||||||
|
local key_count
|
||||||
|
key_count=$(redis-cli -h localhost -p 6379 -n "$redis_db" DBSIZE 2>&1 | grep -v "ERR" || echo "1")
|
||||||
|
|
||||||
|
if [ "$key_count" = "0" ] || [ "$key_count" = "(integer) 0" ]; then
|
||||||
|
print_status "Found available Redis database: $redis_db"
|
||||||
|
REDIS_DB=$redis_db
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
redis_db=$((redis_db + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$REDIS_DB" ]; then
|
||||||
|
print_warning "No empty Redis database found, using database 0"
|
||||||
|
REDIS_DB=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate admin password if not exists
|
||||||
|
REDIS_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
|
||||||
|
|
||||||
|
# Configure Redis with ACL if needed
|
||||||
|
print_info "Configuring Redis ACL..."
|
||||||
|
|
||||||
|
# Create ACL file if it doesn't exist
|
||||||
|
if [ ! -f /etc/redis/users.acl ]; then
|
||||||
|
touch /etc/redis/users.acl
|
||||||
|
chown redis:redis /etc/redis/users.acl
|
||||||
|
chmod 640 /etc/redis/users.acl
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configure ACL file in redis.conf
|
||||||
|
if ! grep -q "^aclfile" /etc/redis/redis.conf 2>/dev/null; then
|
||||||
|
echo "aclfile /etc/redis/users.acl" >> /etc/redis/redis.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove requirepass (incompatible with ACL)
|
||||||
|
if grep -q "^requirepass" /etc/redis/redis.conf 2>/dev/null; then
|
||||||
|
sed -i 's/^requirepass.*/# &/' /etc/redis/redis.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create admin user if it doesn't exist
|
||||||
|
if ! grep -q "^user admin" /etc/redis/users.acl; then
|
||||||
|
echo "user admin on sanitize-payload >$REDIS_PASSWORD ~* &* +@all" >> /etc/redis/users.acl
|
||||||
|
systemctl restart redis-server
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create instance-specific Redis user
|
||||||
|
print_info "Creating Redis user: $REDIS_USER"
|
||||||
|
|
||||||
|
# Try to authenticate with admin (may already exist from another instance)
|
||||||
|
local acl_result
|
||||||
|
acl_result=$(redis-cli -h 127.0.0.1 -p 6379 --user admin --pass "$REDIS_PASSWORD" --no-auth-warning ACL SETUSER "$REDIS_USER" on ">${REDIS_USER_PASSWORD}" ~* +@all 2>&1)
|
||||||
|
|
||||||
|
if [ "$acl_result" = "OK" ] || echo "$acl_result" | grep -q "OK"; then
|
||||||
|
print_status "Redis user created successfully"
|
||||||
|
redis-cli -h 127.0.0.1 -p 6379 --user admin --pass "$REDIS_PASSWORD" --no-auth-warning ACL SAVE > /dev/null 2>&1
|
||||||
|
else
|
||||||
|
print_warning "Could not create Redis user with ACL, trying without authentication..."
|
||||||
|
# Fallback for systems without ACL configured
|
||||||
|
redis-cli -h 127.0.0.1 -p 6379 CONFIG SET requirepass "$REDIS_USER_PASSWORD" > /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Backup existing .env
|
||||||
|
cp "$instance_dir/backend/.env" "$instance_dir/backend/.env.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
print_info "Backed up existing .env file"
|
||||||
|
|
||||||
|
# Add Redis configuration to .env
|
||||||
|
print_info "Adding Redis configuration to .env..."
|
||||||
|
cat >> "$instance_dir/backend/.env" << EOF
|
||||||
|
|
||||||
|
# Redis Configuration (added during update)
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_USER=$REDIS_USER
|
||||||
|
REDIS_PASSWORD=$REDIS_USER_PASSWORD
|
||||||
|
REDIS_DB=$REDIS_DB
|
||||||
|
EOF
|
||||||
|
|
||||||
|
print_status "Redis configuration added to .env"
|
||||||
|
print_info "Redis User: $REDIS_USER"
|
||||||
|
print_info "Redis Database: $REDIS_DB"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update nginx configuration for existing installation
|
||||||
|
update_nginx_configuration() {
|
||||||
|
print_info "Updating nginx configuration..."
|
||||||
|
|
||||||
|
# Detect SSL status
|
||||||
|
local ssl_enabled="false"
|
||||||
|
if [ -f "/etc/letsencrypt/live/$SELECTED_INSTANCE/fullchain.pem" ]; then
|
||||||
|
ssl_enabled="true"
|
||||||
|
print_info "SSL certificate detected, updating HTTPS configuration"
|
||||||
|
else
|
||||||
|
print_info "No SSL certificate found, updating HTTP configuration"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Backup existing config
|
||||||
|
local backup_file="/etc/nginx/sites-available/$SELECTED_INSTANCE.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
if [ -f "/etc/nginx/sites-available/$SELECTED_INSTANCE" ]; then
|
||||||
|
cp "/etc/nginx/sites-available/$SELECTED_INSTANCE" "$backup_file"
|
||||||
|
print_info "Backed up existing nginx config to: $backup_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract backend port
|
||||||
|
local backend_port=$(grep -o 'proxy_pass http://127.0.0.1:[0-9]*' "/etc/nginx/sites-available/$SELECTED_INSTANCE" 2>/dev/null | grep -oP ':\K[0-9]+' | head -1)
|
||||||
|
if [ -z "$backend_port" ] && [ -f "$instance_dir/backend/.env" ]; then
|
||||||
|
backend_port=$(grep '^PORT=' "$instance_dir/backend/.env" | cut -d'=' -f2 | tr -d ' ')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$backend_port" ]; then
|
||||||
|
print_warning "Could not determine backend port, skipping nginx config update"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_info "Detected backend port: $backend_port"
|
||||||
|
|
||||||
|
# Generate new configuration using the unified function
|
||||||
|
generate_nginx_config "$SELECTED_INSTANCE" "$instance_dir" "$backend_port" "$ssl_enabled"
|
||||||
|
|
||||||
|
# Test and reload nginx
|
||||||
|
if nginx -t; then
|
||||||
|
systemctl reload nginx
|
||||||
|
print_status "Nginx configuration updated successfully"
|
||||||
|
else
|
||||||
|
print_error "Nginx configuration test failed"
|
||||||
|
# Restore backup
|
||||||
|
if [ -f "$backup_file" ]; then
|
||||||
|
mv "$backup_file" "/etc/nginx/sites-available/$SELECTED_INSTANCE"
|
||||||
|
print_info "Restored backup nginx configuration"
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Update existing installation
|
# Update existing installation
|
||||||
update_installation() {
|
update_installation() {
|
||||||
local instance_dir="/opt/$SELECTED_INSTANCE"
|
local instance_dir="/opt/$SELECTED_INSTANCE"
|
||||||
@@ -2145,6 +2271,12 @@ update_installation() {
|
|||||||
npx prisma generate
|
npx prisma generate
|
||||||
npx prisma migrate deploy
|
npx prisma migrate deploy
|
||||||
|
|
||||||
|
# Check and update Redis configuration if needed (for legacy installations)
|
||||||
|
update_redis_configuration
|
||||||
|
|
||||||
|
# Update nginx configuration with latest improvements
|
||||||
|
update_nginx_configuration
|
||||||
|
|
||||||
# Start the service
|
# Start the service
|
||||||
print_info "Starting service: $service_name"
|
print_info "Starting service: $service_name"
|
||||||
systemctl start "$service_name"
|
systemctl start "$service_name"
|
||||||
@@ -2212,6 +2344,27 @@ main() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Normal installation mode
|
# Normal installation mode
|
||||||
|
# Check if existing installations are present
|
||||||
|
local existing_installs=($(detect_installations))
|
||||||
|
if [ ${#existing_installs[@]} -gt 0 ]; then
|
||||||
|
print_warning "⚠️ Found ${#existing_installs[@]} existing PatchMon installation(s):"
|
||||||
|
for install in "${existing_installs[@]}"; do
|
||||||
|
print_info " - $install"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
print_warning "If you want to UPDATE an existing installation, run:"
|
||||||
|
print_info " sudo bash $0 --update"
|
||||||
|
echo ""
|
||||||
|
print_warning "If you want to create a NEW installation alongside the existing one(s), continue below."
|
||||||
|
echo ""
|
||||||
|
read_yes_no "Do you want to continue with NEW installation?" CONTINUE_NEW "n"
|
||||||
|
|
||||||
|
if [ "$CONTINUE_NEW" != "y" ]; then
|
||||||
|
print_info "Installation cancelled. Run with --update flag to update existing installations."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Run interactive setup
|
# Run interactive setup
|
||||||
interactive_setup
|
interactive_setup
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user