From 2fbd1d860d4e9709c1be1092345a3fe12c655ba7 Mon Sep 17 00:00:00 2001 From: izzzzzi Date: Fri, 23 May 2025 02:00:06 +0500 Subject: [PATCH] feat: add PostgreSQL support and database configuration options - Updated Docker configuration to support PostgreSQL as an alternative to SQLite. - Enhanced DockerEntrypoint.sh to create a database environment file and test PostgreSQL connection. - Introduced database setup functions in install.sh for PostgreSQL installation and configuration. - Added database management options in x-ui.sh, including backup and switching between SQLite and PostgreSQL. - Implemented database configuration retrieval in the web service and controller layers. - Updated frontend settings to include database configuration options. - Added translations for new database settings in multiple languages. --- DockerEntrypoint.sh | 56 +++- Dockerfile | 17 +- README.Docker.md | 347 ++++++++++++++++++++ config/database.go | 130 ++++++++ database/db.go | 167 +++++++++- docker-compose.postgresql.yml | 122 +++++++ docker-compose.yml | 61 +++- env.example | 67 ++++ go.mod | 5 + go.sum | 10 + init-postgres.sql | 46 +++ install.sh | 180 +++++++++-- pgadmin_servers.json | 23 ++ wait-for-postgres.sh | 17 + web/assets/js/model/setting.js | 17 +- web/controller/setting.go | 18 ++ web/entity/entity.go | 12 +- web/html/settings.html | 3 + web/html/settings/panel/database.html | 86 +++++ web/service/setting.go | 134 ++++++++ web/translation/translate.en_US.toml | 34 +- web/translation/translate.es_ES.toml | 22 ++ web/translation/translate.zh_CN.toml | 24 +- x-ui-postgresql.service | 42 +++ x-ui.sh | 437 ++++++++++++++++++++++---- 25 files changed, 1969 insertions(+), 108 deletions(-) create mode 100644 README.Docker.md create mode 100644 config/database.go create mode 100644 docker-compose.postgresql.yml create mode 100644 env.example create mode 100644 init-postgres.sql create mode 100644 pgadmin_servers.json create mode 100644 wait-for-postgres.sh create mode 100644 web/html/settings/panel/database.html create mode 100644 x-ui-postgresql.service diff --git a/DockerEntrypoint.sh b/DockerEntrypoint.sh index 7511d2ea..1706c84c 100644 --- a/DockerEntrypoint.sh +++ b/DockerEntrypoint.sh @@ -1,7 +1,61 @@ #!/bin/sh +# Function to create database environment file +create_db_env() { + mkdir -p /etc/x-ui + cat > /etc/x-ui/db.env << EOF +DB_TYPE=${DB_TYPE:-sqlite} +DB_HOST=${DB_HOST:-localhost} +DB_PORT=${DB_PORT:-5432} +DB_NAME=${DB_NAME:-x_ui} +DB_USER=${DB_USER:-x_ui} +DB_PASSWORD=${DB_PASSWORD} +DB_SSLMODE=${DB_SSLMODE:-disable} +DB_TIMEZONE=${DB_TIMEZONE:-UTC} +EOF + chmod 600 /etc/x-ui/db.env +} + +# Function to wait for PostgreSQL +wait_for_postgres() { + if [ "$DB_TYPE" = "postgres" ]; then + echo "Waiting for PostgreSQL to be ready..." + /app/wait-for-postgres.sh "$DB_HOST" "$DB_PORT" "$DB_USER" "$DB_NAME" + echo "PostgreSQL is ready!" + fi +} + +# Function to test database connection +test_db_connection() { + if [ "$DB_TYPE" = "postgres" ]; then + echo "Testing PostgreSQL connection..." + if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;" > /dev/null 2>&1; then + echo "PostgreSQL connection successful!" + else + echo "ERROR: Cannot connect to PostgreSQL database!" + echo "Please check your database configuration:" + echo " DB_HOST: $DB_HOST" + echo " DB_PORT: $DB_PORT" + echo " DB_NAME: $DB_NAME" + echo " DB_USER: $DB_USER" + exit 1 + fi + else + echo "Using SQLite database" + fi +} + +# Create database environment file +create_db_env + +# Wait for PostgreSQL if needed +wait_for_postgres + +# Test database connection +test_db_connection + # Start fail2ban -[ $XUI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start +[ "$XUI_ENABLE_FAIL2BAN" = "true" ] && fail2ban-client -x start # Run x-ui exec /app/x-ui diff --git a/Dockerfile b/Dockerfile index 4a15f98d..40b7749f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,12 +29,14 @@ RUN apk add --no-cache --update \ ca-certificates \ tzdata \ fail2ban \ - bash + bash \ + postgresql-client \ + curl COPY --from=builder /app/build/ /app/ COPY --from=builder /app/DockerEntrypoint.sh /app/ COPY --from=builder /app/x-ui.sh /usr/bin/x-ui - +COPY wait-for-postgres.sh /app/ # Configure fail2ban RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \ @@ -46,9 +48,20 @@ RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \ RUN chmod +x \ /app/DockerEntrypoint.sh \ /app/x-ui \ + /app/wait-for-postgres.sh \ /usr/bin/x-ui +# Environment variables for database configuration ENV XUI_ENABLE_FAIL2BAN="true" +ENV DB_TYPE="sqlite" +ENV DB_HOST="localhost" +ENV DB_PORT="5432" +ENV DB_NAME="x_ui" +ENV DB_USER="x_ui" +ENV DB_PASSWORD="" +ENV DB_SSLMODE="disable" +ENV DB_TIMEZONE="UTC" + VOLUME [ "/etc/x-ui" ] CMD [ "./x-ui" ] ENTRYPOINT [ "/app/DockerEntrypoint.sh" ] diff --git a/README.Docker.md b/README.Docker.md new file mode 100644 index 00000000..9cbea56e --- /dev/null +++ b/README.Docker.md @@ -0,0 +1,347 @@ +# ๐Ÿณ 3X-UI Docker Setup with PostgreSQL Support + +This guide explains how to run 3X-UI with Docker, supporting both SQLite (default) and PostgreSQL databases. + +## ๐Ÿ“‹ Quick Start + +### Option 1: SQLite (Default, Recommended for most users) + +```bash +# Clone the repository +git clone https://github.com/MHSanaei/3x-ui.git +cd 3x-ui + +# Start with SQLite (default) +docker-compose up -d +``` + +### Option 2: PostgreSQL (Production setup) + +```bash +# Clone the repository +git clone https://github.com/MHSanaei/3x-ui.git +cd 3x-ui + +# Copy environment file and configure +cp env.example .env +# Edit .env file with your PostgreSQL settings + +# Start with PostgreSQL +docker-compose -f docker-compose.postgresql.yml up -d +``` + +## ๐Ÿ”ง Configuration + +### Environment Variables + +Copy `env.example` to `.env` and modify according to your needs: + +```bash +cp env.example .env +``` + +Key variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `DB_TYPE` | `sqlite` | Database type: `sqlite` or `postgres` | +| `DB_HOST` | `localhost` | PostgreSQL host | +| `DB_PORT` | `5432` | PostgreSQL port | +| `DB_NAME` | `x_ui` | Database name | +| `DB_USER` | `x_ui` | Database user | +| `DB_PASSWORD` | - | Database password (required for PostgreSQL) | +| `XUI_PORT` | `2053` | 3X-UI web interface port | +| `XUI_SUB_PORT` | `2096` | Subscription port | + +### Database Types + +#### SQLite (Default) +- **Pros**: Simple setup, no additional containers, good for small-medium loads +- **Cons**: Limited concurrent connections, single file storage +- **Use case**: Personal use, small teams, development + +#### PostgreSQL +- **Pros**: High performance, concurrent connections, ACID compliance, scalability +- **Cons**: More complex setup, additional container +- **Use case**: Production environments, high load, multiple users + +## ๐Ÿš€ Deployment Options + +### 1. SQLite Deployment + +```bash +# Basic SQLite setup +docker-compose up -d + +# Check logs +docker-compose logs -f 3x-ui +``` + +### 2. PostgreSQL Deployment + +```bash +# Set environment variables +export DB_PASSWORD="your_secure_password_here" + +# Start PostgreSQL setup +docker-compose -f docker-compose.postgresql.yml up -d + +# Check all services +docker-compose -f docker-compose.postgresql.yml ps +``` + +### 3. PostgreSQL with Admin Interface + +```bash +# Start with PgAdmin for database management +docker-compose -f docker-compose.postgresql.yml --profile admin up -d + +# Access PgAdmin at http://localhost:5050 +# Default login: admin@example.com / admin_password +``` + +### 4. External PostgreSQL + +```bash +# Configure .env for external database +cat > .env << EOF +DB_TYPE=postgres +DB_HOST=your-postgres-server.com +DB_PORT=5432 +DB_NAME=x_ui_production +DB_USER=x_ui_user +DB_PASSWORD=your_external_db_password +DB_SSLMODE=require +EOF + +# Start only 3X-UI container +docker-compose up -d +``` + +## ๐Ÿ” Monitoring and Maintenance + +### Health Checks + +All services include health checks: + +```bash +# Check service health +docker-compose ps + +# View health check logs +docker inspect --format='{{json .State.Health}}' 3x-ui +``` + +### Logs + +```bash +# View 3X-UI logs +docker-compose logs -f 3x-ui + +# View PostgreSQL logs +docker-compose -f docker-compose.postgresql.yml logs -f postgres + +# View all logs +docker-compose -f docker-compose.postgresql.yml logs -f +``` + +### Database Management + +#### Backup PostgreSQL + +```bash +# Create backup +docker-compose -f docker-compose.postgresql.yml exec postgres pg_dump -U x_ui x_ui > backup.sql + +# Restore backup +docker-compose -f docker-compose.postgresql.yml exec -T postgres psql -U x_ui x_ui < backup.sql +``` + +#### Backup SQLite + +```bash +# SQLite backup +docker-compose exec 3x-ui cp /etc/x-ui/x-ui.db /etc/x-ui/x-ui.db.backup +docker cp 3x-ui:/etc/x-ui/x-ui.db.backup ./x-ui-backup.db +``` + +## ๐Ÿ”ง Troubleshooting + +### Common Issues + +#### PostgreSQL Connection Failed +```bash +# Check PostgreSQL is running +docker-compose -f docker-compose.postgresql.yml ps postgres + +# Check PostgreSQL logs +docker-compose -f docker-compose.postgresql.yml logs postgres + +# Test connection manually +docker-compose -f docker-compose.postgresql.yml exec postgres psql -U x_ui -d x_ui -c "SELECT 1;" +``` + +#### 3X-UI Won't Start +```bash +# Check 3X-UI logs +docker-compose logs 3x-ui + +# Check database environment +docker-compose exec 3x-ui cat /etc/x-ui/db.env + +# Restart services +docker-compose restart +``` + +#### Port Conflicts +```bash +# Change ports in .env file +echo "XUI_PORT=3053" >> .env +echo "XUI_SUB_PORT=3096" >> .env + +# Restart with new ports +docker-compose up -d +``` + +### Performance Tuning + +#### PostgreSQL Optimization + +The PostgreSQL container is pre-configured with optimized settings for 3X-UI workload: + +- `max_connections=200` +- `shared_buffers=256MB` +- `effective_cache_size=1GB` +- `work_mem=4MB` + +For high-load environments, consider: + +1. **Increase resources**: + ```yaml + deploy: + resources: + limits: + memory: 2G + cpus: '1.0' + ``` + +2. **Use external PostgreSQL** with dedicated hardware + +3. **Enable connection pooling** (PgBouncer) + +## ๐Ÿ”’ Security Considerations + +### Production Checklist + +- [ ] Change default passwords in `.env` +- [ ] Use strong, unique passwords +- [ ] Enable SSL for PostgreSQL (`DB_SSLMODE=require`) +- [ ] Restrict network access (firewall rules) +- [ ] Regular backups +- [ ] Monitor logs for suspicious activity +- [ ] Keep containers updated + +### Network Security + +```yaml +# Example: Restrict PostgreSQL access +services: + postgres: + ports: + - "127.0.0.1:5432:5432" # Only localhost access +``` + +## ๐Ÿ“Š Monitoring + +### Prometheus Metrics (Optional) + +Add monitoring stack: + +```yaml +# Add to docker-compose.postgresql.yml + prometheus: + image: prom/prometheus + ports: + - "9090:9090" + + grafana: + image: grafana/grafana + ports: + - "3000:3000" +``` + +## ๐Ÿ”„ Migration + +### SQLite to PostgreSQL + +1. **Backup SQLite data**: + ```bash + docker-compose exec 3x-ui cp /etc/x-ui/x-ui.db /etc/x-ui/backup.db + ``` + +2. **Export data** (manual process - depends on your data structure) + +3. **Switch to PostgreSQL**: + ```bash + # Update .env + echo "DB_TYPE=postgres" >> .env + + # Start PostgreSQL + docker-compose -f docker-compose.postgresql.yml up -d + ``` + +4. **Import data** (application will create tables automatically) + +### PostgreSQL to SQLite + +1. **Export PostgreSQL data** +2. **Switch to SQLite** in `.env` +3. **Import data** + +## ๐Ÿ“ž Support + +- **Issues**: [GitHub Issues](https://github.com/MHSanaei/3x-ui/issues) +- **Documentation**: [Main README](README.md) +- **Community**: [Telegram](https://t.me/x_ui_channel) + +## ๐Ÿ“ Examples + +### Development Setup + +```bash +# Quick development setup with SQLite +git clone https://github.com/MHSanaei/3x-ui.git +cd 3x-ui +docker-compose up -d +``` + +### Production Setup + +```bash +# Production setup with PostgreSQL +git clone https://github.com/MHSanaei/3x-ui.git +cd 3x-ui + +# Configure environment +cat > .env << EOF +DB_TYPE=postgres +DB_PASSWORD=$(openssl rand -base64 32) +XUI_PORT=2053 +XUI_SUB_PORT=2096 +HOSTNAME=$(hostname) +EOF + +# Deploy +docker-compose -f docker-compose.postgresql.yml up -d + +# Verify +docker-compose -f docker-compose.postgresql.yml ps +``` + +### High Availability Setup + +For HA setups, consider: +- External PostgreSQL cluster +- Load balancer for 3X-UI instances +- Shared storage for certificates +- Monitoring and alerting \ No newline at end of file diff --git a/config/database.go b/config/database.go new file mode 100644 index 00000000..00e69dfa --- /dev/null +++ b/config/database.go @@ -0,0 +1,130 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" +) + +// DatabaseType represents the type of database +type DatabaseType string + +const ( + DatabaseTypeSQLite DatabaseType = "sqlite" + DatabaseTypePostgreSQL DatabaseType = "postgres" +) + +// DatabaseConfig holds database configuration +type DatabaseConfig struct { + Type DatabaseType `json:"type"` + SQLite SQLiteConfig `json:"sqlite"` + Postgres PostgresConfig `json:"postgres"` +} + +// SQLiteConfig holds SQLite specific configuration +type SQLiteConfig struct { + Path string `json:"path"` +} + +// PostgresConfig holds PostgreSQL specific configuration +type PostgresConfig struct { + Host string `json:"host"` + Port int `json:"port"` + Database string `json:"database"` + Username string `json:"username"` + Password string `json:"password"` + SSLMode string `json:"sslMode"` + TimeZone string `json:"timeZone"` +} + +// GetDSN returns the data source name for the database +func (c *DatabaseConfig) GetDSN() string { + switch c.Type { + case DatabaseTypeSQLite: + return c.SQLite.Path + case DatabaseTypePostgreSQL: + return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=%s", + c.Postgres.Host, + c.Postgres.Username, + c.Postgres.Password, + c.Postgres.Database, + c.Postgres.Port, + c.Postgres.SSLMode, + c.Postgres.TimeZone, + ) + default: + return c.SQLite.Path + } +} + +// GetDefaultDatabaseConfig returns default database configuration +func GetDefaultDatabaseConfig() *DatabaseConfig { + return &DatabaseConfig{ + Type: DatabaseTypeSQLite, + SQLite: SQLiteConfig{ + Path: getDefaultSQLitePath(), + }, + Postgres: PostgresConfig{ + Host: "localhost", + Port: 5432, + Database: "x_ui", + Username: "x_ui", + Password: "", + SSLMode: "disable", + TimeZone: "UTC", + }, + } +} + +// getDefaultSQLitePath returns the default SQLite database path +func getDefaultSQLitePath() string { + if IsDebug() { + return "db/x-ui.db" + } + return "/etc/x-ui/x-ui.db" +} + +// ValidateConfig validates the database configuration +func (c *DatabaseConfig) ValidateConfig() error { + switch c.Type { + case DatabaseTypeSQLite: + if c.SQLite.Path == "" { + return fmt.Errorf("SQLite path cannot be empty") + } + case DatabaseTypePostgreSQL: + if c.Postgres.Host == "" { + return fmt.Errorf("PostgreSQL host cannot be empty") + } + if c.Postgres.Database == "" { + return fmt.Errorf("PostgreSQL database name cannot be empty") + } + if c.Postgres.Username == "" { + return fmt.Errorf("PostgreSQL username cannot be empty") + } + if c.Postgres.Port <= 0 || c.Postgres.Port > 65535 { + return fmt.Errorf("PostgreSQL port must be between 1 and 65535") + } + default: + return fmt.Errorf("unsupported database type: %s", c.Type) + } + return nil +} + +// IsPostgreSQL returns true if the database type is PostgreSQL +func (c *DatabaseConfig) IsPostgreSQL() bool { + return c.Type == DatabaseTypePostgreSQL +} + +// IsSQLite returns true if the database type is SQLite +func (c *DatabaseConfig) IsSQLite() bool { + return c.Type == DatabaseTypeSQLite +} + +// EnsureDirectoryExists ensures the directory for SQLite database exists +func (c *DatabaseConfig) EnsureDirectoryExists() error { + if c.Type == DatabaseTypeSQLite { + dir := filepath.Dir(c.SQLite.Path) + return os.MkdirAll(dir, 0755) + } + return nil +} diff --git a/database/db.go b/database/db.go index c72d28cf..674ae2fa 100644 --- a/database/db.go +++ b/database/db.go @@ -1,19 +1,22 @@ package database import ( + "bufio" "bytes" + "fmt" "io" - "io/fs" "log" "os" - "path" "slices" + "strconv" + "strings" "x-ui/config" "x-ui/database/model" "x-ui/util/crypto" "x-ui/xray" + "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" @@ -113,15 +116,109 @@ func isTableEmpty(tableName string) (bool, error) { return count == 0, err } +// loadEnvFile loads environment variables from a file +func loadEnvFile(filename string) error { + if _, err := os.Stat(filename); os.IsNotExist(err) { + return nil // File doesn't exist, not an error + } + + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + os.Setenv(key, value) + } + } + + return scanner.Err() +} + +// getDatabaseConfig retrieves database configuration from settings +func getDatabaseConfig() (*config.DatabaseConfig, error) { + // Load environment variables from file if it exists + if err := loadEnvFile("/etc/x-ui/db.env"); err != nil { + log.Printf("Warning: Could not load database environment file: %v", err) + } + + // Try to get configuration from settings + // This is a simplified version - in real implementation you'd get this from SettingService + dbConfig := config.GetDefaultDatabaseConfig() + + // Load configuration from environment variables + if dbType := os.Getenv("DB_TYPE"); dbType != "" { + dbConfig.Type = config.DatabaseType(dbType) + } + + if dbConfig.Type == config.DatabaseTypePostgreSQL { + if host := os.Getenv("DB_HOST"); host != "" { + dbConfig.Postgres.Host = host + } + if port := os.Getenv("DB_PORT"); port != "" { + if p, err := strconv.Atoi(port); err == nil { + dbConfig.Postgres.Port = p + } + } + if database := os.Getenv("DB_NAME"); database != "" { + dbConfig.Postgres.Database = database + } + if username := os.Getenv("DB_USER"); username != "" { + dbConfig.Postgres.Username = username + } + if password := os.Getenv("DB_PASSWORD"); password != "" { + dbConfig.Postgres.Password = password + } + if sslMode := os.Getenv("DB_SSLMODE"); sslMode != "" { + dbConfig.Postgres.SSLMode = sslMode + } + if timeZone := os.Getenv("DB_TIMEZONE"); timeZone != "" { + dbConfig.Postgres.TimeZone = timeZone + } + } + + return dbConfig, nil +} + func InitDB(dbPath string) error { - dir := path.Dir(dbPath) - err := os.MkdirAll(dir, fs.ModePerm) + // Try to get configuration from environment file first + dbConfig, err := getDatabaseConfig() if err != nil { return err } - var gormLogger logger.Interface + // If still using SQLite and dbPath is provided, use it + if dbConfig.Type == config.DatabaseTypeSQLite && dbPath != "" { + dbConfig.SQLite.Path = dbPath + } + return InitDBWithConfig(dbConfig) +} + +// InitDBWithConfig initializes database with provided configuration +func InitDBWithConfig(dbConfig *config.DatabaseConfig) error { + // Validate configuration + if err := dbConfig.ValidateConfig(); err != nil { + return err + } + + // Ensure directory exists for SQLite + if err := dbConfig.EnsureDirectoryExists(); err != nil { + return err + } + + var gormLogger logger.Interface if config.IsDebug() { gormLogger = logger.Default } else { @@ -131,7 +228,18 @@ func InitDB(dbPath string) error { c := &gorm.Config{ Logger: gormLogger, } - db, err = gorm.Open(sqlite.Open(dbPath), c) + + // Open database connection based on type + var err error + switch dbConfig.Type { + case config.DatabaseTypeSQLite: + db, err = gorm.Open(sqlite.Open(dbConfig.GetDSN()), c) + case config.DatabaseTypePostgreSQL: + db, err = gorm.Open(postgres.Open(dbConfig.GetDSN()), c) + default: + return fmt.Errorf("unsupported database type: %s", dbConfig.Type) + } + if err != nil { return err } @@ -141,6 +249,9 @@ func InitDB(dbPath string) error { } isUsersEmpty, err := isTableEmpty("users") + if err != nil { + return err + } if err := initUser(); err != nil { return err @@ -148,6 +259,50 @@ func InitDB(dbPath string) error { return runSeeders(isUsersEmpty) } +// TestDatabaseConnection tests database connection with provided configuration +func TestDatabaseConnection(dbConfig *config.DatabaseConfig) error { + // Validate configuration + if err := dbConfig.ValidateConfig(); err != nil { + return err + } + + var gormLogger logger.Interface + if config.IsDebug() { + gormLogger = logger.Default + } else { + gormLogger = logger.Discard + } + + c := &gorm.Config{ + Logger: gormLogger, + } + + // Test database connection based on type + var testDB *gorm.DB + var err error + switch dbConfig.Type { + case config.DatabaseTypeSQLite: + testDB, err = gorm.Open(sqlite.Open(dbConfig.GetDSN()), c) + case config.DatabaseTypePostgreSQL: + testDB, err = gorm.Open(postgres.Open(dbConfig.GetDSN()), c) + default: + return fmt.Errorf("unsupported database type: %s", dbConfig.Type) + } + + if err != nil { + return err + } + + // Test the connection + sqlDB, err := testDB.DB() + if err != nil { + return err + } + defer sqlDB.Close() + + return sqlDB.Ping() +} + func CloseDB() error { if db != nil { sqlDB, err := db.DB() diff --git a/docker-compose.postgresql.yml b/docker-compose.postgresql.yml new file mode 100644 index 00000000..0fb89f46 --- /dev/null +++ b/docker-compose.postgresql.yml @@ -0,0 +1,122 @@ +version: '3.8' + +networks: + x-ui-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 + +volumes: + postgres_data: + driver: local + x-ui-data: + driver: local + x-ui-cert: + driver: local + +services: + postgres: + image: postgres:16-alpine + container_name: x-ui-postgres + hostname: postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${DB_NAME:-x_ui} + POSTGRES_USER: ${DB_USER:-x_ui} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init-postgres.sql:/docker-entrypoint-initdb.d/init.sql:ro + networks: + x-ui-network: + ipv4_address: 172.20.0.2 + ports: + - "127.0.0.1:5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-x_ui} -d ${DB_NAME:-x_ui}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + command: > + postgres + -c max_connections=200 + -c shared_buffers=256MB + -c effective_cache_size=1GB + -c maintenance_work_mem=64MB + -c checkpoint_completion_target=0.9 + -c wal_buffers=16MB + -c default_statistics_target=100 + -c random_page_cost=1.1 + -c effective_io_concurrency=200 + -c work_mem=4MB + -c min_wal_size=1GB + -c max_wal_size=4GB + + 3x-ui: + image: ghcr.io/mhsanaei/3x-ui:latest + container_name: 3x-ui + hostname: 3x-ui + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + XRAY_VMESS_AEAD_FORCED: "false" + XUI_ENABLE_FAIL2BAN: "true" + DB_TYPE: "postgres" + DB_HOST: "postgres" + DB_PORT: "5432" + DB_NAME: ${DB_NAME:-x_ui} + DB_USER: ${DB_USER:-x_ui} + DB_PASSWORD: ${DB_PASSWORD} + DB_SSLMODE: ${DB_SSLMODE:-disable} + DB_TIMEZONE: ${DB_TIMEZONE:-UTC} + volumes: + - x-ui-data:/etc/x-ui/ + - x-ui-cert:/root/cert/ + networks: + x-ui-network: + ipv4_address: 172.20.0.3 + ports: + - "${XUI_PORT:-2053}:2053" + - "${XUI_SUB_PORT:-2096}:2096" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:2053/login"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv6.conf.all.forwarding=1 + cap_add: + - NET_ADMIN + security_opt: + - no-new-privileges:true + + # Optional: PostgreSQL Admin Interface + pgadmin: + image: dpage/pgadmin4:latest + container_name: x-ui-pgadmin + hostname: pgadmin + restart: unless-stopped + profiles: + - admin + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@example.com} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin} + PGADMIN_CONFIG_SERVER_MODE: 'False' + volumes: + - ./pgadmin_servers.json:/pgadmin4/servers.json:ro + networks: + x-ui-network: + ipv4_address: 172.20.0.4 + ports: + - "127.0.0.1:5050:80" + depends_on: + postgres: + condition: service_healthy \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e27a735e..260939d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,57 @@ +version: '3.8' + +networks: + default: + name: x-ui-network + driver: bridge + +volumes: + x-ui-data: + driver: local + x-ui-cert: + driver: local + services: 3x-ui: image: ghcr.io/mhsanaei/3x-ui:latest container_name: 3x-ui - hostname: yourhostname - volumes: - - $PWD/db/:/etc/x-ui/ - - $PWD/cert/:/root/cert/ - environment: - XRAY_VMESS_AEAD_FORCED: "false" - XUI_ENABLE_FAIL2BAN: "true" - tty: true - network_mode: host + hostname: ${HOSTNAME:-3x-ui} restart: unless-stopped + environment: + XRAY_VMESS_AEAD_FORCED: ${XRAY_VMESS_AEAD_FORCED:-false} + XUI_ENABLE_FAIL2BAN: ${XUI_ENABLE_FAIL2BAN:-true} + # Database configuration (defaults to SQLite) + DB_TYPE: ${DB_TYPE:-sqlite} + DB_HOST: ${DB_HOST:-localhost} + DB_PORT: ${DB_PORT:-5432} + DB_NAME: ${DB_NAME:-x_ui} + DB_USER: ${DB_USER:-x_ui} + DB_PASSWORD: ${DB_PASSWORD} + DB_SSLMODE: ${DB_SSLMODE:-disable} + DB_TIMEZONE: ${DB_TIMEZONE:-UTC} + volumes: + # Use named volumes for better data management + - x-ui-data:/etc/x-ui/ + - x-ui-cert:/root/cert/ + # Fallback to bind mounts for compatibility + # - $PWD/db/:/etc/x-ui/ + # - $PWD/cert/:/root/cert/ + ports: + - "${XUI_PORT:-2053}:2053" + - "${XUI_SUB_PORT:-2096}:2096" + # Use host network for simple setups (comment out ports above if using this) + # network_mode: host + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:2053/login"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv6.conf.all.forwarding=1 + cap_add: + - NET_ADMIN + security_opt: + - no-new-privileges:true + tty: true diff --git a/env.example b/env.example new file mode 100644 index 00000000..3b72257c --- /dev/null +++ b/env.example @@ -0,0 +1,67 @@ +# ============================================================================= +# 3X-UI Docker Environment Configuration +# ============================================================================= +# Copy this file to .env and modify the values according to your setup + +# ============================================================================= +# General Configuration +# ============================================================================= +HOSTNAME=3x-ui +XUI_PORT=2053 +XUI_SUB_PORT=2096 + +# ============================================================================= +# X-UI Configuration +# ============================================================================= +XRAY_VMESS_AEAD_FORCED=false +XUI_ENABLE_FAIL2BAN=true + +# ============================================================================= +# Database Configuration +# ============================================================================= +# Database type: sqlite or postgres +DB_TYPE=sqlite + +# PostgreSQL Configuration (only needed if DB_TYPE=postgres) +DB_HOST=postgres +DB_PORT=5432 +DB_NAME=x_ui +DB_USER=x_ui +# IMPORTANT: Change this password for production! +DB_PASSWORD=your_secure_password_here +DB_SSLMODE=disable +DB_TIMEZONE=UTC + +# ============================================================================= +# PostgreSQL Admin (PgAdmin) Configuration +# ============================================================================= +# Only used with docker-compose.postgresql.yml and --profile admin +PGADMIN_EMAIL=admin@example.com +PGADMIN_PASSWORD=admin_password + +# ============================================================================= +# Example configurations for different setups: +# ============================================================================= + +# For SQLite (default, simple setup): +# DB_TYPE=sqlite + +# For PostgreSQL (production setup): +# DB_TYPE=postgres +# DB_HOST=postgres +# DB_PORT=5432 +# DB_NAME=x_ui +# DB_USER=x_ui +# DB_PASSWORD=your_very_secure_password_123 +# DB_SSLMODE=require +# DB_TIMEZONE=UTC + +# For external PostgreSQL: +# DB_TYPE=postgres +# DB_HOST=your-postgres-server.com +# DB_PORT=5432 +# DB_NAME=x_ui_production +# DB_USER=x_ui_user +# DB_PASSWORD=your_external_db_password +# DB_SSLMODE=require +# DB_TIMEZONE=America/New_York \ No newline at end of file diff --git a/go.mod b/go.mod index 1f545c64..6e74cec9 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( golang.org/x/crypto v0.38.0 golang.org/x/text v0.25.0 google.golang.org/grpc v1.72.1 + gorm.io/driver/postgres v1.5.9 gorm.io/driver/sqlite v1.5.7 gorm.io/gorm v1.25.12 ) @@ -49,6 +50,10 @@ require ( github.com/gorilla/sessions v1.4.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grbit/go-json v0.11.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/go.sum b/go.sum index c6ae9c41..32f89025 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,14 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc= github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -261,6 +269,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= +gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= diff --git a/init-postgres.sql b/init-postgres.sql new file mode 100644 index 00000000..20df5568 --- /dev/null +++ b/init-postgres.sql @@ -0,0 +1,46 @@ +-- ============================================================================= +-- 3X-UI PostgreSQL Database Initialization Script +-- ============================================================================= +-- This script is automatically executed when PostgreSQL container starts +-- for the first time + +-- Create extensions if needed +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pg_stat_statements"; + +-- Set timezone +SET timezone = 'UTC'; + +-- Create additional indexes for better performance (will be created by GORM if needed) +-- These are just examples, actual tables will be created by the application + +-- Grant additional permissions to the user +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO x_ui; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO x_ui; +GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO x_ui; + +-- Set default privileges for future objects +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO x_ui; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO x_ui; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO x_ui; + +-- Optimize PostgreSQL settings for 3X-UI workload +-- These settings are also configured in docker-compose.postgresql.yml +ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements'; +ALTER SYSTEM SET log_statement = 'mod'; +ALTER SYSTEM SET log_min_duration_statement = 1000; +ALTER SYSTEM SET log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '; + +-- Create a simple health check function +CREATE OR REPLACE FUNCTION health_check() +RETURNS TEXT AS $$ +BEGIN + RETURN 'OK - ' || current_timestamp; +END; +$$ LANGUAGE plpgsql; + +-- Log initialization completion +DO $$ +BEGIN + RAISE NOTICE '3X-UI PostgreSQL database initialized successfully at %', current_timestamp; +END $$; \ No newline at end of file diff --git a/install.sh b/install.sh index 0398285d..6431ea51 100644 --- a/install.sh +++ b/install.sh @@ -81,6 +81,145 @@ gen_random_string() { echo "$random_string" } +install_postgresql() { + echo -e "${green}Installing PostgreSQL...${plain}" + + case "${release}" in + ubuntu | debian | armbian) + apt-get update + apt-get install -y postgresql postgresql-contrib + ;; + centos | almalinux | rocky | ol) + yum install -y postgresql-server postgresql-contrib + postgresql-setup initdb + ;; + fedora | amzn | virtuozzo) + dnf install -y postgresql-server postgresql-contrib + postgresql-setup --initdb + ;; + arch | manjaro | parch) + pacman -S --noconfirm postgresql + sudo -u postgres initdb -D /var/lib/postgres/data + ;; + opensuse-tumbleweed) + zypper install -y postgresql-server postgresql-contrib + ;; + *) + echo -e "${red}Unsupported OS for PostgreSQL installation${plain}" + return 1 + ;; + esac + + # Start and enable PostgreSQL service + systemctl start postgresql + systemctl enable postgresql + + if ! systemctl is-active --quiet postgresql; then + echo -e "${red}Failed to start PostgreSQL service${plain}" + return 1 + fi + + echo -e "${green}PostgreSQL installed and started successfully${plain}" + return 0 +} + +setup_postgresql_for_xui() { + echo -e "${green}Setting up PostgreSQL for x-ui...${plain}" + + local db_name="x_ui" + local db_user="x_ui" + local db_password=$(gen_random_string 16) + + # Create database and user + sudo -u postgres psql -c "CREATE DATABASE ${db_name};" 2>/dev/null || true + sudo -u postgres psql -c "CREATE USER ${db_user} WITH PASSWORD '${db_password}';" 2>/dev/null || true + sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${db_name} TO ${db_user};" 2>/dev/null || true + sudo -u postgres psql -c "ALTER USER ${db_user} CREATEDB;" 2>/dev/null || true + + # Create environment file for x-ui + cat > /etc/x-ui/db.env << EOF +DB_TYPE=postgres +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=${db_name} +DB_USER=${db_user} +DB_PASSWORD=${db_password} +DB_SSLMODE=disable +DB_TIMEZONE=UTC +EOF + + chmod 600 /etc/x-ui/db.env + + echo -e "${green}PostgreSQL setup completed${plain}" + echo -e "${yellow}Database: ${db_name}${plain}" + echo -e "${yellow}User: ${db_user}${plain}" + echo -e "${yellow}Password: ${db_password}${plain}" + echo -e "${yellow}Configuration saved to: /etc/x-ui/db.env${plain}" + + return 0 +} + +database_setup() { + echo -e "${green}Database Setup${plain}" + echo -e "Choose your database type:" + echo -e "${green}1.${plain} SQLite (Default, recommended for most users)" + echo -e "${green}2.${plain} PostgreSQL (For high-load environments)" + + read -rp "Enter your choice [1-2, default: 1]: " db_choice + + case "${db_choice}" in + 2) + echo -e "${yellow}Setting up PostgreSQL...${plain}" + + # Check if PostgreSQL is already installed + if command -v psql &> /dev/null && systemctl is-active --quiet postgresql; then + echo -e "${yellow}PostgreSQL is already installed and running${plain}" + read -rp "Do you want to use existing PostgreSQL installation? [y/n, default: y]: " use_existing + if [[ "${use_existing}" == "n" || "${use_existing}" == "N" ]]; then + return 1 + fi + else + read -rp "PostgreSQL will be installed. Continue? [y/n, default: y]: " install_confirm + if [[ "${install_confirm}" == "n" || "${install_confirm}" == "N" ]]; then + echo -e "${yellow}Using SQLite instead${plain}" + return 0 + fi + + if ! install_postgresql; then + echo -e "${red}Failed to install PostgreSQL. Using SQLite instead${plain}" + return 0 + fi + fi + + if ! setup_postgresql_for_xui; then + echo -e "${red}Failed to setup PostgreSQL. Using SQLite instead${plain}" + return 0 + fi + + echo -e "${green}PostgreSQL setup completed successfully${plain}" + ;; + 1|"") + echo -e "${green}Using SQLite database (default)${plain}" + # Create empty env file to indicate SQLite usage + mkdir -p /etc/x-ui + cat > /etc/x-ui/db.env << EOF +DB_TYPE=sqlite +EOF + chmod 600 /etc/x-ui/db.env + ;; + *) + echo -e "${yellow}Invalid choice. Using SQLite (default)${plain}" + mkdir -p /etc/x-ui + cat > /etc/x-ui/db.env << EOF +DB_TYPE=sqlite +EOF + chmod 600 /etc/x-ui/db.env + ;; + esac + + return 0 +} + config_after_install() { local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}') local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') @@ -193,6 +332,10 @@ install_x-ui() { wget -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh chmod +x /usr/local/x-ui/x-ui.sh chmod +x /usr/bin/x-ui + + # Setup database before configuration + database_setup + config_after_install systemctl daemon-reload @@ -200,24 +343,25 @@ install_x-ui() { systemctl start x-ui echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..." echo -e "" - echo -e "โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ${blue}x-ui control menu usages (subcommands):${plain} โ”‚ -โ”‚ โ”‚ -โ”‚ ${blue}x-ui${plain} - Admin Management Script โ”‚ -โ”‚ ${blue}x-ui start${plain} - Start โ”‚ -โ”‚ ${blue}x-ui stop${plain} - Stop โ”‚ -โ”‚ ${blue}x-ui restart${plain} - Restart โ”‚ -โ”‚ ${blue}x-ui status${plain} - Current Status โ”‚ -โ”‚ ${blue}x-ui settings${plain} - Current Settings โ”‚ -โ”‚ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup โ”‚ -โ”‚ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup โ”‚ -โ”‚ ${blue}x-ui log${plain} - Check logs โ”‚ -โ”‚ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs โ”‚ -โ”‚ ${blue}x-ui update${plain} - Update โ”‚ -โ”‚ ${blue}x-ui legacy${plain} - legacy version โ”‚ -โ”‚ ${blue}x-ui install${plain} - Install โ”‚ -โ”‚ ${blue}x-ui uninstall${plain} - Uninstall โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜" + echo -e "โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”" + echo -e "โ”‚ ${blue}x-ui control menu usages (subcommands):${plain} โ”‚" + echo -e "โ”‚ โ”‚" + echo -e "โ”‚ ${blue}x-ui${plain} - Admin Management Script โ”‚" + echo -e "โ”‚ ${blue}x-ui start${plain} - Start โ”‚" + echo -e "โ”‚ ${blue}x-ui stop${plain} - Stop โ”‚" + echo -e "โ”‚ ${blue}x-ui restart${plain} - Restart โ”‚" + echo -e "โ”‚ ${blue}x-ui status${plain} - Current Status โ”‚" + echo -e "โ”‚ ${blue}x-ui settings${plain} - Current Settings โ”‚" + echo -e "โ”‚ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup โ”‚" + echo -e "โ”‚ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup โ”‚" + echo -e "โ”‚ ${blue}x-ui log${plain} - Check logs โ”‚" + echo -e "โ”‚ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs โ”‚" + echo -e "โ”‚ ${blue}x-ui update${plain} - Update โ”‚" + echo -e "โ”‚ ${blue}x-ui legacy${plain} - legacy version โ”‚" + echo -e "โ”‚ ${blue}x-ui install${plain} - Install โ”‚" + echo -e "โ”‚ ${blue}x-ui uninstall${plain} - Uninstall โ”‚" + echo -e "โ”‚ ${blue}x-ui database${plain} - Database Management โ”‚" + echo -e "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜" } echo -e "${green}Running...${plain}" diff --git a/pgadmin_servers.json b/pgadmin_servers.json new file mode 100644 index 00000000..f4779255 --- /dev/null +++ b/pgadmin_servers.json @@ -0,0 +1,23 @@ +{ + "Servers": { + "1": { + "Name": "3X-UI PostgreSQL", + "Group": "Servers", + "Host": "postgres", + "Port": 5432, + "MaintenanceDB": "x_ui", + "Username": "x_ui", + "PassFile": "/tmp/pgpassfile", + "SSLMode": "disable", + "SSLCert": "/.postgresql/postgresql.crt", + "SSLKey": "/.postgresql/postgresql.key", + "SSLCompression": 0, + "Timeout": 10, + "UseSSHTunnel": 0, + "TunnelHost": "", + "TunnelPort": "22", + "TunnelUsername": "", + "TunnelAuthentication": 0 + } + } +} \ No newline at end of file diff --git a/wait-for-postgres.sh b/wait-for-postgres.sh new file mode 100644 index 00000000..7dedf111 --- /dev/null +++ b/wait-for-postgres.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +host="$1" +port="$2" +user="$3" +database="$4" +shift 4 +cmd="$@" + +until PGPASSWORD="$DB_PASSWORD" psql -h "$host" -p "$port" -U "$user" -d "$database" -c '\q'; do + >&2 echo "PostgreSQL is unavailable - sleeping" + sleep 1 +done + +>&2 echo "PostgreSQL is up - executing command" +exec $cmd \ No newline at end of file diff --git a/web/assets/js/model/setting.js b/web/assets/js/model/setting.js index 89ea171f..2d5af3b2 100644 --- a/web/assets/js/model/setting.js +++ b/web/assets/js/model/setting.js @@ -42,11 +42,18 @@ class AllSetting { this.subShowInfo = true; this.subURI = ""; this.subJsonURI = ""; - this.subJsonFragment = ""; - this.subJsonNoises = ""; - this.subJsonMux = ""; - this.subJsonRules = ""; - + this.subJsonFragment = ""; + this.subJsonNoises = ""; + this.subJsonMux = ""; + this.subJsonRules = ""; + this.dbType = "sqlite"; + this.dbHost = "localhost"; + this.dbPort = 5432; + this.dbName = "x_ui"; + this.dbUser = "x_ui"; + this.dbPassword = ""; + this.dbSSLMode = "disable"; + this.dbTimeZone = "UTC"; this.timeLocation = "Local"; if (data == null) { diff --git a/web/controller/setting.go b/web/controller/setting.go index ddd9f55a..479d4ffe 100644 --- a/web/controller/setting.go +++ b/web/controller/setting.go @@ -4,6 +4,7 @@ import ( "errors" "time" + "x-ui/database" "x-ui/util/crypto" "x-ui/web/entity" "x-ui/web/service" @@ -40,6 +41,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) { g.POST("/updateUser", a.updateUser) g.POST("/restartPanel", a.restartPanel) g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) + g.POST("/testDatabaseConnection", a.testDatabaseConnection) } func (a *SettingController) getAllSetting(c *gin.Context) { @@ -109,3 +111,19 @@ func (a *SettingController) getDefaultXrayConfig(c *gin.Context) { } jsonObj(c, defaultJsonConfig, nil) } + +func (a *SettingController) testDatabaseConnection(c *gin.Context) { + dbConfig, err := a.settingService.GetDatabaseConfig() + if err != nil { + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.testDatabaseConnection"), err) + return + } + + err = database.TestDatabaseConnection(dbConfig) + if err != nil { + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.testDatabaseConnection"), err) + return + } + + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.testDatabaseConnectionSuccess"), nil) +} diff --git a/web/entity/entity.go b/web/entity/entity.go index 543b80e0..6cb5650b 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -38,8 +38,8 @@ type AllSetting struct { TgCpu int `json:"tgCpu" form:"tgCpu"` TgLang string `json:"tgLang" form:"tgLang"` TimeLocation string `json:"timeLocation" form:"timeLocation"` - TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` - TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` + TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"` + TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"` SubEnable bool `json:"subEnable" form:"subEnable"` SubTitle string `json:"subTitle" form:"subTitle"` SubListen string `json:"subListen" form:"subListen"` @@ -61,6 +61,14 @@ type AllSetting struct { SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` Datepicker string `json:"datepicker" form:"datepicker"` + DbType string `json:"dbType" form:"dbType"` + DbHost string `json:"dbHost" form:"dbHost"` + DbPort int `json:"dbPort" form:"dbPort"` + DbName string `json:"dbName" form:"dbName"` + DbUser string `json:"dbUser" form:"dbUser"` + DbPassword string `json:"dbPassword" form:"dbPassword"` + DbSSLMode string `json:"dbSSLMode" form:"dbSSLMode"` + DbTimeZone string `json:"dbTimeZone" form:"dbTimeZone"` } func (s *AllSetting) CheckValid() error { diff --git a/web/html/settings.html b/web/html/settings.html index dec40de7..f34b23a2 100644 --- a/web/html/settings.html +++ b/web/html/settings.html @@ -115,6 +115,9 @@ {{ template "settings/panel/subscription/json" . }} + + {{ template "settings/panel/database" . }} + diff --git a/web/html/settings/panel/database.html b/web/html/settings/panel/database.html new file mode 100644 index 00000000..4d6211b6 --- /dev/null +++ b/web/html/settings/panel/database.html @@ -0,0 +1,86 @@ +{{define "settings/panel/database"}} + + + + + + + + + + + + + + + +{{end}} \ No newline at end of file diff --git a/web/service/setting.go b/web/service/setting.go index 62d66c11..c581bc7f 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "x-ui/config" "x-ui/database" "x-ui/database/model" "x-ui/logger" @@ -72,6 +73,14 @@ var defaultValueMap = map[string]string{ "warp": "", "externalTrafficInformEnable": "false", "externalTrafficInformURI": "", + "dbType": "sqlite", + "dbHost": "localhost", + "dbPort": "5432", + "dbName": "x_ui", + "dbUser": "x_ui", + "dbPassword": "", + "dbSSLMode": "disable", + "dbTimeZone": "UTC", } type SettingService struct{} @@ -519,6 +528,131 @@ func (s *SettingService) SetExternalTrafficInformURI(InformURI string) error { return s.setString("externalTrafficInformURI", InformURI) } +// Database configuration methods +func (s *SettingService) GetDbType() (string, error) { + return s.getString("dbType") +} + +func (s *SettingService) SetDbType(dbType string) error { + return s.setString("dbType", dbType) +} + +func (s *SettingService) GetDbHost() (string, error) { + return s.getString("dbHost") +} + +func (s *SettingService) SetDbHost(host string) error { + return s.setString("dbHost", host) +} + +func (s *SettingService) GetDbPort() (int, error) { + return s.getInt("dbPort") +} + +func (s *SettingService) SetDbPort(port int) error { + return s.setInt("dbPort", port) +} + +func (s *SettingService) GetDbName() (string, error) { + return s.getString("dbName") +} + +func (s *SettingService) SetDbName(name string) error { + return s.setString("dbName", name) +} + +func (s *SettingService) GetDbUser() (string, error) { + return s.getString("dbUser") +} + +func (s *SettingService) SetDbUser(user string) error { + return s.setString("dbUser", user) +} + +func (s *SettingService) GetDbPassword() (string, error) { + return s.getString("dbPassword") +} + +func (s *SettingService) SetDbPassword(password string) error { + return s.setString("dbPassword", password) +} + +func (s *SettingService) GetDbSSLMode() (string, error) { + return s.getString("dbSSLMode") +} + +func (s *SettingService) SetDbSSLMode(sslMode string) error { + return s.setString("dbSSLMode", sslMode) +} + +func (s *SettingService) GetDbTimeZone() (string, error) { + return s.getString("dbTimeZone") +} + +func (s *SettingService) SetDbTimeZone(timeZone string) error { + return s.setString("dbTimeZone", timeZone) +} + +// GetDatabaseConfig returns database configuration from settings +func (s *SettingService) GetDatabaseConfig() (*config.DatabaseConfig, error) { + dbType, err := s.GetDbType() + if err != nil { + return nil, err + } + + dbConfig := &config.DatabaseConfig{ + Type: config.DatabaseType(dbType), + } + + if dbConfig.Type == config.DatabaseTypePostgreSQL { + host, err := s.GetDbHost() + if err != nil { + return nil, err + } + port, err := s.GetDbPort() + if err != nil { + return nil, err + } + name, err := s.GetDbName() + if err != nil { + return nil, err + } + user, err := s.GetDbUser() + if err != nil { + return nil, err + } + password, err := s.GetDbPassword() + if err != nil { + return nil, err + } + sslMode, err := s.GetDbSSLMode() + if err != nil { + return nil, err + } + timeZone, err := s.GetDbTimeZone() + if err != nil { + return nil, err + } + + dbConfig.Postgres = config.PostgresConfig{ + Host: host, + Port: port, + Database: name, + Username: user, + Password: password, + SSLMode: sslMode, + TimeZone: timeZone, + } + } else { + // For SQLite, use default path + dbConfig.SQLite = config.SQLiteConfig{ + Path: config.GetDefaultDatabaseConfig().SQLite.Path, + } + } + + return dbConfig, nil +} + func (s *SettingService) GetIpLimitEnable() (bool, error) { accessLogPath, err := xray.GetAccessLogPath() if err != nil { diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index ec7507f4..c5e12252 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -293,11 +293,11 @@ "panelPort" = "Listen Port" "panelPortDesc" = "The port number for the web panel. (must be an unused port)" "publicKeyPath" = "Public Key Path" -"publicKeyPathDesc" = "The public key file path for the web panel. (begins with โ€˜/โ€˜)" +"publicKeyPathDesc" = "The public key file path for the web panel. (begins with '//')" "privateKeyPath" = "Private Key Path" -"privateKeyPathDesc" = "The private key file path for the web panel. (begins with โ€˜/โ€˜)" +"privateKeyPathDesc" = "The private key file path for the web panel. (begins with '//')" "panelUrlPath" = "URI Path" -"panelUrlPathDesc" = "The URI path for the web panel. (begins with โ€˜/โ€˜ and concludes with โ€˜/โ€˜)" +"panelUrlPathDesc" = "The URI path for the web panel. (begins with '//' and concludes with '//')" "pageSize" = "Pagination Size" "pageSizeDesc" = "Define page size for inbounds table. (0 = disable)" "remarkModel" = "Remark Model & Separation Character" @@ -345,11 +345,11 @@ "subPort" = "Listen Port" "subPortDesc" = "The port number for the subscription service. (must be an unused port)" "subCertPath" = "Public Key Path" -"subCertPathDesc" = "The public key file path for the subscription service. (begins with โ€˜/โ€˜)" +"subCertPathDesc" = "The public key file path for the subscription service. (begins with '//')" "subKeyPath" = "Private Key Path" -"subKeyPathDesc" = "The private key file path for the subscription service. (begins with โ€˜/โ€˜)" +"subKeyPathDesc" = "The private key file path for the subscription service. (begins with '//')" "subPath" = "URI Path" -"subPathDesc" = "The URI path for the subscription service. (begins with โ€˜/โ€˜ and concludes with โ€˜/โ€˜)" +"subPathDesc" = "The URI path for the subscription service. (begins with '//' and concludes with '//')" "subDomain" = "Listen Domain" "subDomainDesc" = "The domain name for the subscription service. (leave blank to listen on all domains and IPs)" "subUpdates" = "Update Intervals" @@ -551,6 +551,28 @@ "userPassMustBeNotEmpty" = "The new username and password is empty" "getOutboundTrafficError" = "Error getting traffics" "resetOutboundTrafficError" = "Error in reset outbound traffics" +"testDatabaseConnection" = "Failed to test database connection" +"testDatabaseConnectionSuccess" = "Database connection test successful" + +[pages.settings.database] +"databaseSettings" = "Database Settings" +"databaseType" = "Database Type" +"databaseTypeDesc" = "Choose between SQLite (default) or PostgreSQL database" +"databaseHost" = "Database Host" +"databaseHostDesc" = "PostgreSQL server hostname or IP address" +"databasePort" = "Database Port" +"databasePortDesc" = "PostgreSQL server port (default: 5432)" +"databaseName" = "Database Name" +"databaseNameDesc" = "Name of the PostgreSQL database" +"databaseUser" = "Database User" +"databaseUserDesc" = "PostgreSQL username for authentication" +"databasePassword" = "Database Password" +"databasePasswordDesc" = "PostgreSQL password for authentication" +"databaseSSLMode" = "SSL Mode" +"databaseSSLModeDesc" = "PostgreSQL SSL connection mode" +"databaseTimeZone" = "Time Zone" +"databaseTimeZoneDesc" = "Database timezone (default: UTC)" +"databaseWarning" = "Warning: Changing database settings requires panel restart to take effect" [tgbot] "keyboardClosed" = "โŒ Custom keyboard closed!" diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml index 376bd115..2f2ea87c 100644 --- a/web/translation/translate.es_ES.toml +++ b/web/translation/translate.es_ES.toml @@ -554,6 +554,28 @@ "userPassMustBeNotEmpty" = "El nuevo nombre de usuario y la nueva contraseรฑa no pueden estar vacรญos" "getOutboundTrafficError" = "Error al obtener el trรกfico saliente" "resetOutboundTrafficError" = "Error al reiniciar el trรกfico saliente" +"testDatabaseConnection" = "Error al probar la conexiรณn de la base de datos" +"testDatabaseConnectionSuccess" = "Prueba de conexiรณn de base de datos exitosa" + +[pages.settings.database] +"databaseSettings" = "Configuraciรณn de Base de Datos" +"databaseType" = "Tipo de Base de Datos" +"databaseTypeDesc" = "Elija entre SQLite (predeterminado) o PostgreSQL" +"databaseHost" = "Host de Base de Datos" +"databaseHostDesc" = "Nombre del host o direcciรณn IP del servidor PostgreSQL" +"databasePort" = "Puerto de Base de Datos" +"databasePortDesc" = "Puerto del servidor PostgreSQL (predeterminado: 5432)" +"databaseName" = "Nombre de Base de Datos" +"databaseNameDesc" = "Nombre de la base de datos PostgreSQL" +"databaseUser" = "Usuario de Base de Datos" +"databaseUserDesc" = "Nombre de usuario PostgreSQL para autenticaciรณn" +"databasePassword" = "Contraseรฑa de Base de Datos" +"databasePasswordDesc" = "Contraseรฑa PostgreSQL para autenticaciรณn" +"databaseSSLMode" = "Modo SSL" +"databaseSSLModeDesc" = "Modo de conexiรณn SSL de PostgreSQL" +"databaseTimeZone" = "Zona Horaria" +"databaseTimeZoneDesc" = "Zona horaria de la base de datos (predeterminado: UTC)" +"databaseWarning" = "Advertencia: Cambiar la configuraciรณn de la base de datos requiere reiniciar el panel" [tgbot] "keyboardClosed" = "โŒ ยกTeclado personalizado cerrado!" diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml index d9f163ee..578ca446 100644 --- a/web/translation/translate.zh_CN.toml +++ b/web/translation/translate.zh_CN.toml @@ -218,7 +218,7 @@ "IPLimitlogclear" = "ๆธ…้™คๆ—ฅๅฟ—" "setDefaultCert" = "ไปŽ้ขๆฟ่ฎพ็ฝฎ่ฏไนฆ" "telegramDesc" = "่ฏทๆไพ›Telegram่ŠๅคฉIDใ€‚๏ผˆๅœจๆœบๅ™จไบบไธญไฝฟ็”จ'/id'ๅ‘ฝไปค๏ผ‰ๆˆ–๏ผˆ@userinfobot" -"subscriptionDesc" = "่ฆๆ‰พๅˆฐไฝ ็š„่ฎข้˜… URL๏ผŒ่ฏทๅฏผ่ˆชๅˆฐโ€œ่ฏฆ็ป†ไฟกๆฏโ€ใ€‚ๆญคๅค–๏ผŒไฝ ๅฏไปฅไธบๅคšไธชๅฎขๆˆท็ซฏไฝฟ็”จ็›ธๅŒ็š„ๅ็งฐใ€‚" +"subscriptionDesc" = "่ฆๆ‰พๅˆฐไฝ ็š„่ฎข้˜… URL๏ผŒ่ฏทๅฏผ่ˆชๅˆฐ"่ฏฆ็ป†ไฟกๆฏ"ใ€‚ๆญคๅค–๏ผŒไฝ ๅฏไปฅไธบๅคšไธชๅฎขๆˆท็ซฏไฝฟ็”จ็›ธๅŒ็š„ๅ็งฐใ€‚" "info" = "ไฟกๆฏ" "same" = "็›ธๅŒ" "inboundData" = "ๅ…ฅ็ซ™ๆ•ฐๆฎ" @@ -554,6 +554,28 @@ "userPassMustBeNotEmpty" = "ๆ–ฐ็”จๆˆทๅๅ’Œๆ–ฐๅฏ†็ ไธ่ƒฝไธบ็ฉบ" "getOutboundTrafficError" = "่Žทๅ–ๅ‡บ็ซ™ๆต้‡้”™่ฏฏ" "resetOutboundTrafficError" = "้‡็ฝฎๅ‡บ็ซ™ๆต้‡้”™่ฏฏ" +"testDatabaseConnection" = "ๆ•ฐๆฎๅบ“่ฟžๆŽฅๆต‹่ฏ•ๅคฑ่ดฅ" +"testDatabaseConnectionSuccess" = "ๆ•ฐๆฎๅบ“่ฟžๆŽฅๆต‹่ฏ•ๆˆๅŠŸ" + +[pages.settings.database] +"databaseSettings" = "ๆ•ฐๆฎๅบ“่ฎพ็ฝฎ" +"databaseType" = "ๆ•ฐๆฎๅบ“็ฑปๅž‹" +"databaseTypeDesc" = "้€‰ๆ‹ฉ SQLite๏ผˆ้ป˜่ฎค๏ผ‰ๆˆ– PostgreSQL ๆ•ฐๆฎๅบ“" +"databaseHost" = "ๆ•ฐๆฎๅบ“ไธปๆœบ" +"databaseHostDesc" = "PostgreSQL ๆœๅŠกๅ™จไธปๆœบๅๆˆ– IP ๅœฐๅ€" +"databasePort" = "ๆ•ฐๆฎๅบ“็ซฏๅฃ" +"databasePortDesc" = "PostgreSQL ๆœๅŠกๅ™จ็ซฏๅฃ๏ผˆ้ป˜่ฎค๏ผš5432๏ผ‰" +"databaseName" = "ๆ•ฐๆฎๅบ“ๅ็งฐ" +"databaseNameDesc" = "PostgreSQL ๆ•ฐๆฎๅบ“ๅ็งฐ" +"databaseUser" = "ๆ•ฐๆฎๅบ“็”จๆˆท" +"databaseUserDesc" = "PostgreSQL ่ฎค่ฏ็”จๆˆทๅ" +"databasePassword" = "ๆ•ฐๆฎๅบ“ๅฏ†็ " +"databasePasswordDesc" = "PostgreSQL ่ฎค่ฏๅฏ†็ " +"databaseSSLMode" = "SSL ๆจกๅผ" +"databaseSSLModeDesc" = "PostgreSQL SSL ่ฟžๆŽฅๆจกๅผ" +"databaseTimeZone" = "ๆ—ถๅŒบ" +"databaseTimeZoneDesc" = "ๆ•ฐๆฎๅบ“ๆ—ถๅŒบ๏ผˆ้ป˜่ฎค๏ผšUTC๏ผ‰" +"databaseWarning" = "่ญฆๅ‘Š๏ผšๆ›ดๆ”นๆ•ฐๆฎๅบ“่ฎพ็ฝฎ้œ€่ฆ้‡ๅฏ้ขๆฟๆ‰่ƒฝ็”Ÿๆ•ˆ" [tgbot] "keyboardClosed" = "โŒ ่‡ชๅฎšไน‰้”ฎ็›˜ๅทฒๅ…ณ้—ญ๏ผ" diff --git a/x-ui-postgresql.service b/x-ui-postgresql.service new file mode 100644 index 00000000..c62c0c0d --- /dev/null +++ b/x-ui-postgresql.service @@ -0,0 +1,42 @@ +[Unit] +Description=PostgreSQL database server for 3x-ui +Documentation=man:postgres(1) +After=network.target +Wants=network.target +Before=x-ui.service + +[Service] +Type=notify +User=postgres +Group=postgres + +# PostgreSQL data directory +Environment=PGDATA=/var/lib/postgresql/data + +# PostgreSQL configuration +ExecStart=/usr/bin/postgres -D ${PGDATA} +ExecReload=/bin/kill -HUP $MAINPID + +# Process management +KillMode=mixed +KillSignal=SIGINT +TimeoutSec=0 + +# Restart policy +Restart=on-failure +RestartSec=5s + +# Security settings +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/var/lib/postgresql + +# Resource limits +LimitNOFILE=65536 +LimitNPROC=4096 + +[Install] +WantedBy=multi-user.target +WantedBy=x-ui.service \ No newline at end of file diff --git a/x-ui.sh b/x-ui.sh index 88e5a7bb..10d40165 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -1620,6 +1620,319 @@ remove_iplimit() { esac } +install_postgresql() { + echo -e "${green}Installing PostgreSQL...${plain}" + + case "${release}" in + ubuntu | debian | armbian) + apt-get update + apt-get install -y postgresql postgresql-contrib + ;; + centos | almalinux | rocky | ol) + yum install -y postgresql-server postgresql-contrib + postgresql-setup initdb + ;; + fedora | amzn | virtuozzo) + dnf install -y postgresql-server postgresql-contrib + postgresql-setup --initdb + ;; + arch | manjaro | parch) + pacman -S --noconfirm postgresql + sudo -u postgres initdb -D /var/lib/postgres/data + ;; + opensuse-tumbleweed) + zypper install -y postgresql-server postgresql-contrib + ;; + *) + echo -e "${red}Unsupported OS for PostgreSQL installation${plain}" + return 1 + ;; + esac + + # Start and enable PostgreSQL service + systemctl start postgresql + systemctl enable postgresql + + if ! systemctl is-active --quiet postgresql; then + echo -e "${red}Failed to start PostgreSQL service${plain}" + return 1 + fi + + echo -e "${green}PostgreSQL installed and started successfully${plain}" + return 0 +} + +setup_postgresql_for_xui() { + echo -e "${green}Setting up PostgreSQL for x-ui...${plain}" + + local db_name="x_ui" + local db_user="x_ui" + local db_password=$(gen_random_string 16) + + # Create database and user + sudo -u postgres psql -c "CREATE DATABASE ${db_name};" 2>/dev/null || true + sudo -u postgres psql -c "CREATE USER ${db_user} WITH PASSWORD '${db_password}';" 2>/dev/null || true + sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${db_name} TO ${db_user};" 2>/dev/null || true + sudo -u postgres psql -c "ALTER USER ${db_user} CREATEDB;" 2>/dev/null || true + + # Create environment file for x-ui + mkdir -p /etc/x-ui + cat > /etc/x-ui/db.env << EOF +DB_TYPE=postgres +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=${db_name} +DB_USER=${db_user} +DB_PASSWORD=${db_password} +DB_SSLMODE=disable +DB_TIMEZONE=UTC +EOF + + chmod 600 /etc/x-ui/db.env + + echo -e "${green}PostgreSQL setup completed${plain}" + echo -e "${yellow}Database: ${db_name}${plain}" + echo -e "${yellow}User: ${db_user}${plain}" + echo -e "${yellow}Password: ${db_password}${plain}" + echo -e "${yellow}Configuration saved to: /etc/x-ui/db.env${plain}" + + return 0 +} + +switch_to_postgresql() { + echo -e "${yellow}Switching to PostgreSQL database...${plain}" + + # Check if PostgreSQL is installed + if ! command -v psql &> /dev/null; then + echo -e "${yellow}PostgreSQL is not installed. Installing now...${plain}" + if ! install_postgresql; then + echo -e "${red}Failed to install PostgreSQL${plain}" + return 1 + fi + fi + + # Check if PostgreSQL service is running + if ! systemctl is-active --quiet postgresql; then + echo -e "${yellow}Starting PostgreSQL service...${plain}" + systemctl start postgresql + systemctl enable postgresql + fi + + # Setup PostgreSQL for x-ui + if ! setup_postgresql_for_xui; then + echo -e "${red}Failed to setup PostgreSQL${plain}" + return 1 + fi + + echo -e "${green}Successfully switched to PostgreSQL${plain}" + echo -e "${yellow}Please restart x-ui panel to apply changes${plain}" + return 0 +} + +switch_to_sqlite() { + echo -e "${yellow}Switching to SQLite database...${plain}" + + # Create environment file for SQLite + mkdir -p /etc/x-ui + cat > /etc/x-ui/db.env << EOF +DB_TYPE=sqlite +EOF + chmod 600 /etc/x-ui/db.env + + echo -e "${green}Successfully switched to SQLite${plain}" + echo -e "${yellow}Please restart x-ui panel to apply changes${plain}" + return 0 +} + +backup_database() { + local backup_dir="/etc/x-ui/backups" + local timestamp=$(date +"%Y%m%d_%H%M%S") + + mkdir -p "$backup_dir" + + # Check current database type + if [[ -f /etc/x-ui/db.env ]]; then + source /etc/x-ui/db.env + else + DB_TYPE="sqlite" + fi + + case "$DB_TYPE" in + postgres) + echo -e "${green}Backing up PostgreSQL database...${plain}" + if [[ -f /etc/x-ui/db.env ]]; then + source /etc/x-ui/db.env + PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" > "$backup_dir/postgresql_backup_$timestamp.sql" + if [[ $? -eq 0 ]]; then + echo -e "${green}PostgreSQL backup created: $backup_dir/postgresql_backup_$timestamp.sql${plain}" + else + echo -e "${red}Failed to create PostgreSQL backup${plain}" + return 1 + fi + else + echo -e "${red}Database configuration not found${plain}" + return 1 + fi + ;; + sqlite|*) + echo -e "${green}Backing up SQLite database...${plain}" + local sqlite_path="/etc/x-ui/x-ui.db" + if [[ -f "$sqlite_path" ]]; then + cp "$sqlite_path" "$backup_dir/sqlite_backup_$timestamp.db" + echo -e "${green}SQLite backup created: $backup_dir/sqlite_backup_$timestamp.db${plain}" + else + echo -e "${red}SQLite database not found at $sqlite_path${plain}" + return 1 + fi + ;; + esac + + return 0 +} + +show_database_status() { + echo -e "${green}Database Status:${plain}" + + if [[ -f /etc/x-ui/db.env ]]; then + source /etc/x-ui/db.env + echo -e "Current database type: ${green}$DB_TYPE${plain}" + + case "$DB_TYPE" in + postgres) + echo -e "Host: $DB_HOST" + echo -e "Port: $DB_PORT" + echo -e "Database: $DB_NAME" + echo -e "User: $DB_USER" + + # Check PostgreSQL service status + if systemctl is-active --quiet postgresql; then + echo -e "PostgreSQL service: ${green}Running${plain}" + else + echo -e "PostgreSQL service: ${red}Not Running${plain}" + fi + + # Test connection + if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "SELECT 1;" &>/dev/null; then + echo -e "Database connection: ${green}OK${plain}" + else + echo -e "Database connection: ${red}Failed${plain}" + fi + ;; + sqlite|*) + local sqlite_path="/etc/x-ui/x-ui.db" + if [[ -f "$sqlite_path" ]]; then + echo -e "SQLite file: ${green}$sqlite_path${plain}" + echo -e "File size: $(du -h "$sqlite_path" | cut -f1)" + else + echo -e "SQLite file: ${red}Not found${plain}" + fi + ;; + esac + else + echo -e "Database configuration: ${red}Not found${plain}" + echo -e "Using default: ${green}SQLite${plain}" + fi +} + +database_menu() { + echo -e "${green}\t1.${plain} Show Database Status" + echo -e "${green}\t2.${plain} Switch to PostgreSQL" + echo -e "${green}\t3.${plain} Switch to SQLite" + echo -e "${green}\t4.${plain} Install PostgreSQL" + echo -e "${green}\t5.${plain} Backup Database" + echo -e "${green}\t6.${plain} PostgreSQL Service Management" + echo -e "${green}\t0.${plain} Back to Main Menu" + read -rp "Choose an option: " choice + case "$choice" in + 0) + show_menu + ;; + 1) + show_database_status + database_menu + ;; + 2) + confirm "Switch to PostgreSQL database? This will require panel restart." "y" + if [[ $? == 0 ]]; then + switch_to_postgresql + fi + database_menu + ;; + 3) + confirm "Switch to SQLite database? This will require panel restart." "y" + if [[ $? == 0 ]]; then + switch_to_sqlite + fi + database_menu + ;; + 4) + confirm "Install PostgreSQL?" "y" + if [[ $? == 0 ]]; then + install_postgresql + fi + database_menu + ;; + 5) + confirm "Create database backup?" "y" + if [[ $? == 0 ]]; then + backup_database + fi + database_menu + ;; + 6) + postgresql_service_menu + database_menu + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + database_menu + ;; + esac +} + +postgresql_service_menu() { + echo -e "${green}\t1.${plain} Start PostgreSQL" + echo -e "${green}\t2.${plain} Stop PostgreSQL" + echo -e "${green}\t3.${plain} Restart PostgreSQL" + echo -e "${green}\t4.${plain} PostgreSQL Status" + echo -e "${green}\t5.${plain} Enable PostgreSQL Autostart" + echo -e "${green}\t6.${plain} Disable PostgreSQL Autostart" + echo -e "${green}\t0.${plain} Back to Database Menu" + read -rp "Choose an option: " choice + case "$choice" in + 0) + return + ;; + 1) + systemctl start postgresql + echo -e "${green}PostgreSQL started${plain}" + ;; + 2) + systemctl stop postgresql + echo -e "${green}PostgreSQL stopped${plain}" + ;; + 3) + systemctl restart postgresql + echo -e "${green}PostgreSQL restarted${plain}" + ;; + 4) + systemctl status postgresql + ;; + 5) + systemctl enable postgresql + echo -e "${green}PostgreSQL autostart enabled${plain}" + ;; + 6) + systemctl disable postgresql + echo -e "${green}PostgreSQL autostart disabled${plain}" + ;; + *) + echo -e "${red}Invalid option. Please select a valid number.${plain}\n" + postgresql_service_menu + ;; + esac +} + SSH_port_forwarding() { local server_ip=$(curl -s https://api.ipify.org) local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') @@ -1698,66 +2011,66 @@ SSH_port_forwarding() { } show_usage() { - echo -e "โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ${blue}x-ui control menu usages (subcommands):${plain} โ”‚ -โ”‚ โ”‚ -โ”‚ ${blue}x-ui${plain} - Admin Management Script โ”‚ -โ”‚ ${blue}x-ui start${plain} - Start โ”‚ -โ”‚ ${blue}x-ui stop${plain} - Stop โ”‚ -โ”‚ ${blue}x-ui restart${plain} - Restart โ”‚ -โ”‚ ${blue}x-ui status${plain} - Current Status โ”‚ -โ”‚ ${blue}x-ui settings${plain} - Current Settings โ”‚ -โ”‚ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup โ”‚ -โ”‚ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup โ”‚ -โ”‚ ${blue}x-ui log${plain} - Check logs โ”‚ -โ”‚ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs โ”‚ -โ”‚ ${blue}x-ui update${plain} - Update โ”‚ -โ”‚ ${blue}x-ui legacy${plain} - legacy version โ”‚ -โ”‚ ${blue}x-ui install${plain} - Install โ”‚ -โ”‚ ${blue}x-ui uninstall${plain} - Uninstall โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜" + echo -e "โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”" + echo -e "โ”‚ ${blue}x-ui control menu usages (subcommands):${plain} โ”‚" + echo -e "โ”‚ โ”‚" + echo -e "โ”‚ ${blue}x-ui${plain} - Admin Management Script โ”‚" + echo -e "โ”‚ ${blue}x-ui start${plain} - Start โ”‚" + echo -e "โ”‚ ${blue}x-ui stop${plain} - Stop โ”‚" + echo -e "โ”‚ ${blue}x-ui restart${plain} - Restart โ”‚" + echo -e "โ”‚ ${blue}x-ui status${plain} - Current Status โ”‚" + echo -e "โ”‚ ${blue}x-ui settings${plain} - Current Settings โ”‚" + echo -e "โ”‚ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup โ”‚" + echo -e "โ”‚ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup โ”‚" + echo -e "โ”‚ ${blue}x-ui log${plain} - Check logs โ”‚" + echo -e "โ”‚ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs โ”‚" + echo -e "โ”‚ ${blue}x-ui update${plain} - Update โ”‚" + echo -e "โ”‚ ${blue}x-ui legacy${plain} - legacy version โ”‚" + echo -e "โ”‚ ${blue}x-ui install${plain} - Install โ”‚" + echo -e "โ”‚ ${blue}x-ui uninstall${plain} - Uninstall โ”‚" + echo -e "โ”‚ ${blue}x-ui database${plain} - Database Management โ”‚" + echo -e "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜" } show_menu() { - echo -e " -โ•”โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•— -โ”‚ ${green}3X-UI Panel Management Script${plain} โ”‚ -โ”‚ ${green}0.${plain} Exit Script โ”‚ -โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ -โ”‚ ${green}1.${plain} Install โ”‚ -โ”‚ ${green}2.${plain} Update โ”‚ -โ”‚ ${green}3.${plain} Update Menu โ”‚ -โ”‚ ${green}4.${plain} Legacy Version โ”‚ -โ”‚ ${green}5.${plain} Uninstall โ”‚ -โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ -โ”‚ ${green}6.${plain} Reset Username & Password โ”‚ -โ”‚ ${green}7.${plain} Reset Web Base Path โ”‚ -โ”‚ ${green}8.${plain} Reset Settings โ”‚ -โ”‚ ${green}9.${plain} Change Port โ”‚ -โ”‚ ${green}10.${plain} View Current Settings โ”‚ -โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ -โ”‚ ${green}11.${plain} Start โ”‚ -โ”‚ ${green}12.${plain} Stop โ”‚ -โ”‚ ${green}13.${plain} Restart โ”‚ -โ”‚ ${green}14.${plain} Check Status โ”‚ -โ”‚ ${green}15.${plain} Logs Management โ”‚ -โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ -โ”‚ ${green}16.${plain} Enable Autostart โ”‚ -โ”‚ ${green}17.${plain} Disable Autostart โ”‚ -โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ -โ”‚ ${green}18.${plain} SSL Certificate Management โ”‚ -โ”‚ ${green}19.${plain} Cloudflare SSL Certificate โ”‚ -โ”‚ ${green}20.${plain} IP Limit Management โ”‚ -โ”‚ ${green}21.${plain} Firewall Management โ”‚ -โ”‚ ${green}22.${plain} SSH Port Forwarding Management โ”‚ -โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ -โ”‚ ${green}23.${plain} Enable BBR โ”‚ -โ”‚ ${green}24.${plain} Update Geo Files โ”‚ -โ”‚ ${green}25.${plain} Speedtest by Ookla โ”‚ -โ•šโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ• -" + echo -e "โ•”โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•—" + echo -e "โ”‚ ${green}3X-UI Panel Management Script${plain} โ”‚" + echo -e "โ”‚ ${green}0.${plain} Exit Script โ”‚" + echo -e "โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚" + echo -e "โ”‚ ${green}1.${plain} Install โ”‚" + echo -e "โ”‚ ${green}2.${plain} Update โ”‚" + echo -e "โ”‚ ${green}3.${plain} Update Menu โ”‚" + echo -e "โ”‚ ${green}4.${plain} Legacy Version โ”‚" + echo -e "โ”‚ ${green}5.${plain} Uninstall โ”‚" + echo -e "โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚" + echo -e "โ”‚ ${green}6.${plain} Reset Username & Password โ”‚" + echo -e "โ”‚ ${green}7.${plain} Reset Web Base Path โ”‚" + echo -e "โ”‚ ${green}8.${plain} Reset Settings โ”‚" + echo -e "โ”‚ ${green}9.${plain} Change Port โ”‚" + echo -e "โ”‚ ${green}10.${plain} View Current Settings โ”‚" + echo -e "โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚" + echo -e "โ”‚ ${green}11.${plain} Start โ”‚" + echo -e "โ”‚ ${green}12.${plain} Stop โ”‚" + echo -e "โ”‚ ${green}13.${plain} Restart โ”‚" + echo -e "โ”‚ ${green}14.${plain} Check Status โ”‚" + echo -e "โ”‚ ${green}15.${plain} Logs Management โ”‚" + echo -e "โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚" + echo -e "โ”‚ ${green}16.${plain} Enable Autostart โ”‚" + echo -e "โ”‚ ${green}17.${plain} Disable Autostart โ”‚" + echo -e "โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚" + echo -e "โ”‚ ${green}18.${plain} SSL Certificate Management โ”‚" + echo -e "โ”‚ ${green}19.${plain} Cloudflare SSL Certificate โ”‚" + echo -e "โ”‚ ${green}20.${plain} IP Limit Management โ”‚" + echo -e "โ”‚ ${green}21.${plain} Firewall Management โ”‚" + echo -e "โ”‚ ${green}22.${plain} SSH Port Forwarding Management โ”‚" + echo -e "โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚" + echo -e "โ”‚ ${green}23.${plain} Enable BBR โ”‚" + echo -e "โ”‚ ${green}24.${plain} Update Geo Files โ”‚" + echo -e "โ”‚ ${green}25.${plain} Speedtest by Ookla โ”‚" + echo -e "โ”‚ ${green}26.${plain} Database Management โ”‚" + echo -e "โ•šโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•" show_status - echo && read -rp "Please enter your selection [0-25]: " num + echo && read -rp "Please enter your selection [0-26]: " num case "${num}" in 0) @@ -1838,8 +2151,11 @@ show_menu() { 25) run_speedtest ;; + 26) + database_menu + ;; *) - LOGE "Please enter the correct number [0-25]" + LOGE "Please enter the correct number [0-26]" ;; esac } @@ -1885,7 +2201,12 @@ if [[ $# > 0 ]]; then "uninstall") check_install 0 && uninstall 0 ;; - *) show_usage ;; + "database") + check_install 0 && database_menu 0 + ;; + *) + show_usage + ;; esac else show_menu