Files
virtfusion-whmcs-module/modules/servers/VirtFusionDirect/templates/overview.tpl
Prophet731 ad85439dfb feat: add PowerDNS reverse DNS (PTR) integration
Introduces an opt-in reverse DNS management subsystem backed by a PowerDNS
Authoritative HTTP API. Runs via a companion WHMCS addon module
(modules/addons/VirtFusionDns) that holds settings and a Test Connection
page; the server module reads those settings from tbladdonmodules and
short-circuits when the addon is absent or disabled, so provisioning is
unaffected for operators who don't use the feature.

Lifecycle hooks:
- createAccount creates PTRs for every assigned IP (forward DNS must
  already resolve to the IP — FCrDNS enforcement)
- renameServer updates only PTRs whose content matched the old hostname,
  preserving client-custom records
- terminateAccount deletes all PTRs before the local state is purged
- TestConnection merges PowerDNS health check with the existing VirtFusion
  check
- A DailyCronJob hook reconciles missing PTRs additive-only (never
  overwrites)

Client UI: new "Reverse DNS" panel on the service overview with one
editable PTR input per assigned IP, per-row status badges, and
forward-DNS rejection on save. Admin services tab gets a parallel
widget with Reconcile (additive) and Reconcile (force reset) buttons.

New subsystem at lib/PowerDns/:
- Client.php    PowerDNS API wrapper (X-API-Key, listZones/getZone/
                patchRRset/notifyZone), auto-NOTIFY on successful PATCH
- Config.php    Loads + decrypts addon settings from tbladdonmodules
- IpUtil.php    PTR-name generation (IPv4 + IPv6), zone matching,
                RFC 2317 classless parsing
- Resolver.php  FCrDNS verification via dns_get_record with CNAME-chain
                following and per-(hostname,ip) caching
- PtrManager.php Orchestrator: syncServer, deleteForServer, listPtrs,
                setPtr, reconcile, reconcileAll

Security hardening helpers added to Module and applied to the rDNS
endpoints:
- requirePost()           HTTP method gate (405 on non-POST mutations)
- requireSameOrigin()     Origin/Referer check against WHMCS host (CSRF
                          defence against cross-site form POST)
- requireServiceStatus()  tblhosting.domainstatus filter (Active for
                          writes, Active+Suspended for reads)

RFC 2317 classless delegations (e.g. 64/64.113.0.203.in-addr.arpa.)
supported with alignment validation: rejects misaligned start addresses
that don't correspond to any real delegation boundary.

PowerDNS zone IDs containing '/' are URL-encoded as '=2F' per the
PowerDNS API convention. PATCH success triggers PUT /zones/{id}/notify
so slaves pick up the SOA-bumped serial immediately.

Includes IPv4 + IPv6 support, per-IP write rate limit (10s), fresh
IP-ownership re-verification on every client write (defends against
stale-ownership after IP reassignment), and audit logging of every
successful edit to the WHMCS module log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:08:22 -04:00

470 lines
25 KiB
Smarty

<link href="{$systemURL}modules/servers/VirtFusionDirect/templates/css/module.css?v={$smarty.now}" rel="stylesheet">
<script src="{$systemURL}modules/servers/VirtFusionDirect/templates/js/module.js?v={$smarty.now}"></script>
{if $serviceStatus eq 'Active'}
{* Server Overview Panel *}
<div class="panel card panel-default mb-3">
<div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">
Server Overview
<span id="vf-status-badge" class="vf-badge" style="float: right;"></span>
</h3>
</div>
<div class="panel-body card-body p-4">
<div id="vf-action-progress" style="display:none;">
<div class="spinner-border spinner-border-sm text-light"></div>
<span id="vf-action-progress-text"></span>
<span id="vf-action-progress-timer" class="ml-auto" style="margin-left:auto;"></span>
</div>
<div id="vf-server-info-loader-container">
<div id="vf-server-info-loader">
<div class="row">
<div class="col-md-6">
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-medium"></div>
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-short"></div>
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-medium"></div>
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-short"></div>
</div>
<div class="col-md-6">
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-medium"></div>
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-short"></div>
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-medium"></div>
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-short"></div>
</div>
</div>
</div>
</div>
<script>vfServerData('{$serviceid}', '{$systemURL}');</script>
<div id="vf-server-info-error">
<div class="alert alert-warning mb-0">Information unavailable. Try again later.</div>
</div>
<div id="vf-server-info" class="row mb-2">
<div class="col-12">
<div class="row">
<div class="col-md-6">
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">Name:</div>
<div class="col-xs-8 col-8">
<div class="d-flex" style="display:flex; gap:6px; align-items:center;">
<input type="text" id="vf-rename-input" class="form-control form-control-sm" maxlength="63" style="max-width:200px;" placeholder="Server name">
<button id="vf-randomise-btn" onclick="vfShowNameDropdown('{$serviceid}','{$systemURL}')" type="button" class="btn btn-sm btn-outline-secondary" title="Randomise">&#x21bb;</button>
<button id="vf-rename-save" onclick="vfRenameServer('{$serviceid}','{$systemURL}')" type="button" class="btn btn-sm btn-primary">Save</button>
</div>
<div id="vf-name-dropdown" style="display:none;"></div>
<div id="vf-rename-alert" class="mt-1" style="display:none;"></div>
</div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">Hostname:</div>
<div class="col-xs-8 col-8" id="vf-data-server-hostname"></div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">Memory:</div>
<div class="col-xs-8 col-8" id="vf-data-server-memory"></div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">CPU:</div>
<div class="col-xs-8 col-8" id="vf-data-server-cpu"></div>
</div>
</div>
<div class="col-md-6">
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">IPv4:</div>
<div class="col-xs-8 col-8" id="vf-data-server-ipv4"></div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">IPv6:</div>
<div class="col-xs-8 col-8" id="vf-data-server-ipv6"></div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">Storage:</div>
<div class="col-xs-8 col-8" id="vf-data-server-storage"></div>
</div>
<div class="row p-1">
<div class="col-xs-4 col-4 text-right vf-bold">Traffic:</div>
<div class="col-xs-8 col-8">
<span id="vf-data-server-traffic-used"></span>
<span id="vf-data-server-traffic-sep"> / </span>
<span id="vf-data-server-traffic"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{* Power Management Panel *}
<div class="panel card panel-default mb-3">
<div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">Power Management</h3>
</div>
<div class="panel-body card-body p-4">
<div id="vf-power-alert" class="alert" style="display: none;"></div>
<div class="row">
<div class="col-12">
<div class="vf-power-buttons">
<button id="vf-power-boot" onclick="vfPowerAction('{$serviceid}','{$systemURL}','boot')" type="button" class="btn btn-success vf-btn-power">
<span class="vf-btn-spinner spinner-border spinner-border-sm" style="display:none;"></span>
Start
</button>
<button id="vf-power-restart" onclick="vfPowerAction('{$serviceid}','{$systemURL}','restart')" type="button" class="btn btn-warning vf-btn-power">
<span class="vf-btn-spinner spinner-border spinner-border-sm" style="display:none;"></span>
Restart
</button>
<button id="vf-power-shutdown" onclick="vfPowerAction('{$serviceid}','{$systemURL}','shutdown')" type="button" class="btn btn-info vf-btn-power">
<span class="vf-btn-spinner spinner-border spinner-border-sm" style="display:none;"></span>
Shutdown
</button>
<button id="vf-power-poweroff" onclick="vfPowerAction('{$serviceid}','{$systemURL}','poweroff')" type="button" class="btn btn-danger vf-btn-power">
<span class="vf-btn-spinner spinner-border spinner-border-sm" style="display:none;"></span>
Force Off
</button>
</div>
</div>
</div>
</div>
</div>
{* Manage Panel *}
<div class="panel card panel-default mb-3">
<div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">Manage</h3>
</div>
<div class="panel-body card-body p-4">
<div class="row">
<div class="col-12">
<div id="vf-login-error" class="alert alert-danger"></div>
<p>Manage your server via our dedicated control panel. You will be automatically authenticated and the control panel will open in a new window.</p>
<button id="vf-login-button" onclick="vfLoginAsServerOwner('{$serviceid}','{$systemURL}',true)" type="button" class="btn btn-primary text-uppercase d-flex align-items-center">
<div id="vf-login-button-spinner" class="spinner-border spinner-border-sm text-light vf-spinner-margin"></div>
Open Control Panel
</button>
</div>
<div class="col-12">
<p class="mb-0 pt-3 vf-small">Having trouble opening the control panel in a new window? <a href="#" onclick="vfLoginAsServerOwner('{$serviceid}','{$systemURL}',false); return false;">Click here</a> to open in this window.</p>
</div>
{if $serverHostname}
<div class="col-12">
<hr>
<div id="vf-password-reset-error" class="alert alert-danger">Oops! Something went wrong. Try again later.</div>
<div id="vf-password-reset-success" class="alert alert-success">
<div class="mb-2 font-weight-bold">Your new login credentials. These will only be displayed once.</div>
<div class="font-weight-bold">Email: <span class="font-weight-normal" id="vf-data-user-email"></span></div>
<div class="font-weight-bold">Password: <span class="font-weight-normal" id="vf-data-user-password"></span></div>
</div>
<p class="pt-0">Alternatively you may directly access the control panel at <a target="_blank" href="https://{$serverHostname|escape:'htmlall'}">{$serverHostname|escape:'htmlall'}</a></p>
<button id="vf-password-reset-button" onclick="vfUserPasswordReset('{$serviceid}','{$systemURL}')" type="button" class="btn btn-primary text-uppercase d-flex align-items-center">
<div id="vf-password-reset-button-spinner" class="spinner-border spinner-border-sm text-light vf-spinner-margin"></div>
Reset Login Credentials
</button>
</div>
{/if}
<div class="col-12">
<hr>
<div id="vf-server-password-alert" class="alert" style="display:none;"></div>
<p class="vf-small text-muted">Reset the server's root password. The new password will be copied to your clipboard automatically.</p>
<button id="vf-server-password-btn" onclick="vfResetServerPassword('{$serviceid}','{$systemURL}')" type="button" class="btn btn-warning text-uppercase d-flex align-items-center">
<span id="vf-server-password-spinner" class="spinner-border spinner-border-sm vf-spinner-margin" style="display:none;"></span>
Reset Server Password
</button>
</div>
<div class="col-12" id="vf-backups-section" style="display:none;">
<hr>
<h5 class="vf-bold">Backups</h5>
<div id="vf-backups-loader"><div class="spinner-border spinner-border-sm"></div></div>
<div id="vf-backups-timeline" class="vf-timeline"></div>
<button id="vf-backups-show-all" class="btn btn-sm btn-link" style="display:none;" onclick="$('.vf-timeline-item-hidden').show(); $(this).hide();">Show all</button>
</div>
<script>
if (typeof vfLoadBackups === 'function') {
vfLoadBackups('{$serviceid}', '{$systemURL}');
}
</script>
</div>
</div>
</div>
{* Rebuild Panel *}
<div class="panel card panel-default mb-3">
<div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">Rebuild Server</h3>
</div>
<div class="panel-body card-body p-4">
<div id="vf-rebuild-alert" class="alert" style="display: none;"></div>
<div class="alert alert-warning">
<strong>Warning:</strong> Rebuilding your server will erase all data on the server and reinstall the operating system. This action cannot be undone.
</div>
<input type="hidden" id="vf-rebuild-os" value="">
<div class="form-group mb-3">
<label>Operating System</label>
<input type="text" id="vf-os-search" class="form-control vf-os-search" placeholder="Search templates...">
</div>
<div id="vf-os-gallery-loader" class="mb-3">
<div class="vf-skeleton" style="height:120px;"></div>
</div>
<div id="vf-os-gallery" class="mb-3" style="display:none;"></div>
<div id="vf-os-details" class="mb-3" style="display:none;"></div>
<button id="vf-rebuild-button" onclick="vfRebuildServer('{$serviceid}','{$systemURL}')" type="button" class="btn btn-danger text-uppercase d-flex align-items-center">
<span id="vf-rebuild-spinner" class="spinner-border spinner-border-sm vf-spinner-margin" style="display:none;"></span>
Rebuild Server
</button>
<script>vfLoadOsTemplates('{$serviceid}', '{$systemURL}');</script>
</div>
</div>
{* Network Management Panel *}
<div class="panel card panel-default mb-3">
<div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">Network</h3>
</div>
<div class="panel-body card-body p-4">
<div id="vf-network-alert" class="alert" style="display: none;"></div>
<div id="vf-network-content" style="display: none;">
<div class="row mb-3">
<div class="col-md-6">
<h5 class="vf-bold">IPv4 Addresses</h5>
<div id="vf-ipv4-list" class="mb-2"></div>
</div>
<div class="col-md-6">
<h5 class="vf-bold">IPv6 Subnets</h5>
<div id="vf-ipv6-list" class="mb-2"></div>
</div>
</div>
</div>
</div>
</div>
{if $rdnsEnabled}
{* Reverse DNS Panel *}
<div class="panel card panel-default mb-3">
<div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">Reverse DNS</h3>
</div>
<div class="panel-body card-body p-4">
<p class="vf-small text-muted mb-3">Set a custom PTR record for each assigned IP. Forward DNS (A/AAAA) for the hostname must already resolve to the IP before the PTR can be saved.</p>
<div id="vf-rdns-alert" class="alert" style="display:none;"></div>
<div id="vf-rdns-list">
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-medium"></div>
<div class="vf-skeleton vf-skeleton-line vf-skeleton-line-medium"></div>
</div>
<script>
if (typeof vfLoadRdns === 'function') {
vfLoadRdns('{$serviceid}', '{$systemURL}');
}
</script>
</div>
</div>
{/if}
{* Resources Panel — populated by JS after server data loads *}
<div id="vf-resources-panel" class="panel card panel-default mb-3" style="display: none;">
<div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">Resources</h3>
</div>
<div class="panel-body card-body p-4">
<div class="row">
<div class="col-md-6">
<div class="vf-resource-item mb-3">
<div class="d-flex justify-content-between mb-1">
<span class="vf-bold">Memory</span>
<span id="vf-res-memory"></span>
</div>
</div>
<div class="vf-resource-item mb-3">
<div class="d-flex justify-content-between mb-1">
<span class="vf-bold">CPU Cores</span>
<span id="vf-res-cpu"></span>
</div>
</div>
<div class="vf-resource-item mb-3">
<div class="d-flex justify-content-between mb-1">
<span class="vf-bold">Storage</span>
<span id="vf-res-storage"></span>
</div>
</div>
</div>
<div class="col-md-6">
<div class="vf-resource-item mb-3">
<div class="d-flex justify-content-between mb-1">
<span class="vf-bold">Traffic</span>
<span id="vf-res-traffic"></span>
</div>
<div class="progress" style="height: 8px;">
<div id="vf-res-traffic-bar" class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
</div>
<div class="vf-resource-item mb-3">
<div class="d-flex justify-content-between mb-1">
<span class="vf-bold">Network Speed</span>
<span id="vf-res-network-speed"></span>
</div>
</div>
</div>
</div>
<div id="vf-traffic-chart-section" style="display:none;">
<hr>
<h5 class="vf-bold mb-2">Traffic Usage</h5>
<canvas id="vf-traffic-chart" style="width:100%; height:200px;"></canvas>
<div class="row mt-2 text-center">
<div class="col-4"><small class="text-muted">Used</small><div id="vf-traffic-used" class="vf-bold">-</div></div>
<div class="col-4"><small class="text-muted">Limit</small><div id="vf-traffic-limit" class="vf-bold">-</div></div>
<div class="col-4"><small class="text-muted">Remaining</small><div id="vf-traffic-remaining" class="vf-bold">-</div></div>
</div>
</div>
<script>
if (typeof vfLoadTrafficStats === 'function') {
vfLoadTrafficStats('{$serviceid}', '{$systemURL}');
}
</script>
</div>
</div>
{* VNC Console Panel — hidden by default, shown by JS if VNC is enabled *}
<div id="vf-vnc-panel" class="panel card panel-default mb-3" style="display: none;">
<div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">VNC Console</h3>
</div>
<div class="panel-body card-body p-4">
<div id="vf-vnc-alert" class="alert" style="display: none;"></div>
<p>Access your server's console directly in your browser. The server must be running for VNC access.</p>
<div class="d-flex align-items-center mb-3" style="display:flex; gap:12px; align-items:center;">
<button id="vf-vnc-button" onclick="vfOpenVnc('{$serviceid}','{$systemURL}')" type="button" class="btn btn-primary text-uppercase d-flex align-items-center">
<span id="vf-vnc-spinner" class="spinner-border spinner-border-sm vf-spinner-margin" style="display:none;"></span>
Open Console
</button>
<label class="vf-toggle-label mb-0" style="display:flex; align-items:center; gap:6px; cursor:pointer;">
<input type="checkbox" id="vf-vnc-toggle" class="vf-toggle-input" onchange="vfToggleVnc('{$serviceid}','{$systemURL}', this.checked)">
<span class="vf-toggle-switch"></span>
<span class="vf-small">VNC Enabled</span>
</label>
</div>
<div id="vf-vnc-details" style="display:none;">
<div class="row">
<div class="col-md-6">
<div class="row p-1">
<div class="col-4 text-right vf-bold vf-small">IP:</div>
<div class="col-8 vf-small" id="vf-vnc-ip">-</div>
</div>
<div class="row p-1">
<div class="col-4 text-right vf-bold vf-small">Port:</div>
<div class="col-8 vf-small" id="vf-vnc-port">-</div>
</div>
</div>
<div class="col-md-6">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="vfCopyVncPassword('{$serviceid}','{$systemURL}')">
Copy VNC Password
</button>
<span id="vf-vnc-copy-confirm" class="text-success vf-small" style="display:none;">Copied!</span>
</div>
</div>
</div>
</div>
</div>
{* Self Service — Billing & Usage Panel *}
{if $selfServiceMode > 0}
<div id="vf-selfservice-panel" class="panel card panel-default mb-3" style="display: none;">
<div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">Billing & Usage</h3>
</div>
<div class="panel-body card-body p-4">
<div id="vf-selfservice-alert" class="alert" style="display: none;"></div>
<div id="vf-selfservice-loader" class="d-flex align-items-center justify-content-center" style="min-height: 60px;">
<div class="spinner-border spinner-border-sm"></div>
</div>
<div id="vf-selfservice-content" style="display: none;">
<div class="row mb-3">
<div class="col-md-6">
<h5 class="vf-bold">Credit Balance</h5>
<div class="h4 mb-3" id="vf-ss-credit-balance">-</div>
</div>
<div class="col-md-6">
<h5 class="vf-bold">Add Credit</h5>
<div class="input-group mb-2">
<input type="number" id="vf-ss-credit-amount" class="form-control" placeholder="Amount" min="1" step="1">
<div class="input-group-append input-group-btn">
<button id="vf-ss-add-credit-btn" onclick="vfAddCredit('{$serviceid}','{$systemURL}')" type="button" class="btn btn-primary">
<span id="vf-ss-add-credit-spinner" class="spinner-border spinner-border-sm" style="display:none;"></span>
Add Credit
</button>
</div>
</div>
</div>
</div>
<h5 class="vf-bold">Usage Breakdown</h5>
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Description</th>
<th class="text-right">Cost</th>
</tr>
</thead>
<tbody id="vf-ss-usage-table">
<tr><td colspan="2" class="text-muted">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<script>vfLoadSelfServiceUsage('{$serviceid}', '{$systemURL}');</script>
</div>
</div>
{/if}
{elseif $serviceStatus eq 'Suspended'}
<div class="panel card panel-default mb-3">
<div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">Service Suspended</h3>
</div>
<div class="panel-body card-body p-4">
<div class="alert alert-danger mb-0">
Your service is currently suspended. Please contact support or pay any outstanding invoices to restore access.
</div>
</div>
</div>
{/if}
{* Billing Overview - Always visible *}
<div class="panel card panel-default mb-3">
<div class="panel-heading card-header">
<h3 class="panel-title card-title m-0">Billing Overview</h3>
</div>
<div class="panel-body card-body">
<div class="row">
<div class="col-lg-6">
<div class="row p-2">
<div class="col-xs-6 col-6 text-right vf-bold">Product:</div>
<div class="col-xs-6 col-6">{$groupname|escape:'htmlall'} - {$product|escape:'htmlall'}</div>
</div>
<div class="row p-2">
<div class="col-xs-6 col-6 text-right vf-bold">{$LANG.recurringamount}:</div>
<div class="col-xs-6 col-6">{$recurringamount|escape:'htmlall'}</div>
</div>
<div class="row p-2">
<div class="col-xs-6 col-6 text-right vf-bold">{$LANG.orderbillingcycle}:</div>
<div class="col-xs-6 col-6">{$billingcycle|escape:'htmlall'}</div>
</div>
</div>
<div class="col-lg-6">
<div class="row p-2">
<div class="col-xs-6 col-6 text-right vf-bold">{$LANG.clientareahostingregdate}:</div>
<div class="col-xs-6 col-6">{$regdate|escape:'htmlall'}</div>
</div>
<div class="row p-2">
<div class="col-xs-6 col-6 text-right vf-bold">{$LANG.clientareahostingnextduedate}:</div>
<div class="col-xs-6 col-6">{$nextduedate|escape:'htmlall'}</div>
</div>
<div class="row p-2">
<div class="col-xs-6 col-6 text-right vf-bold">{$LANG.orderpaymentmethod}:</div>
<div class="col-xs-6 col-6">{$paymentmethod|escape:'htmlall'}</div>
</div>
</div>
</div>
</div>
</div>