Files
virtfusion-whmcs-module/modules/servers/VirtFusionDirect/templates/css/module.css
Prophet731 7e7f3c1c14 feat(ipv6): surface /64 subnet allocations with custom-host PTR flow
VirtFusion's IPv6 allocation model routes a whole /64 to the VPS rather
than exposing discrete host addresses via the API. The previous module
silently filtered these entries — the client saw v4 IPs in the rDNS
panel but no v6 at all, with no indication why, and no way to set a
PTR for a specific address they were using inside the /64.

This commit surfaces subnets as first-class entries throughout:

- IpUtil::extractIps() now returns {addresses, subnets, skipped}. The
  subnets bucket carries {subnet, cidr} pairs for any v6 allocation
  with cidr != 128; /128 entries continue to be treated as discrete
  addresses, and genuinely malformed entries still go to skipped.

- IpUtil::ipv6InSubnet($ip, $prefix, $cidrBits) — new helper that does
  binary-prefix subnet containment via inet_pton + bit masking. Used
  for v6 ownership verification (see below).

- PtrManager::listPtrs() emits subnet-only rows ahead of per-IP rows,
  so the client UI can render the /64 as an informational anchor with
  an entry point for the custom-host flow.

- client.php::rdnsUpdate adds a second ownership-check stage: if the
  submitted IP is v6 AND doesn't match any discrete address, check
  whether it falls inside one of the server's allocated subnets. This
  preserves "only your own IPs" while unlocking the feature.

- Client-side (module.js / module.css) renders subnet rows with a
  collapsible "Add host PTR" form (IP + hostname inputs) that posts
  to the same rdnsUpdate endpoint. Subnet rows get a distinct cyan
  accent so they visually differ from per-host rows.

The usual guards still apply to v6 custom-host writes: forward-DNS
(FCrDNS) verification, PTR regex, per-IP rate limit, same-origin /
POST-method gates. Nothing about the security envelope changes — only
what input is accepted as "you own this IP".

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

577 lines
11 KiB
CSS

/* VirtFusion Direct Provisioning Module Styles */
/* Typography */
.vf-bold {
font-weight: 800;
}
.vf-small {
font-size: 0.9rem;
}
/* Buttons */
.vf-spinner-margin {
margin-right: 7px;
}
/* Status Badges */
.vf-badge {
font-size: 0.75rem;
padding: 0.35rem 0.75rem;
text-transform: uppercase;
font-weight: 700;
border-radius: 6px;
display: inline-block;
}
.vf-badge-active {
background-color: rgba(32, 177, 0, 0.12);
color: #276900;
}
.vf-badge-awaiting {
background-color: rgba(177, 89, 0, 0.12);
color: #692000;
}
.vf-badge-suspended {
background-color: rgba(220, 53, 69, 0.12);
color: #721c24;
}
/* Power Management */
.vf-power-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.vf-btn-power {
min-width: 100px;
font-weight: 600;
text-transform: uppercase;
font-size: 0.8rem;
padding: 0.5rem 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
}
/* Hidden elements (initial state) */
#vf-login-button-spinner {
display: none;
}
#vf-password-reset-button-spinner {
display: none;
}
#vf-password-reset-error {
display: none;
}
#vf-password-reset-success {
display: none;
}
#vf-login-error {
display: none;
}
#vf-server-info {
display: none;
}
#vf-server-info-error {
display: none;
margin: 10px;
}
/* Skeleton Loading */
.vf-skeleton {
background: linear-gradient(90deg, #e9ecef 25%, #f4f4f4 50%, #e9ecef 75%);
background-size: 200% 100%;
animation: vf-skeleton-pulse 1.5s ease-in-out infinite;
border-radius: 4px;
}
.vf-skeleton-line {
height: 14px;
margin-bottom: 10px;
border-radius: 4px;
}
.vf-skeleton-line-short {
width: 60%;
}
.vf-skeleton-line-medium {
width: 80%;
}
@keyframes vf-skeleton-pulse {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Action Progress Banner */
#vf-action-progress {
background: #337ab7;
color: #fff;
padding: 8px 16px;
border-radius: 6px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 10px;
font-size: 0.85rem;
}
#vf-action-progress .spinner-border {
width: 16px;
height: 16px;
border-width: 2px;
}
/* Loader */
#vf-server-info-loader {
min-height: 136px;
}
#vf-loading {
display: inline-block;
width: 30px;
height: 30px;
border: 3px solid rgba(225, 224, 224, 0.3);
border-radius: 50%;
border-top-color: #0e151a;
animation: vf-spin 1s ease-in-out infinite;
-webkit-animation: vf-spin 1s ease-in-out infinite;
}
.vf-loader {
margin: 30px;
}
@keyframes vf-spin {
to {
transform: rotate(360deg);
}
}
@-webkit-keyframes vf-spin {
to {
transform: rotate(360deg);
}
}
/* Network / IP Management */
.vf-ip-row {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
}
.vf-ip-address {
font-family: monospace;
font-size: 0.9rem;
}
/* Backup Timeline */
.vf-timeline {
position: relative;
padding-left: 20px;
border-left: 2px solid #dee2e6;
margin-left: 8px;
}
.vf-timeline-item {
position: relative;
padding: 8px 0 8px 12px;
}
.vf-timeline-dot {
position: absolute;
left: -27px;
top: 12px;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid #fff;
}
.vf-timeline-dot-success {
background: #28a745;
}
.vf-timeline-dot-pending {
background: #ffc107;
}
.vf-timeline-content {
font-size: 0.85rem;
}
/* Server Name Dropdown */
#vf-name-dropdown {
position: relative;
background: #fff;
border: 1px solid #dee2e6;
border-radius: 6px;
margin-top: 4px;
max-width: 250px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
z-index: 10;
}
.vf-name-option {
padding: 6px 12px;
cursor: pointer;
font-family: monospace;
font-size: 0.85rem;
border-bottom: 1px solid #f0f0f0;
}
.vf-name-option:last-child {
border-bottom: none;
}
.vf-name-option:hover {
background: rgba(51,122,183,0.06);
}
/* Copy to Clipboard */
.vf-ip-copy {
padding: 2px 5px;
line-height: 1;
color: #6c757d;
background: none;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
vertical-align: middle;
}
.vf-ip-copy:hover {
color: #337ab7;
background: rgba(51,122,183,0.08);
border-color: rgba(51,122,183,0.2);
}
.vf-copy-tooltip {
position: absolute;
margin-left: 4px;
padding: 2px 8px;
font-size: 0.75rem;
color: #fff;
background: #28a745;
border-radius: 3px;
white-space: nowrap;
animation: vf-fade-in 0.2s ease;
}
@keyframes vf-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* OS Template Gallery */
.vf-os-category-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
margin-top: 6px;
border: 1px solid #dee2e6;
border-radius: 6px;
cursor: pointer;
font-weight: 700;
font-size: 0.9rem;
user-select: none;
transition: background 0.15s;
}
.vf-os-category:first-child .vf-os-category-header {
margin-top: 0;
}
.vf-os-category-header:hover {
background: rgba(0,0,0,0.03);
}
.vf-os-category-icon {
width: 28px;
height: 28px;
min-width: 28px;
min-height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 0.85rem;
flex-shrink: 0;
overflow: hidden;
}
.vf-os-category-icon img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
}
.vf-os-category-arrow {
margin-left: auto;
font-size: 0.7rem;
color: #888;
}
.vf-os-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 10px 0;
margin-bottom: 4px;
}
.vf-os-card {
width: 120px;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 10px 8px;
text-align: center;
cursor: pointer;
overflow: hidden;
transition: border-color 0.15s, background-color 0.15s, box-shadow 0.15s;
}
.vf-os-card:hover {
border-color: #337ab7;
}
.vf-os-card-selected {
border-color: #337ab7;
background: rgba(51,122,183,0.06);
box-shadow: 0 0 0 1px rgba(51,122,183,0.3);
}
.vf-os-card-eol {
opacity: 0.6;
}
.vf-os-icon {
width: 40px;
height: 40px;
border-radius: 8px;
margin: 0 auto 6px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 1.1rem;
overflow: hidden;
flex-shrink: 0;
}
.vf-os-icon img {
max-width: 100%;
max-height: 100%;
width: 24px;
height: 24px;
object-fit: contain;
}
.vf-os-label {
font-weight: 600;
font-size: 12px;
line-height: 1.2;
}
.vf-os-version {
font-size: 10px;
color: #888;
line-height: 1.2;
}
.vf-os-eol-badge {
display: inline-block;
background: #dc3545;
color: #fff;
font-size: 9px;
font-weight: 700;
padding: 1px 5px;
border-radius: 3px;
margin-top: 3px;
}
#vf-os-details {
border-top: 1px solid #dee2e6;
padding-top: 10px;
}
.vf-os-search {
margin-bottom: 10px;
}
@media (max-width: 768px) {
.vf-os-grid {
gap: 6px;
}
.vf-os-card {
width: calc(50% - 3px);
min-width: 100px;
}
}
/* Resource panel */
.vf-resource-item .progress {
background-color: rgba(0,0,0,0.08);
border-radius: 4px;
}
/* Order form slider UI */
.vf-slider-container {
padding: 8px 0;
}
.vf-slider-value {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 4px;
text-align: center;
}
.vf-slider {
width: 100%;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: #ddd;
border-radius: 3px;
outline: none;
cursor: pointer;
}
.vf-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #337ab7;
cursor: pointer;
border: 2px solid #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.vf-slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #337ab7;
cursor: pointer;
border: 2px solid #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
/* Toggle Switch */
.vf-toggle-input {
display: none;
}
.vf-toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
background: #ccc;
border-radius: 10px;
transition: background 0.2s;
flex-shrink: 0;
}
.vf-toggle-switch::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: #fff;
border-radius: 50%;
transition: transform 0.2s;
}
.vf-toggle-input:checked + .vf-toggle-switch {
background: #28a745;
}
.vf-toggle-input:checked + .vf-toggle-switch::after {
transform: translateX(16px);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.vf-power-buttons {
flex-direction: column;
}
.vf-btn-power {
width: 100%;
}
.vf-ip-row {
flex-wrap: wrap;
}
}
/* =========================================================================
Reverse DNS panel
========================================================================= */
.vf-rdns-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid rgba(0,0,0,.06);
}
.vf-rdns-row:last-child { border-bottom: none; }
.vf-rdns-ip {
font-family: monospace;
font-size: 13px;
min-width: 180px;
font-weight: 600;
}
.vf-rdns-edit {
display: flex;
flex: 1 1 auto;
gap: 6px;
align-items: center;
min-width: 240px;
}
.vf-rdns-input {
flex: 1 1 auto;
min-width: 180px;
max-width: 420px;
font-family: monospace;
font-size: 13px;
}
.vf-rdns-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .02em;
line-height: 1.4;
white-space: nowrap;
}
.vf-rdns-msg {
flex-basis: 100%;
font-size: 12px;
display: none;
padding-left: 180px;
}
.vf-rdns-admin-row {
display: flex;
align-items: center;
gap: 12px;
padding: 4px 0;
font-size: 13px;
}
.vf-rdns-ip-admin {
font-family: monospace;
font-weight: 600;
min-width: 180px;
}
.vf-rdns-ptr-admin {
font-family: monospace;
color: #333;
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 768px) {
.vf-rdns-row { flex-direction: column; align-items: stretch; }
.vf-rdns-edit { flex-direction: column; align-items: stretch; }
.vf-rdns-msg { padding-left: 0; }
}
/* Subnet-only rows (IPv6 /64 allocations). Distinct visual treatment so
customers see "this is a subnet, not a host" without reading the badge. */
.vf-rdns-subnet-row {
background: rgba(23, 162, 184, 0.04);
border-left: 3px solid #17a2b8;
padding-left: 8px;
}
.vf-rdns-subnet-form {
flex-basis: 100%;
padding: 10px 0 0 180px;
display: flex;
flex-direction: column;
gap: 6px;
}
.vf-rdns-subnet-inputs {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.vf-rdns-subnet-actions {
display: flex;
gap: 6px;
align-items: center;
}
@media (max-width: 768px) {
.vf-rdns-subnet-form { padding-left: 0; }
.vf-rdns-subnet-inputs { flex-direction: column; }
}