From 870ec062f062820309cf0d155aef459d567a7738 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Sun, 7 Jun 2026 06:08:32 +0000 Subject: [PATCH] =?UTF-8?q?Add=20complete=20user=20and=20reseller=20panel?= =?UTF-8?q?=20JS=20=E2=80=94=20all=20pages=20fully=20implemented?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User panel (user.js): dashboard with usage rings, domains+SSL, email accounts+forwarders, databases, FTP, SSL manager, PHP switcher, cron jobs, file manager (edit/upload/chmod), stats Reseller panel (reseller.js): dashboard, accounts list+search+suspend/terminate, create account form, packages CRUD, DNS zones editor Both panels: dynamic sidebar nav using nova-icons.svg sprite, inline auth guard Co-Authored-By: Claude Sonnet 4.6 --- panel/public/assets/js/reseller.js | 329 ++++++++++++ panel/public/assets/js/user.js | 804 +++++++++++++++++++++++++++++ panel/public/reseller/index.php | 115 +---- panel/public/user/index.php | 7 +- 4 files changed, 1140 insertions(+), 115 deletions(-) create mode 100644 panel/public/assets/js/reseller.js create mode 100644 panel/public/assets/js/user.js diff --git a/panel/public/assets/js/reseller.js b/panel/public/assets/js/reseller.js new file mode 100644 index 0000000..e038804 --- /dev/null +++ b/panel/public/assets/js/reseller.js @@ -0,0 +1,329 @@ +/** + * NovaCPX Reseller Panel JS + */ + +let _rUser = null; + +async function initReseller() { + const res = await Nova.api('auth', 'me'); + if (!res?.success || !['admin','reseller'].includes(res.data?.role)) { + document.getElementById('auth-check').innerHTML = renderLogin(); + document.getElementById('main-layout').style.display = 'none'; + return false; + } + _rUser = res.data; + document.getElementById('user-name').textContent = _rUser.username || 'Reseller'; + return true; +} + +function renderLogin() { + return ``; +} + +async function doLogin() { + const res = await Nova.api('auth', 'login', { method: 'POST', body: { username: document.getElementById('li-user')?.value, password: document.getElementById('li-pass')?.value }}); + if (res?.success) { + if (res.data?.portal_url && !res.data.portal_url.includes(':8881')) location.href = res.data.portal_url; + else location.reload(); + } else { + const err = document.getElementById('li-err'); + if (err) { err.textContent = res?.message || 'Login failed'; err.style.display = 'block'; } + } +} +window.doLogin = doLogin; + +/* ── Pages ─────────────────────────────────────────────────────────────── */ + +async function rDashboard(el) { + el.innerHTML = ` +
Loading…
+
+
Recent Accounts + +
+
Loading…
+
`; + + const res = await Nova.api('accounts', 'list', { params:{ limit:5 }}); + const accts = res?.data?.accounts || []; + + document.getElementById('r-stats').innerHTML = [ + { label: 'Total Accounts', val: res?.data?.total || 0, icon: 'ni-accounts' }, + { label: 'Active', val: accts.filter(a=>a.status==='active').length, icon: 'ni-stats' }, + { label: 'Suspended', val: accts.filter(a=>a.status==='suspended').length, icon: 'ni-suspend' }, + ].map(s => `
+ +
${s.val}
${s.label}
+
`).join(''); + + document.getElementById('r-recent').innerHTML = accts.length + ? ` + ${accts.map(a => ` + + + `).join('')} +
UsernameDomainPackageStatus
${a.username}${a.domain}${a.package_name||'—'}${Nova.badge(a.status, a.status==='active'?'green':'yellow')}
` + : '
No accounts yet.
'; +} + +async function rAccounts(el) { + el.innerHTML = ` +
+
+ +
+
Loading…
+
`; + loadRAccounts(); +} + +async function loadRAccounts(search = '') { + const el = document.getElementById('r-accounts-list'); + if (!el) return; + const res = await Nova.api('accounts', 'list', { params: search ? { search } : {}}); + if (!res?.success || !res.data.accounts.length) { el.innerHTML = '
No accounts found.
'; return; } + el.innerHTML = ` + ${res.data.accounts.map(a => ` + + + + + + + `).join('')} +
UsernameDomainPackageDiskStatusActions
${a.username}${a.domain}${a.package_name || '—'}${a.disk_usage_mb || 0} MB${Nova.badge(a.status, a.status==='active'?'green':a.status==='suspended'?'yellow':'red')} + ${a.status === 'active' + ? `` + : ``} + + +
`; +} +window.loadRAccounts = loadRAccounts; +window.rSearchAccounts = (v) => loadRAccounts(v); + +window.rSuspend = async (id, user) => { + Nova.confirm(`Suspend account ${user}? Their website will show a suspension page.`, async () => { + const res = await Nova.api('accounts', 'suspend', { method:'POST', body:{ account_id: id }}); + if (res?.success) { Nova.toast('Account suspended','success'); loadRAccounts(); } + else Nova.toast(res?.message,'error'); + }); +}; +window.rUnsuspend = async (id, user) => { + const res = await Nova.api('accounts', 'unsuspend', { method:'POST', body:{ account_id: id }}); + if (res?.success) { Nova.toast('Account unsuspended','success'); loadRAccounts(); } + else Nova.toast(res?.message,'error'); +}; +window.rTerminate = (id, user) => { + Nova.confirm(`TERMINATE ${user}? This permanently deletes all files, databases, DNS, and email. THIS CANNOT BE UNDONE.`, async () => { + const res = await Nova.api('accounts', 'terminate', { method:'POST', body:{ account_id: id }}); + if (res?.success) { Nova.toast('Account terminated','success'); loadRAccounts(); } + else Nova.toast(res?.message,'error'); + }, true); +}; +window.rChangePass = (id, user) => { + Nova.modal(`Change Password — ${user}`, `
`, + ``); +}; + +async function rCreateAccount(el) { + el.innerHTML = ` +
+
+
+
+
+
+
+
+ + +
+
+
+
`; + + Nova.api('packages', 'list').then(res => { + const sel = document.getElementById('ca-pkg'); + if (sel && res?.success) { + sel.innerHTML = res.data.map(p => ``).join(''); + } + }); +} + +window.submitCreateAccount = async () => { + const btn = document.querySelector('#ca-result'); + if (btn) btn.textContent = ''; + const res = await Nova.api('accounts', 'create', { method:'POST', body:{ + username: document.getElementById('ca-user')?.value, + password: document.getElementById('ca-pass')?.value, + email: document.getElementById('ca-email')?.value, + domain: document.getElementById('ca-domain')?.value, + package_id: document.getElementById('ca-pkg')?.value, + }}); + if (res?.success) { + Nova.toast('Account created successfully!','success'); + if (btn) btn.innerHTML = `
Account created! View accounts →
`; + } else { + Nova.toast(res?.message || 'Failed to create account','error'); + if (btn) btn.innerHTML = `
${res?.message || 'Error'}
`; + } +}; + +async function rPackages(el) { + el.innerHTML = ` +
Loading…
`; + + const res = await Nova.api('packages', 'list'); + const plist = document.getElementById('pkg-list'); + if (!res?.success || !res.data.length) { plist.innerHTML = '
No packages yet.
'; return; } + plist.innerHTML = ` + ${res.data.map(p => ` + + + + + + + + + `).join('')} +
NameDiskBWDBsEmailsDomainsPriceActions
${p.name}${p.disk_mb > 0 ? p.disk_mb+'MB' : '∞'}${p.bandwidth_mb > 0 ? p.bandwidth_mb+'MB' : '∞'}${p.databases || '∞'}${p.email_accounts || '∞'}${p.addon_domains || '∞'}${p.price ? '$'+p.price : 'Free'} + + +
`; +} + +window.rAddPackage = () => showPackageModal(); +window.rEditPackage = async (id) => { + const res = await Nova.api('packages', 'get', { params:{ id }}); + if (res?.success) showPackageModal(res.data); +}; +function showPackageModal(pkg = null) { + const p = pkg || {}; + Nova.modal(pkg ? 'Edit Package' : 'Add Package', ` +
+
+
+
+
+
+
+
+
+
+
`, + ``); +} +window.submitPackage = async (id) => { + const body = { name:document.getElementById('pk-name')?.value, disk_mb:parseInt(document.getElementById('pk-disk')?.value), bandwidth_mb:parseInt(document.getElementById('pk-bw')?.value), databases:parseInt(document.getElementById('pk-db')?.value), email_accounts:parseInt(document.getElementById('pk-email')?.value), addon_domains:parseInt(document.getElementById('pk-adom')?.value), subdomains:parseInt(document.getElementById('pk-sub')?.value), ftp_accounts:parseInt(document.getElementById('pk-ftp')?.value), price:parseFloat(document.getElementById('pk-price')?.value) }; + const res = id ? await Nova.api('packages','update',{method:'POST',body:{...body,id}}) : await Nova.api('packages','create',{method:'POST',body}); + if (res?.success) { Nova.toast(id ? 'Package updated' : 'Package created','success'); document.querySelector('.modal-overlay')?.remove(); rPackages(document.getElementById('page-content')); } + else Nova.toast(res?.message,'error'); +}; +window.rDeletePackage = (id, name) => { + Nova.confirm(`Delete package "${name}"? Cannot delete if accounts are using it.`, async () => { + const res = await Nova.api('packages','delete',{method:'POST',body:{id}}); + if (res?.success) { Nova.toast('Deleted','success'); rPackages(document.getElementById('page-content')); } + else Nova.toast(res?.message,'error'); + }, true); +}; + +async function rDNS(el) { + el.innerHTML = ` +
Loading…
`; + const res = await Nova.api('dns', 'zones'); + const list = document.getElementById('r-dns-list'); + if (!res?.success || !res.data.length) { list.innerHTML = '
No DNS zones.
'; return; } + list.innerHTML = ` + ${res.data.map(z => ` + + + + + `).join('')} +
DomainAccountRecordsActions
${z.domain}${z.username||'—'}${z.record_count||0}
`; +} + +window.rViewZone = async (zoneId, domain) => { + const res = await Nova.api('dns', 'records', { params:{ zone_id: zoneId }}); + if (!res?.success) { Nova.toast('Failed to load records','error'); return; } + const rows = res.data.map(r => ` + ${r.name}${Nova.badge(r.type,'default')}${r.value}${r.ttl} + + `).join(''); + Nova.modal(`DNS Records — ${domain}`, + ` + ${rows}
NameTypeValueTTL
`); +}; +window.rAddRecord = (zoneId, domain) => { + Nova.modal('Add DNS Record', ` +
+
+
+
+
`, + ``); +}; +window.rDeleteRecord = async (id, zoneId, domain) => { + Nova.confirm('Delete this DNS record?', async () => { + const res = await Nova.api('dns', 'delete-record', { method:'POST', body:{id, zone_id: zoneId }}); + if (res?.success) { Nova.toast('Deleted','success'); document.querySelector('.modal-overlay')?.remove(); rViewZone(zoneId, domain); } + else Nova.toast(res?.message,'error'); + }, true); +}; + +/* ── Nav ────────────────────────────────────────────────────────────────── */ +const rNavItems = [ + { id:'dashboard', label:'Dashboard', icon:'ni-dashboard' }, + { id:'accounts', label:'Accounts', icon:'ni-accounts' }, + { id:'createAccount', label:'New Account', icon:'ni-add' }, + { id:'packages', label:'Packages', icon:'ni-packages' }, + { id:'dns', label:'DNS Zones', icon:'ni-dns' }, +]; +const rPages = { dashboard: rDashboard, accounts: rAccounts, createAccount: rCreateAccount, packages: rPackages, dns: rDNS }; + +let _rActivePage = 'dashboard'; + +function renderRNav() { + const nav = document.getElementById('sidebar-nav'); + if (!nav) return; + nav.innerHTML = rNavItems.map(n => ` + + + ${n.label} + `).join(''); +} + +window.resellerNav = (page) => { + _rActivePage = page; + renderRNav(); + const content = document.getElementById('page-content'); + if (!content) return; + content.innerHTML = '
Loading…
'; + if (rPages[page]) rPages[page](content); +}; + +document.addEventListener('DOMContentLoaded', async () => { + const ok = await initReseller(); + if (!ok) return; + renderRNav(); + window.resellerNav('dashboard'); +}); diff --git a/panel/public/assets/js/user.js b/panel/public/assets/js/user.js new file mode 100644 index 0000000..a02a687 --- /dev/null +++ b/panel/public/assets/js/user.js @@ -0,0 +1,804 @@ +/** + * NovaCPX User Panel JS — all pages + */ + +/* ── Auth guard ──────────────────────────────────────────────────────────── */ +let _user = null; + +async function initUser() { + const res = await Nova.api('auth', 'me'); + if (!res || !res.success) { + document.getElementById('auth-check').innerHTML = renderLogin(); + document.getElementById('main-layout').style.display = 'none'; + return false; + } + _user = res.data; + document.getElementById('user-name').textContent = _user.username || 'User'; + return true; +} + +function renderLogin() { + return ``; +} + +async function doLogin() { + const u = document.getElementById('li-user')?.value; + const p = document.getElementById('li-pass')?.value; + const err = document.getElementById('li-err'); + const res = await Nova.api('auth', 'login', { method: 'POST', body: { username: u, password: p } }); + if (res?.success) { + if (res.data?.portal_url && !res.data.portal_url.includes(':8880')) { + location.href = res.data.portal_url; + } else { + location.reload(); + } + } else { + if (err) { err.textContent = res?.message || 'Login failed'; err.style.display = 'block'; } + } +} +window.doLogin = doLogin; + +/* ── Pages ───────────────────────────────────────────────────────────────── */ + +const userPages = { + dashboard, + domains, + email, + databases, + ftp, + ssl, + php: phpPage, + cron, + files, + stats: statsPage, + backups, +}; + +/* ── Dashboard ───────────────────────────────────────────────────────────── */ +async function dashboard(el) { + el.innerHTML = ` +
+ ${['Disk','Bandwidth','Emails','Databases'].map(l => `
${l}
`).join('')} +
+
+
Quick Access
+
+ ${[ + ['ni-domains','Domains','domains'], + ['ni-email','Email','email'], + ['ni-databases','Databases','databases'], + ['ni-ftp','FTP','ftp'], + ['ni-ssl','SSL','ssl'], + ['ni-php','PHP','php'], + ['ni-cron','Cron Jobs','cron'], + ['ni-files','File Manager','files'], + ].map(([icon,label,page]) => ` + `).join('')} +
+
`; + + const res = await Nova.api('stats', 'account'); + if (res?.success) { + const d = res.data; + const rings = document.getElementById('dash-rings'); + rings.innerHTML = [ + { label: 'Disk', used: d.disk_mb, limit: d.disk_limit, unit: 'MB' }, + { label: 'Databases', used: d.databases, limit: d.db_limit, unit: '' }, + { label: 'Email Accts', used: d.emails, limit: d.email_limit, unit: '' }, + { label: 'FTP Accts', used: d.ftp, limit: d.ftp_limit, unit: '' }, + ].map(item => { + const pct = item.limit > 0 ? Math.min(100, Math.round(item.used / item.limit * 100)) : 0; + const r = 26, circ = 2 * Math.PI * r; + const dash = circ - (pct / 100) * circ; + const color = pct > 85 ? 'var(--red)' : pct > 65 ? 'var(--yellow)' : 'var(--primary)'; + return `
+ + + + ${pct}% + +
${item.label}
+
${item.used}${item.unit} / ${item.limit > 0 ? item.limit + item.unit : '∞'}
+
`; + }).join(''); + } +} + +/* ── Domains ────────────────────────────────────────────────────────────── */ +async function domains(el) { + el.innerHTML = ` +
Loading…
`; + + await loadDomainsList(); +} + +async function loadDomainsList() { + const el = document.getElementById('domains-list'); + if (!el) return; + const res = await Nova.api('domains', 'list'); + if (!res?.success) { el.innerHTML = '
No domains
'; return; } + const rows = res.data; + el.innerHTML = ` + ${rows.map(d => ` + + + + + `).join('')} +
DomainTypeSSLActions
${d.domain}${Nova.badge(d.type, d.is_primary ? 'primary' : 'default')}${d.ssl_enabled ? Nova.badge('SSL','green') : ``} + ${!d.is_primary ? `` : ''} +
`; +} +window.loadDomainsList = loadDomainsList; + +window.addDomain = (type) => { + const fields = type === 'subdomain' + ? `` + : ``; + Nova.modal(`Add ${type.charAt(0).toUpperCase()+type.slice(1)}`, ` +
${fields}
`, + `` + ); +}; + +window.submitAddDomain = async (type) => { + let body = { type }; + if (type === 'subdomain') body.subdomain = document.getElementById('md-sub')?.value; + else body.domain = document.getElementById('md-domain')?.value; + + const action = type === 'subdomain' ? 'add-subdomain' : type === 'alias' ? 'add-alias' : 'add-addon'; + const res = await Nova.api('domains', action, { method: 'POST', body }); + if (res?.success) { Nova.toast(res.message,'success'); document.querySelector('.modal-overlay')?.remove(); loadDomainsList(); } + else Nova.toast(res?.message || 'Failed','error'); +}; + +window.removeDomain = (id, domain) => { + Nova.confirm(`Remove domain ${domain}? This deletes the vhost and DNS zone.`, async () => { + const res = await Nova.api('domains', 'remove', { method: 'POST', body: { id } }); + if (res?.success) { Nova.toast('Domain removed','success'); loadDomainsList(); } + else Nova.toast(res?.message || 'Failed','error'); + }, true); +}; + +window.issueSSL = async (domainId, domain) => { + Nova.toast(`Issuing Let's Encrypt SSL for ${domain}…`,'info',6000); + const res = await Nova.api('ssl', 'issue', { method: 'POST', body: { domain } }); + if (res?.success) { Nova.toast('SSL issued successfully','success'); loadDomainsList(); } + else Nova.toast(res?.message || 'SSL failed — check domain DNS','error',6000); +}; +window.issueSSL = window.issueSSL; + +/* ── Email ──────────────────────────────────────────────────────────────── */ +async function email(el) { + el.innerHTML = ` +
Loading…
+ +
Loading…
`; + + loadEmailList(); + loadForwarderList(); +} + +async function loadEmailList() { + const el = document.getElementById('email-list'); + if (!el) return; + const res = await Nova.api('email', 'list'); + if (!res?.success || !res.data.length) { el.innerHTML = '
No email accounts yet.
'; return; } + el.innerHTML = ` + ${res.data.map(a => ` + + + + + `).join('')} +
EmailQuotaStatusActions
${a.email}${a.quota_mb > 0 ? a.quota_mb + 'MB' : 'Unlimited'}${Nova.badge(a.status, a.status === 'active' ? 'green' : 'yellow')} + Webmail + + +
`; +} +window.loadEmailList = loadEmailList; + +window.addEmailAccount = () => { + Nova.modal('Add Email Account', ` +
+
+
`, + `` + ); +}; + +window.submitAddEmail = async () => { + const res = await Nova.api('email', 'create', { method: 'POST', body: { + email: document.getElementById('em-addr')?.value, + password: document.getElementById('em-pass')?.value, + quota_mb: parseInt(document.getElementById('em-quota')?.value || '0'), + }}); + if (res?.success) { Nova.toast('Email account created','success'); document.querySelector('.modal-overlay')?.remove(); loadEmailList(); } + else Nova.toast(res?.message || 'Failed','error'); +}; + +window.changeEmailPass = (id) => { + Nova.modal('Change Email Password', `
`, + ``); +}; +window.submitEmailPass = async (id) => { + const res = await Nova.api('email', 'change-password', { method: 'POST', body: { id, password: document.getElementById('ep-pass')?.value }}); + if (res?.success) { Nova.toast('Password updated','success'); document.querySelector('.modal-overlay')?.remove(); } + else Nova.toast(res?.message || 'Failed','error'); +}; + +window.deleteEmail = (id, addr) => { + Nova.confirm(`Delete ${addr}?`, async () => { + const res = await Nova.api('email', 'delete', { method: 'POST', body: { id }}); + if (res?.success) { Nova.toast('Email deleted','success'); loadEmailList(); } + }, true); +}; + +window.openWebmail = (email) => { + Nova.api('webmail', 'url').then(res => { + if (res?.success) window.open(res.data.url, '_blank'); + }); +}; + +async function loadForwarderList() { + const el = document.getElementById('forwarder-list'); + if (!el) return; + const res = await Nova.api('email', 'forwarders'); + if (!res?.success || !res.data.length) { el.innerHTML = '
No forwarders yet.
'; return; } + el.innerHTML = ` + ${res.data.map(f => ` + `).join('')} +
FromTo
${f.source}${f.destination}
`; +} + +window.addForwarder = () => { + Nova.modal('Add Forwarder', ` +
+
`, + ``); +}; +window.submitFwd = async () => { + const res = await Nova.api('email', 'add-forwarder', { method: 'POST', body: { source: document.getElementById('fw-from')?.value, destination: document.getElementById('fw-to')?.value }}); + if (res?.success) { Nova.toast('Forwarder added','success'); document.querySelector('.modal-overlay')?.remove(); loadForwarderList(); } + else Nova.toast(res?.message || 'Failed','error'); +}; +window.deleteFwd = async (id) => { + const res = await Nova.api('email', 'delete-forwarder', { method: 'POST', body: { id }}); + if (res?.success) { Nova.toast('Deleted','success'); loadForwarderList(); } +}; + +/* ── Databases ──────────────────────────────────────────────────────────── */ +async function databases(el) { + el.innerHTML = ` +
Loading…
`; + loadDBList(); +} + +async function loadDBList() { + const el = document.getElementById('db-list'); + if (!el) return; + const res = await Nova.api('databases', 'list'); + if (!res?.success || !res.data.length) { el.innerHTML = '
No databases yet.
'; return; } + el.innerHTML = ` + ${res.data.map(d => ` + + + + + + `).join('')} +
DatabaseUserTypeSizeActions
${d.db_name}${d.db_user}${Nova.badge(d.db_type,'default')}${d.size || '—'} + + +
`; +} +window.loadDBList = loadDBList; + +window.addDB = (type) => { + Nova.modal(`Create ${type.toUpperCase()} Database`, ` +
+
+
`, + ``); +}; +window.submitAddDB = async (type) => { + const res = await Nova.api('databases', 'create', { method:'POST', body: { db_type: type, db_name: document.getElementById('dbn-name')?.value, db_user: document.getElementById('dbn-user')?.value, db_pass: document.getElementById('dbn-pass')?.value }}); + if (res?.success) { Nova.toast('Database created','success'); document.querySelector('.modal-overlay')?.remove(); loadDBList(); } + else Nova.toast(res?.message || 'Failed','error'); +}; +window.changeDBPass = (id) => { + Nova.modal('Change DB Password', `
`, + ``); +}; +window.submitDBPass = async (id) => { + const res = await Nova.api('databases', 'change-password', { method:'POST', body:{ id, password: document.getElementById('dbp-pass')?.value }}); + if (res?.success) { Nova.toast('Password updated','success'); document.querySelector('.modal-overlay')?.remove(); } + else Nova.toast(res?.message,'error'); +}; +window.dropDB = (id, name) => { + Nova.confirm(`Drop database ${name}? All data will be permanently deleted.`, async () => { + const res = await Nova.api('databases', 'drop', { method:'POST', body:{ id }}); + if (res?.success) { Nova.toast('Database dropped','success'); loadDBList(); } + else Nova.toast(res?.message,'error'); + }, true); +}; + +/* ── FTP ────────────────────────────────────────────────────────────────── */ +async function ftp(el) { + el.innerHTML = ` +
Loading…
`; + loadFTPList(); +} + +async function loadFTPList() { + const el = document.getElementById('ftp-list'); + if (!el) return; + const res = await Nova.api('ftp', 'list'); + if (!res?.success || !res.data.length) { el.innerHTML = '
No FTP accounts yet.
'; return; } + el.innerHTML = ` + ${res.data.map(f => ` + + + + + `).join('')} +
UsernameDirectoryQuotaActions
${f.username}${f.home_dir}${f.quota_mb > 0 ? f.quota_mb+'MB' : 'Unlimited'} + + +
`; +} +window.loadFTPList = loadFTPList; + +window.addFTP = () => { + Nova.modal('Add FTP Account', ` +
+
+
`, + ``); +}; +window.submitAddFTP = async () => { + const res = await Nova.api('ftp', 'create', { method:'POST', body:{ username: document.getElementById('ftp-user')?.value, password: document.getElementById('ftp-pass')?.value, home_dir: document.getElementById('ftp-dir')?.value || null }}); + if (res?.success) { Nova.toast('FTP account created','success'); document.querySelector('.modal-overlay')?.remove(); loadFTPList(); } + else Nova.toast(res?.message||'Failed','error'); +}; +window.changeFTPPass = (id) => { + Nova.modal('Change FTP Password', `
`, + ``); +}; +window.deleteFTP = (id, user) => { + Nova.confirm(`Delete FTP account ${user}?`, async () => { + const res = await Nova.api('ftp', 'delete', { method:'POST', body:{id}}); + if (res?.success) { Nova.toast('Deleted','success'); loadFTPList(); } + }, true); +}; + +/* ── SSL ────────────────────────────────────────────────────────────────── */ +async function ssl(el) { + el.innerHTML = ` +
Loading…
`; + loadSSLList(); +} + +async function loadSSLList() { + const el = document.getElementById('ssl-list'); + if (!el) return; + const res = await Nova.api('ssl', 'list'); + if (!res?.success || !res.data.length) { el.innerHTML = '
No SSL certificates yet.
'; return; } + el.innerHTML = ` + ${res.data.map(c => { + const days = c.days_remaining; + const status = !days ? 'unknown' : days < 7 ? 'critical' : days < 30 ? 'warning' : 'ok'; + const badge = days !== null ? `${days}d` : c.status; + const badgeType = status === 'critical' ? 'red' : status === 'warning' ? 'yellow' : 'green'; + return ` + + + + + + `; + }).join('')} +
DomainTypeExpiresStatusActions
${c.domain}${Nova.badge(c.type,'default')}${c.expires_at || '—'}${Nova.badge(badge, badgeType)} + + +
`; +} +window.loadSSLList = loadSSLList; + +window.issueNewSSL = () => { + Nova.api('domains','list').then(res => { + const opts = (res?.data || []).map(d => ``).join(''); + Nova.modal("Issue Let's Encrypt SSL", ` +
+
`, + ``); + }); +}; +window.submitIssueSSL = async () => { + const domain = document.getElementById('ssl-dom')?.value; + Nova.toast(`Issuing SSL for ${domain}…`, 'info', 8000); + document.querySelector('.modal-overlay')?.remove(); + const res = await Nova.api('ssl', 'issue', { method:'POST', body:{ domain, email: document.getElementById('ssl-email')?.value }}); + if (res?.success) { Nova.toast('SSL issued!','success'); loadSSLList(); } + else Nova.toast(res?.message || 'SSL issue failed','error',8000); +}; +window.renewCert = async (id) => { + Nova.toast('Renewing…','info'); + const res = await Nova.api('ssl', 'renew', { method:'POST', body:{cert_id:id}}); + if (res?.success) { Nova.toast('Renewed','success'); loadSSLList(); } + else Nova.toast(res?.message,'error'); +}; +window.deleteCert = (id, domain) => { + Nova.confirm(`Remove SSL cert for ${domain}?`, async () => { + const res = await Nova.api('ssl', 'delete', { method:'POST', body:{cert_id:id}}); + if (res?.success) { Nova.toast('Removed','success'); loadSSLList(); } + }, true); +}; + +/* ── PHP Manager ────────────────────────────────────────────────────────── */ +async function phpPage(el) { + el.innerHTML = ` +
+
+
PHP Version
+
Loading…
+
+
+
PHP Settings
+
Loading…
+
+
`; + + const [versRes, cfgRes] = await Promise.all([ + Nova.api('php', 'versions'), + Nova.api('php', 'config'), + ]); + + if (versRes?.success) { + document.getElementById('php-versions').innerHTML = versRes.data.map(v => ` +
+
+ PHP ${v.version} + ${v.is_default ? Nova.badge('default','primary') : ''} + ${!v.installed ? Nova.badge('not installed','muted') : ''} +
+ ${v.installed ? `` : ''} +
`).join(''); + } + + if (cfgRes?.success) { + const c = cfgRes.data; + document.getElementById('php-settings').innerHTML = ` +
+
+
+
+ `; + } +} + +window.switchPHP = async (ver) => { + const res = await Nova.api('php', 'switch-version', { method:'POST', body:{ version: ver }}); + if (res?.success) { Nova.toast(`Switched to PHP ${ver}`,'success'); phpPage(document.getElementById('page-content')); } + else Nova.toast(res?.message,'error'); +}; +window.savePHPSettings = async () => { + const res = await Nova.api('php', 'update-config', { method:'POST', body:{ + memory_limit: document.getElementById('php-mem')?.value, + max_execution_time: document.getElementById('php-exec')?.value, + upload_max_filesize: document.getElementById('php-upload')?.value, + post_max_size: document.getElementById('php-post')?.value, + }}); + if (res?.success) Nova.toast('PHP settings saved','success'); + else Nova.toast(res?.message,'error'); +}; + +/* ── Cron Jobs ──────────────────────────────────────────────────────────── */ +async function cron(el) { + el.innerHTML = ` +
Loading…
`; + loadCronList(); +} + +async function loadCronList() { + const el = document.getElementById('cron-list'); + if (!el) return; + const res = await Nova.api('cron', 'list'); + if (!res?.success || !res.data.length) { el.innerHTML = '
No cron jobs yet.
'; return; } + el.innerHTML = ` + ${res.data.map(j => ` + + + + + `).join('')} +
ScheduleCommandStatusActions
${j.minute} ${j.hour} ${j.day} ${j.month} ${j.weekday}${j.command} + + + +
`; +} +window.loadCronList = loadCronList; + +window.addCron = () => { + Nova.modal('Add Cron Job', ` +
+
+ ${['minute','hour','day','month','weekday'].map(f => `
`).join('')} +
+
* = every | */5 = every 5 | 0 = midnight/Jan/Mon
`, + ``); +}; +window.submitCron = async () => { + const res = await Nova.api('cron', 'create', { method:'POST', body:{ + command: document.getElementById('cr-cmd')?.value, + minute: document.getElementById('cr-minute')?.value || '*', + hour: document.getElementById('cr-hour')?.value || '*', + day: document.getElementById('cr-day')?.value || '*', + month: document.getElementById('cr-month')?.value || '*', + weekday: document.getElementById('cr-weekday')?.value|| '*', + }}); + if (res?.success) { Nova.toast('Cron job added','success'); document.querySelector('.modal-overlay')?.remove(); loadCronList(); } + else Nova.toast(res?.message,'error'); +}; +window.toggleCron = async (id) => { + await Nova.api('cron', 'toggle', { method:'POST', body:{id}}); + loadCronList(); +}; +window.deleteCron = (id) => { + Nova.confirm('Delete this cron job?', async () => { + const res = await Nova.api('cron', 'delete', { method:'POST', body:{id}}); + if (res?.success) { Nova.toast('Deleted','success'); loadCronList(); } + }, true); +}; + +/* ── File Manager ───────────────────────────────────────────────────────── */ +let _fmPath = '/public_html'; + +async function files(el) { + el.innerHTML = ` +
+
+ + ${_fmPath} +
+
Loading…
+
+ `; + + loadFMList(_fmPath); +} + +async function loadFMList(path) { + _fmPath = path; + const pathEl = document.getElementById('fm-path'); + if (pathEl) pathEl.textContent = path; + const el = document.getElementById('fm-list'); + if (!el) return; + const res = await Nova.api('files', 'list', { params: { path }}); + if (!res?.success) { el.innerHTML = `
${res?.message || 'Error loading directory'}
`; return; } + + const parentPath = path.includes('/') ? path.replace(/\/[^/]+$/, '') || '/' : '/'; + el.innerHTML = ` + ${path !== '/' && path !== '/public_html' ? `` : ''} + ${res.data.items.map(f => ` + + + + + + `).join('')} +
NameSizePermsModifiedActions
← ..
+ ${f.type === 'dir' + ? `📁 ${f.name}` + : `📄 ${f.name}`} + ${f.size || '—'}${f.perms}${f.modified} + ${f.type === 'file' ? `` : ''} + + + +
`; +} +window.fmNav = (p) => loadFMList(p); + +window.fmEdit = async (path, name) => { + const res = await Nova.api('files', 'read', { params: { path }}); + if (!res?.success) { Nova.toast(res?.message || 'Cannot read file','error'); return; } + const edEl = document.getElementById('fm-editor'); + edEl.style.display = 'block'; + edEl.innerHTML = `
+
Editing: ${name} +
+ + +
+
+ +
`; +}; +window.fmSave = async (path) => { + const content = document.getElementById('fm-code')?.value || ''; + const res = await Nova.api('files', 'write', { method:'POST', body:{ path, content }}); + if (res?.success) Nova.toast('Saved','success'); + else Nova.toast(res?.message || 'Save failed','error'); +}; +window.fmDelete = (path, name) => { + Nova.confirm(`Delete ${name}?`, async () => { + const res = await Nova.api('files', 'delete', { method:'POST', body:{ path }}); + if (res?.success) { Nova.toast('Deleted','success'); loadFMList(_fmPath); } + else Nova.toast(res?.message,'error'); + }, true); +}; +window.fmMkdir = () => { + Nova.modal('New Folder', `
`, + ``); +}; +window.fmRename = (path, name) => { + const dir = path.replace(/\/[^/]+$/, ''); + Nova.modal('Rename', `
`, + ``); +}; +window.fmChmod = (path, current) => { + Nova.modal('Change Permissions', `
`, + ``); +}; +window.fmUpload = () => { + Nova.modal('Upload File', ` +
`, + ``); +}; +window.submitFMUpload = async () => { + const fileInput = document.getElementById('fm-upfile'); + if (!fileInput?.files[0]) return; + const fd = new FormData(); + fd.append('file', fileInput.files[0]); + fd.append('path', _fmPath); + const res = await fetch(`/api/files/upload?path=${encodeURIComponent(_fmPath)}`, { method:'POST', credentials:'include', body: fd }).then(r => r.json()); + if (res?.success) { Nova.toast('Uploaded','success'); document.querySelector('.modal-overlay')?.remove(); loadFMList(_fmPath); } + else Nova.toast(res?.message || 'Upload failed','error'); +}; + +/* ── Stats ──────────────────────────────────────────────────────────────── */ +async function statsPage(el) { + el.innerHTML = ` +
Loading…
`; + + const res = await Nova.api('stats', 'account'); + if (!res?.success) return; + const d = res.data; + document.getElementById('stats-grid').innerHTML = [ + { label: 'Disk Used', val: d.disk_mb + ' MB', limit: d.disk_limit > 0 ? `/ ${d.disk_limit} MB` : '', pct: d.disk_limit > 0 ? Math.min(100,(d.disk_mb/d.disk_limit*100)) : 0 }, + { label: 'Databases', val: d.databases, limit: d.db_limit > 0 ? `/ ${d.db_limit}` : '', pct: d.db_limit > 0 ? Math.min(100,d.databases/d.db_limit*100) : 0 }, + { label: 'Email Accounts', val: d.emails, limit: d.email_limit > 0 ? `/ ${d.email_limit}` : '', pct: d.email_limit > 0 ? Math.min(100,d.emails/d.email_limit*100) : 0 }, + { label: 'FTP Accounts', val: d.ftp, limit: d.ftp_limit > 0 ? `/ ${d.ftp_limit}` : '', pct: d.ftp_limit > 0 ? Math.min(100,d.ftp/d.ftp_limit*100) : 0 }, + { label: 'Domains', val: d.domains, limit: '', pct: 0 }, + { label: 'Inodes', val: d.inodes.toLocaleString(), limit: '', pct: 0 }, + ].map(item => `
+
${item.label}
+
${item.val} ${item.limit}
+ ${item.pct > 0 ? `
${Nova.progressBar(Math.round(item.pct))}
` : ''} +
`).join(''); +} + +/* ── Backups ────────────────────────────────────────────────────────────── */ +async function backups(el) { + el.innerHTML = ` +
Loading…
`; + + const res = await Nova.api('system', 'audit-log', { params:{ limit:5 }}); + document.getElementById('backup-list').innerHTML = `
+ +
Backup management is being configured by your hosting provider.
+
Contact support to request a manual backup.
+
`; +} +window.createBackup = () => Nova.toast('Backup request submitted — you will be notified when ready.','info'); + +/* ── Navigation ─────────────────────────────────────────────────────────── */ +const navItems = [ + { id: 'dashboard', label: 'Dashboard', icon: 'ni-dashboard' }, + { id: 'domains', label: 'Domains', icon: 'ni-domains' }, + { id: 'email', label: 'Email', icon: 'ni-email' }, + { id: 'databases', label: 'Databases', icon: 'ni-databases' }, + { id: 'ftp', label: 'FTP', icon: 'ni-ftp' }, + { id: 'ssl', label: 'SSL / TLS', icon: 'ni-ssl' }, + { id: 'php', label: 'PHP', icon: 'ni-php' }, + { id: 'cron', label: 'Cron Jobs', icon: 'ni-cron' }, + { id: 'files', label: 'File Manager', icon: 'ni-files' }, + { id: 'stats', label: 'Statistics', icon: 'ni-stats' }, + { id: 'backups', label: 'Backups', icon: 'ni-backups' }, +]; + +let _activePage = 'dashboard'; + +function renderNav() { + const nav = document.getElementById('sidebar-nav'); + if (!nav) return; + nav.innerHTML = navItems.map(n => ` + + + ${n.label} + `).join(''); +} + +window.userNav = (page) => { + _activePage = page; + renderNav(); + const content = document.getElementById('page-content'); + if (!content) return; + content.innerHTML = '
Loading…
'; + if (userPages[page]) userPages[page](content); +}; + +/* ── Boot ────────────────────────────────────────────────────────────────── */ +document.addEventListener('DOMContentLoaded', async () => { + const ok = await initUser(); + if (!ok) return; + renderNav(); + window.userNav('dashboard'); +}); diff --git a/panel/public/reseller/index.php b/panel/public/reseller/index.php index 707f682..79e31fd 100644 --- a/panel/public/reseller/index.php +++ b/panel/public/reseller/index.php @@ -12,7 +12,7 @@ -