Migrate panel DB from MySQL to SQLite

Panel no longer depends on the user-managed MariaDB service.
SQLite at /var/lib/novacpx/panel.db runs independently so the
control panel stays up even when MariaDB is stopped.

- DB.php: switch to sqlite: DSN, add SQL translator (ON DUPLICATE KEY,
  DATE_ADD/DATE_SUB INTERVAL, NOW(), UNIX_TIMESTAMP(), IFNULL)
- Core.php: replace DB_HOST/NAME/USER/PASS with DB_PATH constant
- schema.sql: full SQLite syntax, add TOTP columns to users table
- _branding.php: use sqlite: PDO, datetime('now') for session check
- install.sh: apt install sqlite3, create SQLite DB instead of MySQL DB
- tools/migrate-to-sqlite.sh: one-shot migration script for existing installs
This commit is contained in:
2026-06-09 14:52:02 +00:00
parent 9bd78a81ea
commit fbc445dad2
5 changed files with 713 additions and 335 deletions
+450 -324
View File
@@ -1,405 +1,531 @@
-- NovaCPX Database Schema v1.0.0 -- NovaCPX Database Schema v1.1.0
-- Engine: MySQL 8+ | Charset: utf8mb4_unicode_ci -- Engine: SQLite 3.35+
SET NAMES utf8mb4; PRAGMA journal_mode = WAL;
SET foreign_key_checks = 0; PRAGMA foreign_keys = OFF;
-- ── Version tracking ────────────────────────────────────────────────────────── -- ── Version tracking ──────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS novacpx_version ( CREATE TABLE IF NOT EXISTS novacpx_version (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
version VARCHAR(20) NOT NULL, version TEXT NOT NULL,
installed_at DATETIME DEFAULT CURRENT_TIMESTAMP, installed_at TEXT DEFAULT (datetime('now')),
notes TEXT, notes TEXT,
git_commit VARCHAR(64), git_commit TEXT
INDEX idx_version (version) );
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO novacpx_version (version, notes, git_commit) INSERT OR IGNORE INTO novacpx_version (version, notes, git_commit)
VALUES ('1.0.0', 'Initial installation', 'HEAD'); VALUES ('1.1.0', 'Initial installation', 'HEAD');
-- ── Audit log (every action tracked) ───────────────────────────────────────── -- ── Audit log ─────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS audit_log ( CREATE TABLE IF NOT EXISTS audit_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INT UNSIGNED, user_id INTEGER,
username VARCHAR(100), username TEXT,
action VARCHAR(100) NOT NULL, action TEXT NOT NULL,
resource VARCHAR(200), resource TEXT,
detail JSON, detail TEXT,
ip_address VARCHAR(45), ip_address TEXT,
user_agent TEXT, user_agent TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now'))
INDEX idx_user (user_id), );
INDEX idx_action (action), CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log (user_id);
INDEX idx_created (created_at) CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log (created_at);
-- ── Users (admin, resellers, end-users) ─────────────────────────────────────── -- ── Users ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(100) NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, password TEXT NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE,
role ENUM('admin','reseller','user') DEFAULT 'user', role TEXT NOT NULL DEFAULT 'user' CHECK(role IN ('admin','reseller','user')),
status ENUM('active','suspended','pending') DEFAULT 'active', status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','suspended','pending')),
reseller_id INT UNSIGNED DEFAULT NULL, reseller_id INTEGER DEFAULT NULL,
package_id INT UNSIGNED DEFAULT NULL, package_id INTEGER DEFAULT NULL,
theme VARCHAR(50) DEFAULT 'nova-dark', theme TEXT DEFAULT 'nova-dark',
language VARCHAR(10) DEFAULT 'en', language TEXT DEFAULT 'en',
contact_name VARCHAR(200), contact_name TEXT,
contact_phone VARCHAR(50), contact_phone TEXT,
last_login DATETIME, last_login TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP, updated_at TEXT,
FOREIGN KEY (reseller_id) REFERENCES users(id) ON DELETE SET NULL, totp_secret TEXT,
INDEX idx_role (role), totp_enabled INTEGER DEFAULT 0,
INDEX idx_status (status), totp_backup_codes TEXT,
INDEX idx_reseller (reseller_id) FOREIGN KEY (reseller_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; );
CREATE INDEX IF NOT EXISTS idx_users_role ON users (role);
CREATE INDEX IF NOT EXISTS idx_users_status ON users (status);
CREATE INDEX IF NOT EXISTS idx_users_reseller ON users (reseller_id);
-- ── Sessions ────────────────────────────────────────────────────────────────── -- ── Sessions ──────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (
id VARCHAR(128) PRIMARY KEY, id TEXT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL, user_id INTEGER NOT NULL,
ip_address VARCHAR(45), ip_address TEXT,
user_agent TEXT, user_agent TEXT,
data JSON, data TEXT,
expires_at DATETIME NOT NULL, expires_at TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
INDEX idx_expires (expires_at) );
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions (expires_at);
-- ── Packages / Hosting Plans ────────────────────────────────────────────────── -- ── Packages / Hosting Plans ──────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS packages ( CREATE TABLE IF NOT EXISTS packages (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL, name TEXT NOT NULL,
owner_id INT UNSIGNED DEFAULT NULL, -- NULL = system, or reseller owner_id INTEGER DEFAULT NULL,
disk_mb INT UNSIGNED DEFAULT 1024, disk_mb INTEGER DEFAULT 1024,
bandwidth_mb BIGINT UNSIGNED DEFAULT 10240, bandwidth_mb INTEGER DEFAULT 10240,
max_domains SMALLINT UNSIGNED DEFAULT 1, max_domains INTEGER DEFAULT 1,
max_subdomains SMALLINT UNSIGNED DEFAULT 10, max_subdomains INTEGER DEFAULT 10,
max_addon_domains SMALLINT UNSIGNED DEFAULT 0, max_addon_domains INTEGER DEFAULT 0,
max_parked_domains SMALLINT UNSIGNED DEFAULT 5, max_parked_domains INTEGER DEFAULT 5,
max_email SMALLINT UNSIGNED DEFAULT 10, max_email INTEGER DEFAULT 10,
max_ftp SMALLINT UNSIGNED DEFAULT 5, max_ftp INTEGER DEFAULT 5,
max_databases SMALLINT UNSIGNED DEFAULT 5, max_databases INTEGER DEFAULT 5,
php_version VARCHAR(10) DEFAULT '8.3', php_version TEXT DEFAULT '8.3',
ssl_enabled TINYINT(1) DEFAULT 1, ssl_enabled INTEGER DEFAULT 1,
is_default TINYINT(1) DEFAULT 0, is_default INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL, FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL
INDEX idx_owner (owner_id) );
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX IF NOT EXISTS idx_packages_owner ON packages (owner_id);
INSERT INTO packages (name, disk_mb, bandwidth_mb, max_domains, max_email, max_databases, is_default) INSERT OR IGNORE INTO packages (id, name, disk_mb, bandwidth_mb, max_domains, max_email, max_databases, is_default)
VALUES ('Default', 5120, 51200, 5, 25, 10, 1); VALUES (1, 'Default', 5120, 51200, 5, 25, 10, 1);
-- ── Hosting Accounts ────────────────────────────────────────────────────────── -- ── Hosting Accounts ──────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS accounts ( CREATE TABLE IF NOT EXISTS accounts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INT UNSIGNED NOT NULL UNIQUE, user_id INTEGER NOT NULL UNIQUE,
username VARCHAR(32) NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
domain VARCHAR(253) NOT NULL, domain TEXT NOT NULL,
home_dir VARCHAR(500) NOT NULL, home_dir TEXT NOT NULL,
package_id INT UNSIGNED, document_root TEXT,
disk_used_mb INT UNSIGNED DEFAULT 0, system_user TEXT DEFAULT 'www-data',
bw_used_mb BIGINT UNSIGNED DEFAULT 0, package_id INTEGER,
php_version VARCHAR(10) DEFAULT '8.3', disk_used_mb INTEGER DEFAULT 0,
web_server ENUM('apache','nginx') DEFAULT 'apache', bw_used_mb INTEGER DEFAULT 0,
status ENUM('active','suspended','terminated') DEFAULT 'active', php_version TEXT DEFAULT '8.3',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, web_server TEXT DEFAULT 'apache' CHECK(web_server IN ('apache','nginx')),
suspended_at DATETIME DEFAULT NULL, status TEXT DEFAULT 'active' CHECK(status IN ('active','suspended','terminated')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, cf_api_key TEXT,
FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE SET NULL, cf_api_email TEXT,
INDEX idx_domain (domain), cf_zone_id TEXT,
INDEX idx_status (status) created_at TEXT DEFAULT (datetime('now')),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; suspended_at TEXT DEFAULT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_accounts_domain ON accounts (domain);
CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts (status);
-- ── Domains ─────────────────────────────────────────────────────────────────── -- ── Domains ───────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS domains ( CREATE TABLE IF NOT EXISTS domains (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INT UNSIGNED NOT NULL, account_id INTEGER NOT NULL,
domain VARCHAR(253) NOT NULL, domain TEXT NOT NULL UNIQUE,
type ENUM('main','addon','subdomain','parked','alias') DEFAULT 'main', type TEXT DEFAULT 'main' CHECK(type IN ('main','addon','subdomain','parked','alias')),
document_root VARCHAR(500), document_root TEXT,
php_version VARCHAR(10), php_version TEXT,
ssl_enabled TINYINT(1) DEFAULT 0, ssl_enabled INTEGER DEFAULT 0,
ssl_cert TEXT, ssl_cert TEXT,
ssl_key TEXT, ssl_key TEXT,
ssl_chain TEXT, ssl_chain TEXT,
ssl_expires DATE, ssl_expires TEXT,
redirect_to VARCHAR(500) DEFAULT NULL, redirect_to TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE, FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
UNIQUE KEY uq_domain (domain), );
INDEX idx_account (account_id), CREATE INDEX IF NOT EXISTS idx_domains_account ON domains (account_id);
INDEX idx_type (type) CREATE INDEX IF NOT EXISTS idx_domains_type ON domains (type);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ── DNS Zones & Records ─────────────────────────────────────────────────────── -- ── DNS Zones & Records ───────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS dns_zones ( CREATE TABLE IF NOT EXISTS dns_zones (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INT UNSIGNED NOT NULL, account_id INTEGER NOT NULL,
domain VARCHAR(253) NOT NULL UNIQUE, domain TEXT NOT NULL UNIQUE,
serial BIGINT UNSIGNED DEFAULT 1, serial INTEGER DEFAULT 1,
primary_ns VARCHAR(253) DEFAULT 'ns1.localhost', primary_ns TEXT DEFAULT 'ns1.localhost',
secondary_ns VARCHAR(253) DEFAULT 'ns2.localhost', secondary_ns TEXT DEFAULT 'ns2.localhost',
admin_email VARCHAR(255) DEFAULT 'hostmaster@localhost', admin_email TEXT DEFAULT 'hostmaster@localhost',
ttl INT UNSIGNED DEFAULT 3600, ttl INTEGER DEFAULT 3600,
refresh INT UNSIGNED DEFAULT 86400, refresh INTEGER DEFAULT 86400,
retry INT UNSIGNED DEFAULT 7200, retry INTEGER DEFAULT 7200,
expire INT UNSIGNED DEFAULT 2419200, expire INTEGER DEFAULT 2419200,
minimum INT UNSIGNED DEFAULT 86400, minimum INTEGER DEFAULT 86400,
status ENUM('active','disabled') DEFAULT 'active', status TEXT DEFAULT 'active' CHECK(status IN ('active','disabled')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP, updated_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE, FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
INDEX idx_account (account_id) );
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX IF NOT EXISTS idx_dns_zones_account ON dns_zones (account_id);
CREATE TABLE IF NOT EXISTS dns_records ( CREATE TABLE IF NOT EXISTS dns_records (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
zone_id INT UNSIGNED NOT NULL, zone_id INTEGER NOT NULL,
name VARCHAR(253) NOT NULL, name TEXT NOT NULL,
type ENUM('A','AAAA','CNAME','MX','TXT','SRV','NS','PTR','CAA','DKIM','SPF','DMARC') NOT NULL, type TEXT NOT NULL CHECK(type IN ('A','AAAA','CNAME','MX','TXT','SRV','NS','PTR','CAA','DKIM','SPF','DMARC')),
content TEXT NOT NULL, content TEXT NOT NULL,
ttl INT UNSIGNED DEFAULT 3600, ttl INTEGER DEFAULT 3600,
priority SMALLINT UNSIGNED DEFAULT NULL, priority INTEGER DEFAULT NULL,
proxied TINYINT(1) DEFAULT 0, proxied INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (zone_id) REFERENCES dns_zones(id) ON DELETE CASCADE, FOREIGN KEY (zone_id) REFERENCES dns_zones(id) ON DELETE CASCADE
INDEX idx_zone (zone_id), );
INDEX idx_type (type), CREATE INDEX IF NOT EXISTS idx_dns_records_zone ON dns_records (zone_id);
INDEX idx_name (name(100)) CREATE INDEX IF NOT EXISTS idx_dns_records_type ON dns_records (type);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ── Email Accounts & Forwarders ─────────────────────────────────────────────── -- ── Email ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS email_accounts ( CREATE TABLE IF NOT EXISTS email_accounts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INT UNSIGNED NOT NULL, account_id INTEGER NOT NULL,
email VARCHAR(320) NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, password TEXT NOT NULL,
quota_mb INT UNSIGNED DEFAULT 500, quota_mb INTEGER DEFAULT 500,
used_mb INT UNSIGNED DEFAULT 0, used_mb INTEGER DEFAULT 0,
status ENUM('active','suspended') DEFAULT 'active', status TEXT DEFAULT 'active' CHECK(status IN ('active','suspended')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE, FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
INDEX idx_account (account_id) );
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX IF NOT EXISTS idx_email_accounts_account ON email_accounts (account_id);
CREATE TABLE IF NOT EXISTS email_forwarders ( CREATE TABLE IF NOT EXISTS email_forwarders (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INT UNSIGNED NOT NULL, account_id INTEGER NOT NULL,
source VARCHAR(320) NOT NULL, source TEXT NOT NULL,
destination VARCHAR(320) NOT NULL, destination TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; );
CREATE TABLE IF NOT EXISTS email_autoresponders ( CREATE TABLE IF NOT EXISTS email_autoresponders (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INT UNSIGNED NOT NULL, account_id INTEGER NOT NULL,
email VARCHAR(320) NOT NULL, email TEXT NOT NULL,
subject VARCHAR(255), subject TEXT,
body TEXT, body TEXT,
is_active TINYINT(1) DEFAULT 1, is_active INTEGER DEFAULT 1,
start_at DATETIME, start_at TEXT,
stop_at DATETIME, stop_at TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; );
-- ── Databases ───────────────────────────────────────────────────────────────── -- ── Databases ─────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS databases ( CREATE TABLE IF NOT EXISTS databases (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INT UNSIGNED NOT NULL, account_id INTEGER NOT NULL,
db_name VARCHAR(100) NOT NULL, db_name TEXT NOT NULL,
db_user VARCHAR(100) NOT NULL, db_user TEXT NOT NULL,
db_pass VARCHAR(255) NOT NULL, db_pass TEXT NOT NULL,
db_type ENUM('mysql','postgresql') DEFAULT 'mysql', db_type TEXT DEFAULT 'mysql' CHECK(db_type IN ('mysql','postgresql')),
size_mb FLOAT DEFAULT 0, size_mb REAL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
INDEX idx_account (account_id),
INDEX idx_type (db_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ── FTP Accounts ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS ftp_accounts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
account_id INT UNSIGNED NOT NULL,
username VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
home_dir VARCHAR(500) NOT NULL,
quota_mb INT UNSIGNED DEFAULT 0,
status ENUM('active','suspended') DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; );
CREATE INDEX IF NOT EXISTS idx_databases_account ON databases (account_id);
-- ── FTP Accounts ──────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS ftp_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
home_dir TEXT NOT NULL,
quota_mb INTEGER DEFAULT 0,
status TEXT DEFAULT 'active' CHECK(status IN ('active','suspended')),
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
-- ── SSL Certificates ────────────────────────────────────────────────────────── -- ── SSL Certificates ──────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS ssl_certs ( CREATE TABLE IF NOT EXISTS ssl_certs (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INT UNSIGNED NOT NULL, account_id INTEGER NOT NULL,
domain VARCHAR(253) NOT NULL, domain TEXT NOT NULL,
type ENUM('lets_encrypt','self_signed','custom') DEFAULT 'lets_encrypt', type TEXT DEFAULT 'lets_encrypt' CHECK(type IN ('lets_encrypt','self_signed','custom')),
cert TEXT, cert TEXT,
private_key TEXT, private_key TEXT,
chain TEXT, chain TEXT,
issued_at DATETIME, issued_at TEXT,
expires_at DATETIME, expires_at TEXT,
auto_renew TINYINT(1) DEFAULT 1, auto_renew INTEGER DEFAULT 1,
status ENUM('active','expired','pending','failed') DEFAULT 'pending', status TEXT DEFAULT 'pending' CHECK(status IN ('active','expired','pending','failed')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE, FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
INDEX idx_domain (domain), );
INDEX idx_expires (expires_at) CREATE INDEX IF NOT EXISTS idx_ssl_certs_domain ON ssl_certs (domain);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX IF NOT EXISTS idx_ssl_certs_expires ON ssl_certs (expires_at);
-- ── Cron Jobs ───────────────────────────────────────────────────────────────── -- ── Cron Jobs ─────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS cron_jobs ( CREATE TABLE IF NOT EXISTS cron_jobs (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INT UNSIGNED NOT NULL, account_id INTEGER NOT NULL,
command TEXT NOT NULL, command TEXT NOT NULL,
minute VARCHAR(20) DEFAULT '*', minute TEXT DEFAULT '*',
hour VARCHAR(20) DEFAULT '*', hour TEXT DEFAULT '*',
day VARCHAR(20) DEFAULT '*', day TEXT DEFAULT '*',
month VARCHAR(20) DEFAULT '*', month TEXT DEFAULT '*',
weekday VARCHAR(20) DEFAULT '*', weekday TEXT DEFAULT '*',
is_active TINYINT(1) DEFAULT 1, is_active INTEGER DEFAULT 1,
last_run DATETIME, last_run TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; );
-- ── PHP Configuration ───────────────────────────────────────────────────────── -- ── PHP Configuration ─────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS php_configs ( CREATE TABLE IF NOT EXISTS php_configs (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INT UNSIGNED NOT NULL UNIQUE, account_id INTEGER NOT NULL UNIQUE,
php_version VARCHAR(10) DEFAULT '8.3', php_version TEXT DEFAULT '8.3',
memory_limit VARCHAR(20) DEFAULT '256M', memory_limit TEXT DEFAULT '256M',
max_execution_time INT DEFAULT 30, max_execution_time INTEGER DEFAULT 30,
upload_max_filesize VARCHAR(20) DEFAULT '64M', upload_max_filesize TEXT DEFAULT '64M',
post_max_size VARCHAR(20) DEFAULT '64M', post_max_size TEXT DEFAULT '64M',
max_input_vars INT DEFAULT 1000, max_input_vars INTEGER DEFAULT 1000,
display_errors TINYINT(1) DEFAULT 0, display_errors INTEGER DEFAULT 0,
error_reporting VARCHAR(50) DEFAULT 'E_ALL & ~E_NOTICE', error_reporting TEXT DEFAULT 'E_ALL & ~E_NOTICE',
extensions JSON, extensions TEXT,
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP, updated_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; );
-- ── Backups ─────────────────────────────────────────────────────────────────── -- ── Backups ───────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS backups ( CREATE TABLE IF NOT EXISTS backups (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INT UNSIGNED NOT NULL, account_id INTEGER NOT NULL,
filename VARCHAR(500) NOT NULL, filename TEXT NOT NULL,
size_mb FLOAT DEFAULT 0, size_mb REAL DEFAULT 0,
type ENUM('full','partial','db_only','files_only') DEFAULT 'full', type TEXT DEFAULT 'full' CHECK(type IN ('full','partial','db_only','files_only')),
status ENUM('pending','running','complete','failed') DEFAULT 'pending', status TEXT DEFAULT 'pending' CHECK(status IN ('pending','running','complete','failed')),
storage ENUM('local','s3','ftp','sftp') DEFAULT 'local', storage TEXT DEFAULT 'local' CHECK(storage IN ('local','s3','ftp','sftp')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
completed_at DATETIME, completed_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE, FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
INDEX idx_account (account_id), );
INDEX idx_status (status) CREATE INDEX IF NOT EXISTS idx_backups_account ON backups (account_id);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX IF NOT EXISTS idx_backups_status ON backups (status);
-- ── Server Stats / Monitoring ───────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS backup_schedules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL UNIQUE,
frequency TEXT DEFAULT 'daily' CHECK(frequency IN ('hourly','daily','weekly','monthly')),
type TEXT DEFAULT 'full' CHECK(type IN ('full','files','database')),
retain_count INTEGER DEFAULT 7,
last_run TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
-- ── Server Stats ──────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS server_stats ( CREATE TABLE IF NOT EXISTS server_stats (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
cpu_pct FLOAT, cpu_pct REAL,
ram_pct FLOAT, ram_pct REAL,
disk_pct FLOAT, disk_pct REAL,
load_1m FLOAT, load_1m REAL,
load_5m FLOAT, load_5m REAL,
load_15m FLOAT, load_15m REAL,
net_in_kb BIGINT UNSIGNED DEFAULT 0, net_in_kb INTEGER DEFAULT 0,
net_out_kb BIGINT UNSIGNED DEFAULT 0, net_out_kb INTEGER DEFAULT 0,
recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP, recorded_at TEXT DEFAULT (datetime('now'))
INDEX idx_recorded (recorded_at) );
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX IF NOT EXISTS idx_server_stats_recorded ON server_stats (recorded_at);
-- ── Notifications ───────────────────────────────────────────────────────────── -- ── Notifications ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS notifications ( CREATE TABLE IF NOT EXISTS notifications (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INT UNSIGNED, user_id INTEGER,
title VARCHAR(255) NOT NULL, title TEXT NOT NULL,
message TEXT NOT NULL, message TEXT NOT NULL,
type ENUM('info','success','warning','error') DEFAULT 'info', type TEXT DEFAULT 'info' CHECK(type IN ('info','success','warning','error')),
is_read TINYINT(1) DEFAULT 0, is_read INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
INDEX idx_user (user_id), );
INDEX idx_unread (is_read) CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications (user_id);
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications (is_read);
-- ── API Tokens ──────────────────────────────────────────────────────────────── -- ── API Tokens ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS api_tokens ( CREATE TABLE IF NOT EXISTS api_tokens (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INT UNSIGNED NOT NULL, user_id INTEGER NOT NULL,
name VARCHAR(100) NOT NULL, name TEXT NOT NULL,
token VARCHAR(128) NOT NULL UNIQUE, token TEXT NOT NULL UNIQUE,
permissions JSON, permissions TEXT,
last_used DATETIME, last_used TEXT,
expires_at DATETIME, expires_at TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
INDEX idx_token (token) );
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX IF NOT EXISTS idx_api_tokens_token ON api_tokens (token);
-- ── Settings ────────────────────────────────────────────────────────────────── -- ── Settings ──────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS settings (
`key` VARCHAR(100) PRIMARY KEY, `key` TEXT PRIMARY KEY,
`value` TEXT, `value` TEXT,
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP updated_at TEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; );
INSERT INTO settings (`key`, `value`) VALUES INSERT OR IGNORE INTO settings (`key`, `value`) VALUES
('panel_name', 'NovaCPX'), ('panel_name', 'NovaCPX'),
('panel_version', '1.0.0'), ('panel_version', '1.1.0'),
('default_nameserver1', 'ns1.localhost'), ('default_nameserver1', 'ns1.localhost'),
('default_nameserver2', 'ns2.localhost'), ('default_nameserver2', 'ns2.localhost'),
('default_php', '8.3'), ('default_php', '8.3'),
('mail_enabled', '1'), ('mail_enabled', '1'),
('ftp_enabled', '1'), ('ftp_enabled', '1'),
('dns_enabled', '1'), ('dns_enabled', '1'),
('backup_dir', '/var/novacpx/backups'), ('backup_dir', '/var/novacpx/backups'),
('update_channel', 'stable'), ('update_channel', 'stable'),
('git_remote', 'https://github.com/myronblair/novacpx.git') ('git_remote', 'https://github.com/myronblair/novacpx.git'),
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`); ('proxy_mode', 'disabled'),
('proxy_apache_port', '80');
-- ── DKIM Keys ─────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS dkim_keys ( CREATE TABLE IF NOT EXISTS dkim_keys (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
`account_id` INT UNSIGNED NOT NULL, account_id INTEGER NOT NULL,
`domain` VARCHAR(253) NOT NULL, domain TEXT NOT NULL UNIQUE,
`selector` VARCHAR(63) NOT NULL DEFAULT 'mail', selector TEXT NOT NULL DEFAULT 'mail',
`public_key` TEXT NOT NULL, public_key TEXT NOT NULL,
`private_key_path` VARCHAR(500) NOT NULL, private_key_path TEXT NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE KEY uq_domain (domain), FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
CONSTRAINT fk_dkim_acct FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE );
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ── Rate Limits ───────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS api_rate_limits ( CREATE TABLE IF NOT EXISTS api_rate_limits (
ip VARCHAR(45) NOT NULL, ip TEXT NOT NULL,
endpoint VARCHAR(32) NOT NULL, endpoint TEXT NOT NULL,
hits INT UNSIGNED NOT NULL DEFAULT 1, hits INTEGER NOT NULL DEFAULT 1,
window_start INT UNSIGNED NOT NULL DEFAULT 0, window_start INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (ip, endpoint) PRIMARY KEY (ip, endpoint)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; );
-- ── Proxy Hosts ───────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS proxy_hosts ( CREATE TABLE IF NOT EXISTS proxy_hosts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INT UNSIGNED, account_id INTEGER,
domain VARCHAR(253) NOT NULL, domain TEXT NOT NULL UNIQUE,
upstream VARCHAR(255) NOT NULL, upstream TEXT NOT NULL,
ssl_enabled TINYINT(1) NOT NULL DEFAULT 0, ssl_enabled INTEGER NOT NULL DEFAULT 0,
enabled TINYINT(1) NOT NULL DEFAULT 1, enabled INTEGER NOT NULL DEFAULT 1,
custom_config TEXT, custom_config TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE KEY uq_domain (domain), FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE SET NULL
CONSTRAINT fk_proxy_acct FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE SET NULL );
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SET foreign_key_checks = 1; -- ── Reseller Branding ─────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS reseller_branding (
user_id INTEGER PRIMARY KEY,
panel_name TEXT NOT NULL DEFAULT 'NovaCPX',
logo_url TEXT,
favicon_url TEXT,
primary_color TEXT NOT NULL DEFAULT '#6366f1',
accent_color TEXT NOT NULL DEFAULT '#0ea5e9',
support_email TEXT,
support_url TEXT,
hide_powered_by INTEGER NOT NULL DEFAULT 0,
custom_css TEXT,
updated_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- ── Webmail SSO Tokens ────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS webmail_sso_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT NOT NULL UNIQUE,
email TEXT NOT NULL,
enc_pass TEXT NOT NULL,
expires_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_webmail_sso_expires ON webmail_sso_tokens (expires_at);
-- ── WordPress Installs ────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS wordpress_installs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
domain TEXT NOT NULL,
path TEXT DEFAULT '/',
db_name TEXT,
db_user TEXT,
db_pass TEXT,
wp_version TEXT,
admin_user TEXT,
admin_email TEXT,
status TEXT DEFAULT 'active' CHECK(status IN ('active','updating','suspended')),
staging_of INTEGER,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_wp_installs_account ON wordpress_installs (account_id);
-- ── Docker ────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS docker_containers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
container_id TEXT,
name TEXT NOT NULL,
image TEXT NOT NULL,
app_key TEXT,
status TEXT DEFAULT 'pending' CHECK(status IN ('running','stopped','error','pending')),
ports TEXT,
memory_mb INTEGER,
cpus REAL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_docker_containers_account ON docker_containers (account_id);
CREATE TABLE IF NOT EXISTS docker_compose_stacks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER,
name TEXT NOT NULL,
stack_dir TEXT NOT NULL,
compose_file TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('running','stopped','error','pending')),
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS docker_quotas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE,
max_containers INTEGER DEFAULT 2,
max_memory_mb INTEGER DEFAULT 512,
max_cpus REAL DEFAULT 1.0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- ── Features ──────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS features (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL,
enabled INTEGER DEFAULT 0,
installed INTEGER DEFAULT 0,
install_cmd TEXT,
uninstall_cmd TEXT,
config_keys TEXT,
install_pid INTEGER,
install_log TEXT,
requires TEXT,
requires_restart INTEGER DEFAULT 0,
min_ram_mb INTEGER DEFAULT 0,
updated_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_features_category ON features (category);
CREATE INDEX IF NOT EXISTS idx_features_enabled ON features (enabled);
PRAGMA foreign_keys = ON;
+3 -4
View File
@@ -13,10 +13,9 @@ if (!$_cfg) {
die(json_encode(['error' => 'NovaCPX not configured. Run the installer.'])); die(json_encode(['error' => 'NovaCPX not configured. Run the installer.']));
} }
define('DB_HOST', $_cfg['database']['host'] ?? 'localhost'); define('DB_PATH', $_cfg['database']['path'] ?? '/var/lib/novacpx/panel.db');
define('DB_NAME', $_cfg['database']['name'] ?? 'novacpx'); define('DB_WP_USER', $_cfg['database']['wp_user'] ?? '');
define('DB_USER', $_cfg['database']['user'] ?? ''); define('DB_WP_PASS', $_cfg['database']['wp_pass'] ?? '');
define('DB_PASS', $_cfg['database']['pass'] ?? '');
define('SECRET_KEY', $_cfg['panel']['secret'] ?? ''); define('SECRET_KEY', $_cfg['panel']['secret'] ?? '');
define('PANEL_VER', $_cfg['panel']['version'] ?? NOVACPX_VERSION); define('PANEL_VER', $_cfg['panel']['version'] ?? NOVACPX_VERSION);
define('PORT_USER', (int)($_cfg['panel']['port_user'] ?? 8880)); define('PORT_USER', (int)($_cfg['panel']['port_user'] ?? 8880));
+82 -4
View File
@@ -4,15 +4,21 @@ class DB {
private PDO $pdo; private PDO $pdo;
private function __construct() { private function __construct() {
$path = defined('DB_PATH') ? DB_PATH : '/var/lib/novacpx/panel.db';
$dir = dirname($path);
if (!is_dir($dir)) mkdir($dir, 0750, true);
$this->pdo = new PDO( $this->pdo = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", "sqlite:{$path}",
DB_USER, DB_PASS, null, null,
[ [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
] ]
); );
$this->pdo->exec("PRAGMA journal_mode = WAL");
$this->pdo->exec("PRAGMA foreign_keys = ON");
$this->pdo->exec("PRAGMA busy_timeout = 5000");
} }
public static function getInstance(): self { public static function getInstance(): self {
@@ -20,8 +26,80 @@ class DB {
return self::$instance; return self::$instance;
} }
// Translate MySQL-isms to SQLite equivalents
private function translate(string $sql): string {
// ON DUPLICATE KEY UPDATE col=VALUES(col) → ON CONFLICT DO UPDATE SET col=excluded.col
$sql = preg_replace_callback(
'/ON DUPLICATE KEY UPDATE\s+(.+?)(?=\s*(?:;|$))/is',
function (array $m): string {
$pairs = preg_split('/,\s*/', trim($m[1]));
$sets = array_map(function (string $pair): string {
if (preg_match('/(\w+)\s*=\s*VALUES\s*\(\s*(\w+)\s*\)/i', $pair, $pm)) {
return "{$pm[1]}=excluded.{$pm[2]}";
}
// col=? or col=expr — keep as-is
return $pair;
}, $pairs);
return 'ON CONFLICT DO UPDATE SET ' . implode(', ', $sets);
},
$sql
);
// NOW() → datetime('now')
$sql = preg_replace('/\bNOW\(\)/i', "datetime('now')", $sql);
// UNIX_TIMESTAMP() → strftime('%s','now')
$sql = preg_replace('/\bUNIX_TIMESTAMP\(\)/i', "strftime('%s','now')", $sql);
// DATE_ADD(expr, INTERVAL n UNIT) → datetime(expr, '+n unit')
$sql = preg_replace_callback(
"/DATE_ADD\s*\(\s*(NOW\(\)|datetime\('now'\))\s*,\s*INTERVAL\s+(\d+)\s+(\w+)\s*\)/i",
function (array $m): string {
$n = $m[2];
$unit = strtolower($m[3]);
// Map MySQL interval units to SQLite modifier strings
$map = [
'second' => 'second', 'seconds' => 'second',
'minute' => 'minute', 'minutes' => 'minute',
'hour' => 'hour', 'hours' => 'hour',
'day' => 'day', 'days' => 'day',
'month' => 'month', 'months' => 'month',
'year' => 'year', 'years' => 'year',
];
$mod = $map[$unit] ?? $unit;
return "datetime('now', '+{$n} {$mod}')";
},
$sql
);
// DATE_SUB(expr, INTERVAL n UNIT) → datetime(expr, '-n unit')
$sql = preg_replace_callback(
"/DATE_SUB\s*\(\s*(NOW\(\)|datetime\('now'\))\s*,\s*INTERVAL\s+(\d+)\s+(\w+)\s*\)/i",
function (array $m): string {
$n = $m[2];
$unit = strtolower($m[3]);
$map = [
'second' => 'second', 'seconds' => 'second',
'minute' => 'minute', 'minutes' => 'minute',
'hour' => 'hour', 'hours' => 'hour',
'day' => 'day', 'days' => 'day',
'month' => 'month', 'months' => 'month',
'year' => 'year', 'years' => 'year',
];
$mod = $map[$unit] ?? $unit;
return "datetime('now', '-{$n} {$mod}')";
},
$sql
);
// IFNULL → COALESCE (SQLite supports both but be safe)
$sql = preg_replace('/\bIFNULL\s*\(/i', 'COALESCE(', $sql);
return $sql;
}
public function execute(string $sql, array $params = []): PDOStatement { public function execute(string $sql, array $params = []): PDOStatement {
$stmt = $this->pdo->prepare($sql); $stmt = $this->pdo->prepare($this->translate($sql));
$stmt->execute($params); $stmt->execute($params);
return $stmt; return $stmt;
} }
+3 -3
View File
@@ -9,15 +9,15 @@ function novacpx_get_branding(): array {
$cfg = @parse_ini_file('/etc/novacpx/config.ini', true); $cfg = @parse_ini_file('/etc/novacpx/config.ini', true);
if (!$cfg) return $cache = []; if (!$cfg) return $cache = [];
try { try {
$dbPath = $cfg['database']['path'] ?? '/var/lib/novacpx/panel.db';
$pdo = new PDO( $pdo = new PDO(
"mysql:host={$cfg['database']['host']};dbname={$cfg['database']['name']};charset=utf8mb4", "sqlite:{$dbPath}", null, null,
$cfg['database']['user'], $cfg['database']['pass'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] [PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
); );
$token = $_COOKIE['ncpx_session'] ?? ''; $token = $_COOKIE['ncpx_session'] ?? '';
if (!$token || strlen($token) < 32) return $cache = []; if (!$token || strlen($token) < 32) return $cache = [];
$stmt = $pdo->prepare("SELECT user_id FROM sessions WHERE token = ? AND expires_at > NOW() LIMIT 1"); $stmt = $pdo->prepare("SELECT user_id FROM sessions WHERE token = ? AND expires_at > datetime('now') LIMIT 1");
$stmt->execute([substr($token, 0, 128)]); $stmt->execute([substr($token, 0, 128)]);
$uid = (int)($stmt->fetchColumn() ?: 0); $uid = (int)($stmt->fetchColumn() ?: 0);
if (!$uid) return $cache = []; if (!$uid) return $cache = [];
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env bash
# Migrate NovaCPX panel DB from MySQL to SQLite
# Run as root on the NovaCPX VM.
# Usage: bash tools/migrate-to-sqlite.sh [--schema-only]
set -euo pipefail
SCHEMA_ONLY=false
[[ "${1:-}" == "--schema-only" ]] && SCHEMA_ONLY=true
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
DB_PATH="/var/lib/novacpx/panel.db"
SCHEMA="$REPO_ROOT/db/schema.sql"
CONFIG="/etc/novacpx/config.ini"
BACKUP_DIR="/var/lib/novacpx/migration-backup-$(date +%Y%m%d-%H%M%S)"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
log() { echo -e "${GREEN}[✓]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
fail() { echo -e "${RED}[✗]${NC} $*"; exit 1; }
[[ $EUID -ne 0 ]] && fail "Run as root."
command -v sqlite3 >/dev/null 2>&1 || { apt-get install -y sqlite3 >> /dev/null; log "sqlite3 installed"; }
# ── Read MySQL creds from existing config ─────────────────────────────────────
read_ini() { grep -E "^\s*$1\s*=" "$CONFIG" | head -1 | cut -d= -f2- | tr -d ' '; }
DB_HOST=$(read_ini host)
DB_NAME=$(read_ini name)
DB_USER=$(read_ini user)
DB_PASS=$(read_ini pass)
DB_WP_USER=$(read_ini wp_user)
DB_WP_PASS=$(read_ini wp_pass)
SECRET_KEY=$(read_ini secret)
if [[ -z "$DB_NAME" ]]; then
warn "No MySQL config found in $CONFIG — this install may already be on SQLite."
warn "If you only need to (re)create the SQLite DB from schema, use --schema-only."
[[ "$SCHEMA_ONLY" == "false" ]] && exit 0
fi
# ── Backup ────────────────────────────────────────────────────────────────────
log "Backing up to $BACKUP_DIR..."
mkdir -p "$BACKUP_DIR"
[[ -f "$DB_PATH" ]] && cp "$DB_PATH" "$BACKUP_DIR/panel.db.bak"
cp "$CONFIG" "$BACKUP_DIR/config.ini.bak"
[[ -n "$DB_NAME" ]] && mysqldump -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$BACKUP_DIR/mysql-dump.sql" 2>/dev/null && log "MySQL dump saved" || warn "MySQL dump failed (service may be down)"
# ── Create SQLite DB from schema ──────────────────────────────────────────────
log "Creating SQLite database at $DB_PATH..."
mkdir -p /var/lib/novacpx
rm -f "$DB_PATH"
sqlite3 "$DB_PATH" < "$SCHEMA"
log "Schema applied"
if [[ "$SCHEMA_ONLY" == "false" && -n "$DB_NAME" ]]; then
# ── Migrate data from MySQL → SQLite ─────────────────────────────────────────
log "Migrating data from MySQL ($DB_NAME)..."
TABLES=(
users settings email_domains email_accounts dns_zones dns_records
accounts packages features account_features
ssl_certificates backups backup_schedules
ftp_accounts cron_jobs databases db_users
wordpress_installs
resellers reseller_branding reseller_packages
ip_addresses ssh_keys firewall_rules
docker_containers docker_compose_stacks docker_quotas
webmail_sso_tokens audit_log
)
for TABLE in "${TABLES[@]}"; do
# Check table exists in MySQL
ROWS=$(mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -se "SELECT COUNT(*) FROM \`$TABLE\`;" 2>/dev/null) || { warn "Table $TABLE not found in MySQL — skipping"; continue; }
[[ "$ROWS" == "0" ]] && { log " $TABLE — empty, skip"; continue; }
# Get column names from MySQL for this table
MYSQL_COLS=$(mysql -u "$DB_USER" -p"$DB_PASS" --skip-column-names --batch "$DB_NAME" \
-e "SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='${DB_NAME}' AND TABLE_NAME='${TABLE}' ORDER BY ORDINAL_POSITION;" 2>/dev/null)
[[ -z "$MYSQL_COLS" ]] && { warn " $TABLE: could not get MySQL columns, skipping"; continue; }
# Dump only the columns that exist in both MySQL and SQLite
mysql -u "$DB_USER" -p"$DB_PASS" --skip-column-names --batch "$DB_NAME" \
-e "SELECT $(echo "$MYSQL_COLS" | tr '\n' ',' | sed 's/,$//') FROM \`$TABLE\`" 2>/dev/null | \
python3 - "$TABLE" "$DB_PATH" "$MYSQL_COLS" <<'PYEOF'
import sys, sqlite3, csv, io
table = sys.argv[1]
db_path = sys.argv[2]
mysql_cols = [c for c in sys.argv[3].splitlines() if c]
data = sys.stdin.read()
if not data.strip():
sys.exit(0)
con = sqlite3.connect(db_path)
cur = con.cursor()
cur.execute(f"SELECT name FROM pragma_table_info('{table}')")
sqlite_cols = {r[0] for r in cur.fetchall()}
if not sqlite_cols:
print(f" {table}: not in SQLite schema, skipping", file=sys.stderr)
con.close()
sys.exit(0)
# Only insert columns present in both
common = [c for c in mysql_cols if c in sqlite_cols]
col_idx = [i for i, c in enumerate(mysql_cols) if c in sqlite_cols]
placeholders = ','.join(['?'] * len(common))
col_list = ','.join(common)
reader = csv.reader(io.StringIO(data), delimiter='\t')
rows = list(reader)
inserted = 0
for row in rows:
if len(row) != len(mysql_cols):
continue
vals = [None if row[i] == 'NULL' else row[i] for i in col_idx]
try:
cur.execute(f"INSERT OR IGNORE INTO {table} ({col_list}) VALUES ({placeholders})", vals)
inserted += 1
except Exception as e:
pass
con.commit()
con.close()
print(f" {table}: {inserted}/{len(rows)} rows migrated ({len(common)} cols)")
PYEOF
done
log "Data migration complete"
fi
# ── Update config.ini ─────────────────────────────────────────────────────────
log "Updating $CONFIG to use SQLite..."
# Build new [database] section
NEW_DB_SECTION="[database]
path = ${DB_PATH}
wp_user = ${DB_WP_USER}
wp_pass = ${DB_WP_PASS}"
# Replace everything between [database] and the next [section]
python3 - "$CONFIG" "$NEW_DB_SECTION" <<'PYEOF'
import sys, re
config_path = sys.argv[1]
new_section = sys.argv[2]
with open(config_path) as f:
content = f.read()
# Replace the [database] section with new content
content = re.sub(
r'\[database\].*?(?=\[|\Z)',
new_section + '\n\n',
content,
flags=re.DOTALL
)
with open(config_path, 'w') as f:
f.write(content)
print("config.ini updated")
PYEOF
chown root:www-data "$CONFIG"
chmod 640 "$CONFIG"
# ── Fix permissions ───────────────────────────────────────────────────────────
chown www-data:www-data "$DB_PATH"
chmod 660 "$DB_PATH"
log "Migration complete!"
log "Panel DB: $DB_PATH"
log "Backup: $BACKUP_DIR"
warn "Restart PHP-FPM to clear any cached DB connections:"
echo " systemctl restart php8.3-fpm php8.2-fpm php8.1-fpm php7.4-fpm 2>/dev/null || true"