mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 09:40:38 -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
|
||||
-- Engine: MySQL 8+ | Charset: utf8mb4_unicode_ci
|
||||
-- NovaCPX Database Schema v1.1.0
|
||||
-- Engine: SQLite 3.35+
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET foreign_key_checks = 0;
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
-- ── Version tracking ──────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS novacpx_version (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
version VARCHAR(20) NOT NULL,
|
||||
installed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
version TEXT NOT NULL,
|
||||
installed_at TEXT DEFAULT (datetime('now')),
|
||||
notes TEXT,
|
||||
git_commit VARCHAR(64),
|
||||
INDEX idx_version (version)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
git_commit TEXT
|
||||
);
|
||||
|
||||
INSERT INTO novacpx_version (version, notes, git_commit)
|
||||
VALUES ('1.0.0', 'Initial installation', 'HEAD');
|
||||
INSERT OR IGNORE INTO novacpx_version (version, notes, git_commit)
|
||||
VALUES ('1.1.0', 'Initial installation', 'HEAD');
|
||||
|
||||
-- ── Audit log (every action tracked) ─────────────────────────────────────────
|
||||
-- ── Audit log ─────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED,
|
||||
username VARCHAR(100),
|
||||
action VARCHAR(100) NOT NULL,
|
||||
resource VARCHAR(200),
|
||||
detail JSON,
|
||||
ip_address VARCHAR(45),
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
username TEXT,
|
||||
action TEXT NOT NULL,
|
||||
resource TEXT,
|
||||
detail TEXT,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_action (action),
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action);
|
||||
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 (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
role ENUM('admin','reseller','user') DEFAULT 'user',
|
||||
status ENUM('active','suspended','pending') DEFAULT 'active',
|
||||
reseller_id INT UNSIGNED DEFAULT NULL,
|
||||
package_id INT UNSIGNED DEFAULT NULL,
|
||||
theme VARCHAR(50) DEFAULT 'nova-dark',
|
||||
language VARCHAR(10) DEFAULT 'en',
|
||||
contact_name VARCHAR(200),
|
||||
contact_phone VARCHAR(50),
|
||||
last_login DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (reseller_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
INDEX idx_role (role),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_reseller (reseller_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
role TEXT NOT NULL DEFAULT 'user' CHECK(role IN ('admin','reseller','user')),
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','suspended','pending')),
|
||||
reseller_id INTEGER DEFAULT NULL,
|
||||
package_id INTEGER DEFAULT NULL,
|
||||
theme TEXT DEFAULT 'nova-dark',
|
||||
language TEXT DEFAULT 'en',
|
||||
contact_name TEXT,
|
||||
contact_phone TEXT,
|
||||
last_login TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT,
|
||||
totp_secret TEXT,
|
||||
totp_enabled INTEGER DEFAULT 0,
|
||||
totp_backup_codes TEXT,
|
||||
FOREIGN KEY (reseller_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
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 ──────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id VARCHAR(128) PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
ip_address VARCHAR(45),
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
data JSON,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_expires (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
data TEXT,
|
||||
expires_at TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions (expires_at);
|
||||
|
||||
-- ── Packages / Hosting Plans ──────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS packages (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
owner_id INT UNSIGNED DEFAULT NULL, -- NULL = system, or reseller
|
||||
disk_mb INT UNSIGNED DEFAULT 1024,
|
||||
bandwidth_mb BIGINT UNSIGNED DEFAULT 10240,
|
||||
max_domains SMALLINT UNSIGNED DEFAULT 1,
|
||||
max_subdomains SMALLINT UNSIGNED DEFAULT 10,
|
||||
max_addon_domains SMALLINT UNSIGNED DEFAULT 0,
|
||||
max_parked_domains SMALLINT UNSIGNED DEFAULT 5,
|
||||
max_email SMALLINT UNSIGNED DEFAULT 10,
|
||||
max_ftp SMALLINT UNSIGNED DEFAULT 5,
|
||||
max_databases SMALLINT UNSIGNED DEFAULT 5,
|
||||
php_version VARCHAR(10) DEFAULT '8.3',
|
||||
ssl_enabled TINYINT(1) DEFAULT 1,
|
||||
is_default TINYINT(1) DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
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;
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
owner_id INTEGER DEFAULT NULL,
|
||||
disk_mb INTEGER DEFAULT 1024,
|
||||
bandwidth_mb INTEGER DEFAULT 10240,
|
||||
max_domains INTEGER DEFAULT 1,
|
||||
max_subdomains INTEGER DEFAULT 10,
|
||||
max_addon_domains INTEGER DEFAULT 0,
|
||||
max_parked_domains INTEGER DEFAULT 5,
|
||||
max_email INTEGER DEFAULT 10,
|
||||
max_ftp INTEGER DEFAULT 5,
|
||||
max_databases INTEGER DEFAULT 5,
|
||||
php_version TEXT DEFAULT '8.3',
|
||||
ssl_enabled INTEGER DEFAULT 1,
|
||||
is_default INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
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)
|
||||
VALUES ('Default', 5120, 51200, 5, 25, 10, 1);
|
||||
INSERT OR IGNORE INTO packages (id, name, disk_mb, bandwidth_mb, max_domains, max_email, max_databases, is_default)
|
||||
VALUES (1, 'Default', 5120, 51200, 5, 25, 10, 1);
|
||||
|
||||
-- ── Hosting Accounts ──────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL UNIQUE,
|
||||
username VARCHAR(32) NOT NULL UNIQUE,
|
||||
domain VARCHAR(253) NOT NULL,
|
||||
home_dir VARCHAR(500) NOT NULL,
|
||||
package_id INT UNSIGNED,
|
||||
disk_used_mb INT UNSIGNED DEFAULT 0,
|
||||
bw_used_mb BIGINT UNSIGNED DEFAULT 0,
|
||||
php_version VARCHAR(10) DEFAULT '8.3',
|
||||
web_server ENUM('apache','nginx') DEFAULT 'apache',
|
||||
status ENUM('active','suspended','terminated') DEFAULT 'active',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
suspended_at DATETIME DEFAULT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE SET NULL,
|
||||
INDEX idx_domain (domain),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL UNIQUE,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
domain TEXT NOT NULL,
|
||||
home_dir TEXT NOT NULL,
|
||||
document_root TEXT,
|
||||
system_user TEXT DEFAULT 'www-data',
|
||||
package_id INTEGER,
|
||||
disk_used_mb INTEGER DEFAULT 0,
|
||||
bw_used_mb INTEGER DEFAULT 0,
|
||||
php_version TEXT DEFAULT '8.3',
|
||||
web_server TEXT DEFAULT 'apache' CHECK(web_server IN ('apache','nginx')),
|
||||
status TEXT DEFAULT 'active' CHECK(status IN ('active','suspended','terminated')),
|
||||
cf_api_key TEXT,
|
||||
cf_api_email TEXT,
|
||||
cf_zone_id TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
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 ───────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS domains (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED NOT NULL,
|
||||
domain VARCHAR(253) NOT NULL,
|
||||
type ENUM('main','addon','subdomain','parked','alias') DEFAULT 'main',
|
||||
document_root VARCHAR(500),
|
||||
php_version VARCHAR(10),
|
||||
ssl_enabled TINYINT(1) DEFAULT 0,
|
||||
ssl_cert TEXT,
|
||||
ssl_key TEXT,
|
||||
ssl_chain TEXT,
|
||||
ssl_expires DATE,
|
||||
redirect_to VARCHAR(500) DEFAULT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uq_domain (domain),
|
||||
INDEX idx_account (account_id),
|
||||
INDEX idx_type (type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL,
|
||||
domain TEXT NOT NULL UNIQUE,
|
||||
type TEXT DEFAULT 'main' CHECK(type IN ('main','addon','subdomain','parked','alias')),
|
||||
document_root TEXT,
|
||||
php_version TEXT,
|
||||
ssl_enabled INTEGER DEFAULT 0,
|
||||
ssl_cert TEXT,
|
||||
ssl_key TEXT,
|
||||
ssl_chain TEXT,
|
||||
ssl_expires TEXT,
|
||||
redirect_to TEXT DEFAULT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_domains_account ON domains (account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_domains_type ON domains (type);
|
||||
|
||||
-- ── DNS Zones & Records ───────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS dns_zones (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED NOT NULL,
|
||||
domain VARCHAR(253) NOT NULL UNIQUE,
|
||||
serial BIGINT UNSIGNED DEFAULT 1,
|
||||
primary_ns VARCHAR(253) DEFAULT 'ns1.localhost',
|
||||
secondary_ns VARCHAR(253) DEFAULT 'ns2.localhost',
|
||||
admin_email VARCHAR(255) DEFAULT 'hostmaster@localhost',
|
||||
ttl INT UNSIGNED DEFAULT 3600,
|
||||
refresh INT UNSIGNED DEFAULT 86400,
|
||||
retry INT UNSIGNED DEFAULT 7200,
|
||||
expire INT UNSIGNED DEFAULT 2419200,
|
||||
minimum INT UNSIGNED DEFAULT 86400,
|
||||
status ENUM('active','disabled') DEFAULT 'active',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
INDEX idx_account (account_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL,
|
||||
domain TEXT NOT NULL UNIQUE,
|
||||
serial INTEGER DEFAULT 1,
|
||||
primary_ns TEXT DEFAULT 'ns1.localhost',
|
||||
secondary_ns TEXT DEFAULT 'ns2.localhost',
|
||||
admin_email TEXT DEFAULT 'hostmaster@localhost',
|
||||
ttl INTEGER DEFAULT 3600,
|
||||
refresh INTEGER DEFAULT 86400,
|
||||
retry INTEGER DEFAULT 7200,
|
||||
expire INTEGER DEFAULT 2419200,
|
||||
minimum INTEGER DEFAULT 86400,
|
||||
status TEXT DEFAULT 'active' CHECK(status IN ('active','disabled')),
|
||||
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_dns_zones_account ON dns_zones (account_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dns_records (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
zone_id INT UNSIGNED NOT NULL,
|
||||
name VARCHAR(253) NOT NULL,
|
||||
type ENUM('A','AAAA','CNAME','MX','TXT','SRV','NS','PTR','CAA','DKIM','SPF','DMARC') NOT NULL,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
zone_id INTEGER NOT NULL,
|
||||
name TEXT 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,
|
||||
ttl INT UNSIGNED DEFAULT 3600,
|
||||
priority SMALLINT UNSIGNED DEFAULT NULL,
|
||||
proxied TINYINT(1) DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (zone_id) REFERENCES dns_zones(id) ON DELETE CASCADE,
|
||||
INDEX idx_zone (zone_id),
|
||||
INDEX idx_type (type),
|
||||
INDEX idx_name (name(100))
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
ttl INTEGER DEFAULT 3600,
|
||||
priority INTEGER DEFAULT NULL,
|
||||
proxied INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (zone_id) REFERENCES dns_zones(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_dns_records_zone ON dns_records (zone_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_dns_records_type ON dns_records (type);
|
||||
|
||||
-- ── Email Accounts & Forwarders ───────────────────────────────────────────────
|
||||
-- ── Email ─────────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS email_accounts (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED NOT NULL,
|
||||
email VARCHAR(320) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
quota_mb INT UNSIGNED DEFAULT 500,
|
||||
used_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,
|
||||
INDEX idx_account (account_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
quota_mb INTEGER DEFAULT 500,
|
||||
used_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
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_email_accounts_account ON email_accounts (account_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS email_forwarders (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED NOT NULL,
|
||||
source VARCHAR(320) NOT NULL,
|
||||
destination VARCHAR(320) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
destination TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
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 (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED NOT NULL,
|
||||
email VARCHAR(320) NOT NULL,
|
||||
subject VARCHAR(255),
|
||||
body TEXT,
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
start_at DATETIME,
|
||||
stop_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
subject TEXT,
|
||||
body TEXT,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
start_at TEXT,
|
||||
stop_at TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
);
|
||||
|
||||
-- ── Databases ─────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS databases (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED NOT NULL,
|
||||
db_name VARCHAR(100) NOT NULL,
|
||||
db_user VARCHAR(100) NOT NULL,
|
||||
db_pass VARCHAR(255) NOT NULL,
|
||||
db_type ENUM('mysql','postgresql') DEFAULT 'mysql',
|
||||
size_mb FLOAT DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
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,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL,
|
||||
db_name TEXT NOT NULL,
|
||||
db_user TEXT NOT NULL,
|
||||
db_pass TEXT NOT NULL,
|
||||
db_type TEXT DEFAULT 'mysql' CHECK(db_type IN ('mysql','postgresql')),
|
||||
size_mb REAL DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
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 ──────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS ssl_certs (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED NOT NULL,
|
||||
domain VARCHAR(253) NOT NULL,
|
||||
type ENUM('lets_encrypt','self_signed','custom') DEFAULT 'lets_encrypt',
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
type TEXT DEFAULT 'lets_encrypt' CHECK(type IN ('lets_encrypt','self_signed','custom')),
|
||||
cert TEXT,
|
||||
private_key TEXT,
|
||||
chain TEXT,
|
||||
issued_at DATETIME,
|
||||
expires_at DATETIME,
|
||||
auto_renew TINYINT(1) DEFAULT 1,
|
||||
status ENUM('active','expired','pending','failed') DEFAULT 'pending',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
INDEX idx_domain (domain),
|
||||
INDEX idx_expires (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
issued_at TEXT,
|
||||
expires_at TEXT,
|
||||
auto_renew INTEGER DEFAULT 1,
|
||||
status TEXT DEFAULT 'pending' CHECK(status IN ('active','expired','pending','failed')),
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssl_certs_domain ON ssl_certs (domain);
|
||||
CREATE INDEX IF NOT EXISTS idx_ssl_certs_expires ON ssl_certs (expires_at);
|
||||
|
||||
-- ── Cron Jobs ─────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS cron_jobs (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED NOT NULL,
|
||||
command TEXT NOT NULL,
|
||||
minute VARCHAR(20) DEFAULT '*',
|
||||
hour VARCHAR(20) DEFAULT '*',
|
||||
day VARCHAR(20) DEFAULT '*',
|
||||
month VARCHAR(20) DEFAULT '*',
|
||||
weekday VARCHAR(20) DEFAULT '*',
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
last_run DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL,
|
||||
command TEXT NOT NULL,
|
||||
minute TEXT DEFAULT '*',
|
||||
hour TEXT DEFAULT '*',
|
||||
day TEXT DEFAULT '*',
|
||||
month TEXT DEFAULT '*',
|
||||
weekday TEXT DEFAULT '*',
|
||||
is_active INTEGER DEFAULT 1,
|
||||
last_run TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
);
|
||||
|
||||
-- ── PHP Configuration ─────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS php_configs (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED NOT NULL UNIQUE,
|
||||
php_version VARCHAR(10) DEFAULT '8.3',
|
||||
memory_limit VARCHAR(20) DEFAULT '256M',
|
||||
max_execution_time INT DEFAULT 30,
|
||||
upload_max_filesize VARCHAR(20) DEFAULT '64M',
|
||||
post_max_size VARCHAR(20) DEFAULT '64M',
|
||||
max_input_vars INT DEFAULT 1000,
|
||||
display_errors TINYINT(1) DEFAULT 0,
|
||||
error_reporting VARCHAR(50) DEFAULT 'E_ALL & ~E_NOTICE',
|
||||
extensions JSON,
|
||||
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL UNIQUE,
|
||||
php_version TEXT DEFAULT '8.3',
|
||||
memory_limit TEXT DEFAULT '256M',
|
||||
max_execution_time INTEGER DEFAULT 30,
|
||||
upload_max_filesize TEXT DEFAULT '64M',
|
||||
post_max_size TEXT DEFAULT '64M',
|
||||
max_input_vars INTEGER DEFAULT 1000,
|
||||
display_errors INTEGER DEFAULT 0,
|
||||
error_reporting TEXT DEFAULT 'E_ALL & ~E_NOTICE',
|
||||
extensions TEXT,
|
||||
updated_at TEXT,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
);
|
||||
|
||||
-- ── Backups ───────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS backups (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED NOT NULL,
|
||||
filename VARCHAR(500) NOT NULL,
|
||||
size_mb FLOAT DEFAULT 0,
|
||||
type ENUM('full','partial','db_only','files_only') DEFAULT 'full',
|
||||
status ENUM('pending','running','complete','failed') DEFAULT 'pending',
|
||||
storage ENUM('local','s3','ftp','sftp') DEFAULT 'local',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at DATETIME,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
INDEX idx_account (account_id),
|
||||
INDEX idx_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
size_mb REAL DEFAULT 0,
|
||||
type TEXT DEFAULT 'full' CHECK(type IN ('full','partial','db_only','files_only')),
|
||||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending','running','complete','failed')),
|
||||
storage TEXT DEFAULT 'local' CHECK(storage IN ('local','s3','ftp','sftp')),
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
completed_at TEXT,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_backups_account ON backups (account_id);
|
||||
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 (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
cpu_pct FLOAT,
|
||||
ram_pct FLOAT,
|
||||
disk_pct FLOAT,
|
||||
load_1m FLOAT,
|
||||
load_5m FLOAT,
|
||||
load_15m FLOAT,
|
||||
net_in_kb BIGINT UNSIGNED DEFAULT 0,
|
||||
net_out_kb BIGINT UNSIGNED DEFAULT 0,
|
||||
recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_recorded (recorded_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cpu_pct REAL,
|
||||
ram_pct REAL,
|
||||
disk_pct REAL,
|
||||
load_1m REAL,
|
||||
load_5m REAL,
|
||||
load_15m REAL,
|
||||
net_in_kb INTEGER DEFAULT 0,
|
||||
net_out_kb INTEGER DEFAULT 0,
|
||||
recorded_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_server_stats_recorded ON server_stats (recorded_at);
|
||||
|
||||
-- ── Notifications ─────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
type ENUM('info','success','warning','error') DEFAULT 'info',
|
||||
is_read TINYINT(1) DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_unread (is_read)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
title TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
type TEXT DEFAULT 'info' CHECK(type IN ('info','success','warning','error')),
|
||||
is_read INTEGER DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications (is_read);
|
||||
|
||||
-- ── API Tokens ────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS api_tokens (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
token VARCHAR(128) NOT NULL UNIQUE,
|
||||
permissions JSON,
|
||||
last_used DATETIME,
|
||||
expires_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_token (token)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
permissions TEXT,
|
||||
last_used TEXT,
|
||||
expires_at TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_tokens_token ON api_tokens (token);
|
||||
|
||||
-- ── Settings ──────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
`key` VARCHAR(100) PRIMARY KEY,
|
||||
`value` TEXT,
|
||||
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`key` TEXT PRIMARY KEY,
|
||||
`value` TEXT,
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
INSERT INTO settings (`key`, `value`) VALUES
|
||||
('panel_name', 'NovaCPX'),
|
||||
('panel_version', '1.0.0'),
|
||||
('default_nameserver1', 'ns1.localhost'),
|
||||
('default_nameserver2', 'ns2.localhost'),
|
||||
('default_php', '8.3'),
|
||||
('mail_enabled', '1'),
|
||||
('ftp_enabled', '1'),
|
||||
('dns_enabled', '1'),
|
||||
('backup_dir', '/var/novacpx/backups'),
|
||||
('update_channel', 'stable'),
|
||||
('git_remote', 'https://github.com/myronblair/novacpx.git')
|
||||
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);
|
||||
INSERT OR IGNORE INTO settings (`key`, `value`) VALUES
|
||||
('panel_name', 'NovaCPX'),
|
||||
('panel_version', '1.1.0'),
|
||||
('default_nameserver1', 'ns1.localhost'),
|
||||
('default_nameserver2', 'ns2.localhost'),
|
||||
('default_php', '8.3'),
|
||||
('mail_enabled', '1'),
|
||||
('ftp_enabled', '1'),
|
||||
('dns_enabled', '1'),
|
||||
('backup_dir', '/var/novacpx/backups'),
|
||||
('update_channel', 'stable'),
|
||||
('git_remote', 'https://github.com/myronblair/novacpx.git'),
|
||||
('proxy_mode', 'disabled'),
|
||||
('proxy_apache_port', '80');
|
||||
|
||||
-- ── DKIM Keys ─────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS dkim_keys (
|
||||
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`account_id` INT UNSIGNED NOT NULL,
|
||||
`domain` VARCHAR(253) NOT NULL,
|
||||
`selector` VARCHAR(63) NOT NULL DEFAULT 'mail',
|
||||
`public_key` TEXT NOT NULL,
|
||||
`private_key_path` VARCHAR(500) NOT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_domain (domain),
|
||||
CONSTRAINT fk_dkim_acct FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER NOT NULL,
|
||||
domain TEXT NOT NULL UNIQUE,
|
||||
selector TEXT NOT NULL DEFAULT 'mail',
|
||||
public_key TEXT NOT NULL,
|
||||
private_key_path TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- ── Rate Limits ───────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS api_rate_limits (
|
||||
ip VARCHAR(45) NOT NULL,
|
||||
endpoint VARCHAR(32) NOT NULL,
|
||||
hits INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
window_start INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
ip TEXT NOT NULL,
|
||||
endpoint TEXT NOT NULL,
|
||||
hits INTEGER NOT NULL DEFAULT 1,
|
||||
window_start INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (ip, endpoint)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
);
|
||||
|
||||
-- ── Proxy Hosts ───────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS proxy_hosts (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account_id INT UNSIGNED,
|
||||
domain VARCHAR(253) NOT NULL,
|
||||
upstream VARCHAR(255) NOT NULL,
|
||||
ssl_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id INTEGER,
|
||||
domain TEXT NOT NULL UNIQUE,
|
||||
upstream TEXT NOT NULL,
|
||||
ssl_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
custom_config TEXT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_domain (domain),
|
||||
CONSTRAINT fk_proxy_acct FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
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.']));
|
||||
}
|
||||
|
||||
define('DB_HOST', $_cfg['database']['host'] ?? 'localhost');
|
||||
define('DB_NAME', $_cfg['database']['name'] ?? 'novacpx');
|
||||
define('DB_USER', $_cfg['database']['user'] ?? '');
|
||||
define('DB_PASS', $_cfg['database']['pass'] ?? '');
|
||||
define('DB_PATH', $_cfg['database']['path'] ?? '/var/lib/novacpx/panel.db');
|
||||
define('DB_WP_USER', $_cfg['database']['wp_user'] ?? '');
|
||||
define('DB_WP_PASS', $_cfg['database']['wp_pass'] ?? '');
|
||||
define('SECRET_KEY', $_cfg['panel']['secret'] ?? '');
|
||||
define('PANEL_VER', $_cfg['panel']['version'] ?? NOVACPX_VERSION);
|
||||
define('PORT_USER', (int)($_cfg['panel']['port_user'] ?? 8880));
|
||||
|
||||
+82
-4
@@ -4,15 +4,21 @@ class DB {
|
||||
private PDO $pdo;
|
||||
|
||||
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(
|
||||
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
|
||||
DB_USER, DB_PASS,
|
||||
"sqlite:{$path}",
|
||||
null, null,
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
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 {
|
||||
@@ -20,8 +26,80 @@ class DB {
|
||||
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 {
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt = $this->pdo->prepare($this->translate($sql));
|
||||
$stmt->execute($params);
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@ function novacpx_get_branding(): array {
|
||||
$cfg = @parse_ini_file('/etc/novacpx/config.ini', true);
|
||||
if (!$cfg) return $cache = [];
|
||||
try {
|
||||
$dbPath = $cfg['database']['path'] ?? '/var/lib/novacpx/panel.db';
|
||||
$pdo = new PDO(
|
||||
"mysql:host={$cfg['database']['host']};dbname={$cfg['database']['name']};charset=utf8mb4",
|
||||
$cfg['database']['user'], $cfg['database']['pass'],
|
||||
"sqlite:{$dbPath}", null, null,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
$token = $_COOKIE['ncpx_session'] ?? '';
|
||||
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)]);
|
||||
$uid = (int)($stmt->fetchColumn() ?: 0);
|
||||
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