feat(service): custom product, lịch sử giám sát VM và mã hoá response theo phiên

- Dịch vụ custom: bảng custom_products + services.custom_product_id; createService nhận
  custom{} thay product_id, giá = đơn giá tháng x số tháng chu kỳ (1/3/6/12/24/36),
  gia hạn tính theo cùng đơn giá, ServiceClientDetailResource trả custom_product và specs.
- Giám sát VM: GET v1/system/vm-metrics/history gom lịch sử về tối đa 240 điểm;
  lỗi lấy dung lượng đĩa không còn làm hỏng cả thẻ metrics.
- SealResponse: AES-256-GCM theo khoá phiên do proxy Next cấp qua header x-seal-key
  (HMAC của session id). Client opt-in bằng x-seal; chi tiết dịch vụ, mật khẩu VM và
  console ticket luôn được mã hoá. Body gửi lên dạng {enc} được giải mã trước validate;
  khoá lệch trả 428 để client lấy khoá mới. Đây là che mắt DevTools, không phải bảo mật
  chống chính chủ tài khoản.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-27 13:58:50 +07:00
co-authored by Claude Sonnet 5.5
parent 8108e8fe53
commit 6315a392c0
19 changed files with 549 additions and 33 deletions
@@ -47,8 +47,17 @@ class ServiceClientDetailResource extends JsonResource
*
* @return array<string, mixed>
*/
private function resolveSpecs($vm, $product): ?array
private function resolveSpecs($vm, $product, $custom = null): ?array
{
if ($custom) {
return [
'cpu' => (int) ($custom->cpu ?? 0),
'ram' => round(($custom->ram_mb ?? 0) / 1024, 2),
'disk' => (int) ($custom->disk_gb ?? 0),
'bandwidth' => (int) ($custom->bandwidth_gb ?? 0),
];
}
if ($vm && ($vm->cpu !== null || $vm->ram_mb !== null || $vm->disk_gb !== null)) {
return [
'cpu' => (int) ($vm->cpu ?? 0),
@@ -85,6 +94,8 @@ class ServiceClientDetailResource extends JsonResource
$order = $orderItem?->order;
$vm = $this->resource->vmware;
$product = $orderItem?->product ?? $this->resource->product;
$custom = $this->resource->customProduct;
$renewalSource = $custom ?? $product;
$hosting = $this->resource->hostingAccount;
$pleskCredentials = $hosting ? $this->resolvePleskCredentials($hosting) : null;
$dnsZone = $this->resolveDnsZone();
@@ -102,18 +113,21 @@ class ServiceClientDetailResource extends JsonResource
'version' => $orderItem?->os?->version ?? $vm?->os?->version,
'group' => $orderItem?->os?->osGroup?->name ?? $vm?->os?->osGroup?->name,
],
'specs' => $this->type === 'hosting' ? null : $this->resolveSpecs($vm, $product),
'specs' => $this->type === 'hosting' ? null : $this->resolveSpecs($vm, $product, $custom),
'service_plan' => $this->type === 'hosting' ? $product?->servicePlan?->summary() : null,
'custom_product' => $custom?->summary(),
'order' => $order ? [
'total' => (float) $order->total,
'months' => (int) $orderItem->months,
'billing_label' => $orderItem->billing_label,
'cycle_price' => (float) ($product?->pricings()
->where('billing_cycle', $orderItem->billing_cycle)
->where('is_active', 1)
->value('price') ?? 0),
'cycle_price' => $custom
? (float) $custom->unit_price * (int) $orderItem->months
: (float) ($product?->pricings()
->where('billing_cycle', $orderItem->billing_cycle)
->where('is_active', 1)
->value('price') ?? 0),
'addon_total' => \App\Support\RenewalPricing::addonTotal($this->resource, 1),
'renewal_options' => \App\Support\RenewalPricing::options($product),
'renewal_options' => \App\Support\RenewalPricing::options($renewalSource),
] : null,
'vm' => $vm ? [
'ip_address' => $vm->ipAddress?->ip ?? $vm->ip,