mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
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:
+450
-324
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = [];
|
||||||
|
|||||||
Executable
+175
@@ -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"
|
||||||
Reference in New Issue
Block a user