feat: Add DNS management resources and importer

- Implemented DomainCollection, DomainDetailResource, DomainNSCollection, DomainNSResource, and DomainResource for handling DNS domain and record data.
- Created VpsServiceCollection and VpsServiceResource for VPS service management.
- Added DnsManagerNS and DnsManagerUser models to represent DNS records and user associations.
- Developed AdminZoneDnsSqlImporter for importing DNS data from SQL dumps, including customer and DNS domain records.
- Added migration scripts to update the customers table with new fields and sync foreign key references for DNS tables.
- Created a migration preview document outlining the mapping and schema changes for the admin zone DNS import.
This commit is contained in:
baodev0
2026-05-26 13:54:33 +07:00
parent 520f21cb11
commit fccb0c353a
30 changed files with 4030 additions and 105 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,85 @@
<?php
namespace App\Console\Commands;
use App\Services\Imports\AdminZoneDnsSqlImporter;
use Illuminate\Console\Command;
class ImportAdminZoneDnsSql extends Command
{
protected $signature = 'admin-zonedns:import
{path? : Path to the admin_zonedns SQL dump}
{--preview : Parse and report only, without writing to DB}
{--customers-only : Import only tbl.users into customers}
{--dns-only : Import only DNS tables and customer-domain links}';
protected $description = 'Import admin_zonedns SQL dump into portal customers and DNS tables';
public function handle(AdminZoneDnsSqlImporter $importer): int
{
$path = $this->argument('path') ?: base_path('admin_zonedns (1).sql');
$preview = (bool) $this->option('preview');
$customersOnly = (bool) $this->option('customers-only');
$dnsOnly = (bool) $this->option('dns-only');
if ($customersOnly && $dnsOnly) {
$this->error('Use either --customers-only or --dns-only, not both.');
return self::FAILURE;
}
try {
$summary = $importer->import(
path: $path,
preview: $preview,
importCustomers: ! $dnsOnly,
importDns: ! $customersOnly,
);
} catch (\Throwable $e) {
$this->error($e->getMessage());
return self::FAILURE;
}
$mode = $preview ? 'PREVIEW' : 'IMPORTED';
$this->info("{$mode} from: {$path}");
$this->table(
['Section', 'Created', 'Updated', 'Restored', 'Skipped', 'Source rows'],
[
[
'customers',
$summary['customers']['created'],
$summary['customers']['updated'],
$summary['customers']['restored'],
$summary['customers']['skipped'],
$summary['source']['users'],
],
[
'dns_domains',
$summary['dns_domains']['created'],
$summary['dns_domains']['updated'],
'-',
$summary['dns_domains']['skipped'],
$summary['source']['dns_manager_domain'],
],
[
'dns_records',
$summary['dns_records']['created'],
$summary['dns_records']['updated'],
'-',
$summary['dns_records']['skipped'],
$summary['source']['dns_manager_ns'],
],
[
'dns_links',
$summary['dns_links']['created'],
$summary['dns_links']['updated'],
'-',
$summary['dns_links']['skipped'],
$summary['source']['dns_domain_user'],
],
]
);
return self::SUCCESS;
}
}
@@ -0,0 +1,138 @@
<?php
namespace App\Http\Controllers\Api\Admin\DnsManager;
use App\Http\Controllers\Controller;
use App\Repositories\DnsManager\Record\DnsRecordRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class RecordController extends Controller
{
public function __construct(
private readonly DnsRecordRepositoryInterface $recordRepository,
) {}
public function index(Request $request): JsonResponse
{
$perPage = min((int) $request->get('per_page', 15), 500);
$filter = trim((string) $request->get('filter', ''));
$type = trim((string) $request->get('type', ''));
$query = $this->recordRepository->model
->with(['domain.server'])
->newQuery();
if ($filter !== '') {
$query->where(function ($subQuery) use ($filter) {
$subQuery->where('name', 'like', "%{$filter}%")
->orWhere('value', 'like', "%{$filter}%")
->orWhereHas('domain', function ($domainQuery) use ($filter) {
$domainQuery->where('domain_name', 'like', "%{$filter}%")
->orWhereHas('server', function ($serverQuery) use ($filter) {
$serverQuery->where('name', 'like', "%{$filter}%")
->orWhere('ip_address', 'like', "%{$filter}%");
});
});
});
}
if ($type !== '') {
$query->where('type', $type);
}
$paginator = $query->orderByDesc('id')->paginate($perPage);
return response()->json([
'error' => false,
'data' => [
'items' => $paginator->getCollection()->map(fn($record) => $this->formatRecord($record)),
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
],
]);
}
public function show(int $id): JsonResponse
{
$record = $this->recordRepository->model
->with(['domain.server'])
->find($id);
if (! $record) {
return response()->json(['error' => true, 'message' => "Không tìm thấy bản ghi DNS #$id"], 404);
}
return response()->json([
'error' => false,
'data' => $this->formatRecord($record),
]);
}
public function update(int $id, Request $request): JsonResponse
{
$record = $this->recordRepository->model
->with(['domain.server'])
->find($id);
if (! $record) {
return response()->json(['error' => true, 'message' => "Không tìm thấy bản ghi DNS #$id"], 404);
}
$validated = $request->validate([
'type' => 'required|string|in:A,AAAA,CNAME,TXT,MX,NS,SRV',
'name' => 'required|string|max:255',
'value' => 'required|string',
'ttl' => 'nullable|integer|min:60',
'priority' => 'nullable|integer|min:0',
'weight' => 'nullable|integer|min:0',
'port' => 'nullable|integer|min:0',
'service_name' => 'nullable|string|max:255',
'protocol' => 'nullable|string|max:50',
]);
$payload = [
'type' => $validated['type'],
'type_valid' => $validated['type'],
'name' => $validated['name'],
'value' => $validated['value'],
'ttl' => $validated['ttl'] ?? 300,
'priority' => array_key_exists('priority', $validated) ? (string) ($validated['priority'] ?? 0) : $record->priority,
'weight' => array_key_exists('weight', $validated) ? (string) ($validated['weight'] ?? 0) : $record->weight,
'port' => array_key_exists('port', $validated) ? (string) ($validated['port'] ?? 0) : $record->port,
'service_name' => $validated['service_name'] ?? null,
'protocol' => $validated['protocol'] ?? null,
];
$this->recordRepository->update($id, $payload);
return $this->show($id);
}
private function formatRecord($record): array
{
return [
'id' => $record->id,
'dns_manager_domain_id' => $record->dns_manager_domain_id,
'domain_name' => $record->domain?->domain_name,
'dns_manager_server_id' => $record->domain?->dns_manager_server_id,
'server_name' => $record->domain?->server?->name,
'server_ip_address' => $record->domain?->server?->ip_address,
'type' => $record->type,
'type_valid' => $record->type_valid,
'name' => $record->name,
'value' => $record->value,
'priority' => $record->priority !== null ? (int) $record->priority : null,
'weight' => $record->weight !== null ? (int) $record->weight : null,
'port' => $record->port !== null ? (int) $record->port : null,
'service_name' => $record->service_name,
'protocol' => $record->protocol,
'plesk_id' => $record->plesk_id,
'ttl' => (int) $record->ttl,
'created_at' => $record->created_at,
'updated_at' => $record->updated_at,
];
}
}
@@ -4,46 +4,48 @@ namespace App\Http\Controllers\Api\Admin\DnsManager;
use App\Http\Controllers\Controller;
use App\Repositories\DnsManager\Server\DnsServerRepositoryInterface;
use App\Services\ServerService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
class ServerController extends Controller
{
public function __construct(
private readonly DnsServerRepositoryInterface $serverRepository,
private readonly ServerService $serverService,
) {}
public function index(Request $request): JsonResponse
{
$perPage = min((int) $request->get('per_page', 15), 500);
$filter = $request->get('filter');
$status = $request->get('status');
$filter = $request->get('filter');
$status = $request->get('status');
$query = $this->serverRepository->model->newQuery();
if ($filter) {
$query->where(function ($q) use ($filter) {
$q->where('name', 'like', "%{$filter}%")
->orWhere('ip_address', 'like', "%{$filter}%");
$query->where(function ($subQuery) use ($filter) {
$subQuery->where('name', 'like', "%{$filter}%")
->orWhere('ip_address', 'like', "%{$filter}%")
->orWhere('username', 'like', "%{$filter}%");
});
}
if ($status) {
if ($status !== null && $status !== '') {
$query->where('status', $status);
}
$paginator = $query->paginate($perPage);
$paginator = $query->orderByDesc('id')->paginate($perPage);
return response()->json([
'error' => false,
'data' => [
'items' => $paginator->items(),
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'data' => [
'items' => $paginator->getCollection()->map(fn($server) => $this->formatServer($server)),
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'last_page' => $paginator->lastPage(),
],
]);
}
@@ -56,30 +58,44 @@ class ServerController extends Controller
return response()->json(['error' => true, 'message' => "Không tìm thấy server #$id"], 404);
}
return response()->json(['error' => false, 'data' => $server]);
return response()->json([
'error' => false,
'data' => $this->formatServer($server),
]);
}
public function store(Request $request): JsonResponse
{
$request->validate([
'name' => 'required|string|max:255',
$validated = $request->validate([
'name' => 'required|string|max:255',
'ip_address' => 'required|string|max:255',
'username' => 'required|string|max:255',
'password' => 'required|string|max:255',
'api_key' => 'nullable|string',
'status' => 'nullable|string|in:activated,Stopped',
'username' => 'required|string|max:255',
'password' => 'required|string|max:255',
'api_key' => 'nullable|string',
'status' => 'nullable|string|in:activated,Stopped',
]);
DB::beginTransaction();
try {
$data = $request->all();
$data['password'] = Crypt::encryptString($request->password);
$server = $this->serverRepository->create($data);
$server = $this->serverRepository->create([
'name' => $validated['name'],
'ip_address' => $validated['ip_address'],
'username' => $validated['username'],
'password' => encrypt($validated['password']),
'api_key' => $validated['api_key'] ?? null,
'status' => $validated['status'] ?? 'activated',
]);
DB::commit();
return response()->json(['error' => false, 'data' => $server], 201);
return response()->json([
'error' => false,
'data' => $this->formatServer($server),
], 201);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
}
@@ -92,29 +108,34 @@ class ServerController extends Controller
return response()->json(['error' => true, 'message' => "Không tìm thấy server #$id"], 404);
}
$request->validate([
'name' => 'required|string|max:255',
'ip_address' => 'required|string|max:255',
'username' => 'required|string|max:255',
'password' => 'nullable|string|max:255',
'api_key' => 'nullable|string',
'status' => 'nullable|string|in:activated,Stopped',
$validated = $request->validate([
'name' => 'sometimes|required|string|max:255',
'ip_address' => 'sometimes|required|string|max:255',
'username' => 'sometimes|required|string|max:255',
'password' => 'nullable|string|max:255',
'api_key' => 'nullable|string',
'status' => 'nullable|string|in:activated,Stopped',
]);
DB::beginTransaction();
try {
$data = $request->except(['password']);
if (!empty($request->password)) {
$data['password'] = Crypt::encryptString($request->password);
try {
$payload = collect($validated)
->except('password')
->toArray();
if ($request->filled('password')) {
$payload['password'] = encrypt((string) $request->password);
}
$this->serverRepository->update($id, $data);
$this->serverRepository->update($id, $payload);
DB::commit();
return response()->json(['error' => false, 'data' => $this->serverRepository->find($id)]);
return $this->show($id);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
}
@@ -131,4 +152,37 @@ class ServerController extends Controller
return response()->json(['error' => false, 'message' => 'Đã xoá server thành công']);
}
public function checkConnection(int $id): JsonResponse
{
$server = $this->serverRepository->find($id);
if (!$server) {
return response()->json(['error' => true, 'message' => "Không tìm thấy server #$id"], 404);
}
$connected = $this->serverService->checkConnectionByServer($server);
return response()->json([
'error' => !$connected,
'data' => ['connected' => $connected],
'message' => $connected
? 'Kết nối server thành công.'
: 'Không thể kết nối tới server. Vui lòng kiểm tra lại IP hoặc tài khoản.',
]);
}
private function formatServer($server): array
{
return [
'id' => $server->id,
'name' => $server->name,
'ip_address' => $server->ip_address,
'username' => $server->username,
'api_key' => $server->api_key,
'status' => $server->status,
'created_at' => $server->created_at,
'updated_at' => $server->updated_at,
];
}
}
@@ -0,0 +1,284 @@
<?php
namespace App\Http\Controllers\Api\Client\Service;
use App\Http\Controllers\Controller;
use App\Http\Resources\Api\Service\DomainCollection;
use App\Http\Resources\Api\Service\DomainDetailResource;
use App\Http\Resources\Api\Service\DomainNSResource;
use App\Models\DnsDomain;
use App\Models\DnsManagerNS;
use App\Services\ActivityLogger;
use App\Services\ServerService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class DomainController extends Controller
{
public function __construct(
protected ActivityLogger $activityLogger,
protected ServerService $serverService,
) {}
public function index(Request $request): JsonResponse
{
$customer = $request->user();
$perPage = min((int) $request->get('per_page', 15), 100);
$domains = DnsDomain::query()
->with('server')
->withCount('record')
->whereHas('customer', fn ($query) => $query->where('customers.id', $customer->id))
->orderByDesc('id')
->paginate($perPage);
return (new DomainCollection($domains))
->additional([
'error' => false,
'message' => 'Lấy dữ liệu thành công',
'page_type' => 'service_domain',
])
->response();
}
public function show(Request $request, int $domainId): JsonResponse
{
$domain = $this->resolveCustomerDomain($request, $domainId);
if (! $domain) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy domain'], 404);
}
$perPage = min((int) $request->get('per_page', 15), 100);
$domain->load('server')->loadCount('record');
$records = DnsManagerNS::query()
->where('dns_manager_domain_id', $domain->id)
->orderBy('type')
->orderBy('name')
->paginate($perPage);
return response()->json([
'error' => false,
'message' => 'Lấy dữ liệu thành công',
'page_type' => 'service_domain_detail',
'data' => new DomainDetailResource($domain, $records),
]);
}
public function storeRecord(Request $request, int $domainId): JsonResponse
{
$domain = $this->resolveCustomerDomain($request, $domainId);
if (! $domain) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy domain'], 404);
}
$payload = $this->validateDomainRecordPayload($request);
$this->serverService->findServerByID($domain->dns_manager_server_id);
$serverPayload = $this->serverService->handlePayload([
...$payload,
'domain' => $domain->domain_name,
])->addRecord();
if (! empty($serverPayload['error'])) {
return response()->json([
'error' => true,
'message' => $serverPayload['message'] ?? 'Không thể tạo DNS record trên server',
], 422);
}
DB::beginTransaction();
try {
$record = DnsManagerNS::query()->create([
'dns_manager_domain_id' => $domain->id,
'type' => $payload['type'],
'type_valid' => $payload['type'],
'name' => $payload['name'],
'value' => $payload['value'],
'ttl' => $payload['ttl'] ?? 300,
'priority' => (string) ($payload['priority'] ?? 0),
'weight' => (string) ($payload['weight'] ?? 0),
'port' => (string) ($payload['port'] ?? 0),
'service_name' => $payload['service_name'] ?? null,
'protocol' => $payload['protocol'] ?? null,
'plesk_id' => $serverPayload['data']['id'] ?? null,
]);
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
$customer = $request->user();
$this->activityLogger->log(
event: 'created',
logName: 'DNS',
description: "Customer {$customer->email} tạo DNS record #{$record->id} của domain {$domain->domain_name}",
subject: $record,
newValues: [
'domain_id' => $domain->id,
'domain_name' => $domain->domain_name,
'type' => $record->type,
'name' => $record->name,
'value' => $record->value,
],
);
return response()->json([
'error' => false,
'message' => 'Tạo DNS record thành công',
'data' => new DomainNSResource($record),
], 201);
}
public function updateRecord(Request $request, int $domainId, int $recordId): JsonResponse
{
$domain = $this->resolveCustomerDomain($request, $domainId);
if (! $domain) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy domain'], 404);
}
$record = DnsManagerNS::query()
->where('dns_manager_domain_id', $domain->id)
->find($recordId);
if (! $record) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy bản ghi DNS'], 404);
}
$payload = $this->validateDomainRecordPayload($request);
if (! empty($record->plesk_id)) {
$this->serverService->findServerByID($domain->dns_manager_server_id);
$serverPayload = $this->serverService->handlePayload([
...$payload,
'domain' => $domain->domain_name,
])->updateRecord((int) $record->plesk_id);
if (! empty($serverPayload['error'])) {
return response()->json([
'error' => true,
'message' => $serverPayload['message'] ?? 'Không thể cập nhật DNS record trên server',
], 422);
}
}
$record->update([
'type' => $payload['type'],
'type_valid' => $record->type_valid ?: $payload['type'],
'name' => $payload['name'],
'value' => $payload['value'],
'ttl' => $payload['ttl'] ?? $record->ttl,
'priority' => isset($payload['priority']) ? (string) $payload['priority'] : $record->priority,
'weight' => isset($payload['weight']) ? (string) $payload['weight'] : $record->weight,
'port' => isset($payload['port']) ? (string) $payload['port'] : $record->port,
'service_name' => $payload['service_name'] ?? $record->service_name,
'protocol' => $payload['protocol'] ?? $record->protocol,
]);
$customer = $request->user();
$this->activityLogger->log(
event: 'updated',
logName: 'DNS',
description: "Customer {$customer->email} cập nhật DNS record #{$record->id} của domain {$domain->domain_name}",
subject: $record,
newValues: [
'domain_id' => $domain->id,
'domain_name' => $domain->domain_name,
'type' => $record->type,
'name' => $record->name,
'value' => $record->value,
],
);
return response()->json([
'error' => false,
'message' => 'Cập nhật DNS record thành công',
'data' => new DomainNSResource($record),
]);
}
public function destroyRecord(Request $request, int $domainId, int $recordId): JsonResponse
{
$domain = $this->resolveCustomerDomain($request, $domainId);
if (! $domain) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy domain'], 404);
}
$record = DnsManagerNS::query()
->where('dns_manager_domain_id', $domain->id)
->find($recordId);
if (! $record) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy bản ghi DNS'], 404);
}
if (! empty($record->plesk_id)) {
$this->serverService->findServerByID($domain->dns_manager_server_id);
$serverPayload = $this->serverService->deleteRecord((int) $record->plesk_id);
if (! empty($serverPayload['error'])) {
return response()->json([
'error' => true,
'message' => $serverPayload['message'] ?? 'Không thể xóa DNS record trên server',
], 422);
}
}
$snapshot = new DomainNSResource($record);
$record->delete();
$customer = $request->user();
$this->activityLogger->log(
event: 'deleted',
logName: 'DNS',
description: "Customer {$customer->email} xóa DNS record #{$recordId} của domain {$domain->domain_name}",
subject: $domain,
newValues: [
'domain_id' => $domain->id,
'domain_name' => $domain->domain_name,
'record_id' => $recordId,
],
);
return response()->json([
'error' => false,
'message' => 'Xóa DNS record thành công',
'data' => $snapshot,
]);
}
private function resolveCustomerDomain(Request $request, int $domainId): ?DnsDomain
{
$customer = $request->user();
return DnsDomain::query()
->with('server')
->where('id', $domainId)
->whereHas('customer', fn ($query) => $query->where('customers.id', $customer->id))
->first();
}
private function validateDomainRecordPayload(Request $request): array
{
return $request->validate([
'type' => ['required', 'string', 'in:A,AAAA,CNAME,TXT,MX,NS,SRV'],
'name' => ['required', 'string', 'max:255'],
'value' => ['required', 'string'],
'ttl' => ['nullable', 'integer', 'min:60', 'max:86400'],
'priority' => ['nullable', 'integer', 'min:0', 'max:65535'],
'weight' => ['nullable', 'integer', 'min:0', 'max:65535'],
'port' => ['nullable', 'integer', 'min:0', 'max:65535'],
'service_name' => ['nullable', 'string', 'max:255'],
'protocol' => ['nullable', 'string', 'max:255'],
]);
}
}
@@ -3,8 +3,8 @@
namespace App\Http\Controllers\Api\Client\Service;
use App\Http\Controllers\Controller;
use App\Http\Resources\Api\Service\VpsServiceCollection;
use App\Http\Resources\Api\Client\Service\ServiceClientDetailResource;
use \App\Http\Resources\Api\Client\Service\ServiceClientResource;
use App\Http\Services\vCenter;
use App\Models\Order;
use App\Models\Service;
@@ -31,22 +31,75 @@ class ServiceController extends Controller
$this->activityLogger = $activityLogger;
}
public function getService(Request $request)
public function indexVps(Request $request): JsonResponse
{
$limit = $request->get('limit', 10);
$services = $this->serviceClientService->getActiveServices($limit);
$perPage = min((int) $request->get('per_page', 15), 100);
$customer = $request->user();
return ServiceClientResource::collection($services)->additional([
$services = $this->serviceClientService->getActiveServices($customer->id, 'vps', $perPage);
return (new VpsServiceCollection($services))
->additional([
'error' => false,
'message' => 'Lấy dữ liệu thành công',
'page_type' => 'service_vps',
])
->response();
}
public function indexHosting(Request $request): JsonResponse
{
$perPage = min((int) $request->get('per_page', 15), 100);
return response()->json([
'error' => false,
'message' => 'Lấy dữ liệu thành công',
'page_type' => 'service',
'page_type' => 'service_hosting',
'data' => [
'items' => [],
'pagination' => [
'total' => 0,
'per_page' => $perPage,
'current_page' => 1,
'last_page' => 1,
],
],
]);
}
public function indexWebDesign(Request $request): JsonResponse
{
$perPage = min((int) $request->get('per_page', 15), 100);
return response()->json([
'error' => false,
'message' => 'Lấy dữ liệu thành công',
'page_type' => 'service_web_design',
'data' => [
'items' => [],
'pagination' => [
'total' => 0,
'per_page' => $perPage,
'current_page' => 1,
'last_page' => 1,
],
],
]);
}
public function getDetail(Request $request, string $uuid)
{
$service = $this->serviceClientService->getDetailService($uuid);
$service = $this->serviceClientService->getDetailService($uuid, $request->user()->id);
if (!$service) {
return response()->json(
[
'error' => true,
'message' => 'Không tìm thấy dịch vụ',
],
404,
);
}
return (new ServiceClientDetailResource($service))->additional([
'error' => false,
@@ -113,7 +166,7 @@ class ServiceController extends Controller
);
return response()->json([
'error' => false,
'error' => false,
'message' => $ticket,
]);
}
@@ -188,7 +241,7 @@ class ServiceController extends Controller
$customer = $request->user();
$service = Service::with([
'product.osVersions' => fn ($q) => $q->where('status', 1)->with('osGroup'),
'product.osVersions' => fn($q) => $q->where('status', 1)->with('osGroup'),
])
->where('uuid', $uuid)
->where('customer_id', $customer->id)
@@ -199,7 +252,10 @@ class ServiceController extends Controller
}
if (!in_array($service->status, ['active', 'suspended'], true)) {
return response()->json(['success' => false, 'message' => 'Dịch vụ không ở trạng thái hợp lệ để xem OS'], 422);
return response()->json(
['success' => false, 'message' => 'Dịch vụ không ở trạng thái hợp lệ để xem OS'],
422,
);
}
if (!$service->product) {
@@ -207,32 +263,38 @@ class ServiceController extends Controller
}
$grouped = $service->product->osVersions
->groupBy(fn ($os) => $os->osGroup?->id ?? 0)
->map(fn ($items) => [
'group' => [
'id' => $items->first()->osGroup?->id,
'name' => $items->first()->osGroup?->name,
'thumbnail' => $items->first()->osGroup?->thumbnail,
->groupBy(fn($os) => $os->osGroup?->id ?? 0)
->map(
fn($items) => [
'group' => [
'id' => $items->first()->osGroup?->id,
'name' => $items->first()->osGroup?->name,
'thumbnail' => $items->first()->osGroup?->thumbnail,
],
'items' => $items
->map(
fn($os) => [
'id' => $os->id,
'version' => $os->version,
'template_id' => $os->template_id,
],
)
->values(),
],
'items' => $items->map(fn ($os) => [
'id' => $os->id,
'version' => $os->version,
'template_id' => $os->template_id,
])->values(),
])
)
->values();
return response()->json([
'success' => true,
'data' => $grouped,
'data' => $grouped,
]);
}
public function rebuild(Request $request, string $uuid, vCenter $serviceVCenter): JsonResponse
{
$customer = $request->user();
$payload = $request->validate([
'os_id' => ['required', 'integer', 'exists:os,id'],
$payload = $request->validate([
'os_id' => ['required', 'integer', 'exists:os,id'],
'password' => ['nullable', 'string', 'max:255'],
]);
@@ -246,18 +308,18 @@ class ServiceController extends Controller
}
if ($service->status === 'suspended') {
return response()->json(['success' => false, 'message' => 'Dịch vụ đang bị tạm ngưng, không thể rebuild'], 422);
return response()->json(
['success' => false, 'message' => 'Dịch vụ đang bị tạm ngưng, không thể rebuild'],
422,
);
}
if (!$service->isActive()) {
return response()->json(['success' => false, 'message' => 'Dịch vụ không ở trạng thái active'], 422);
}
$allowedOsIds = $service->product?->osVersions()
->where('status', 1)
->pluck('os.id')
->map('intval')
->toArray() ?? [];
$allowedOsIds =
$service->product?->osVersions()->where('status', 1)->pluck('os.id')->map('intval')->toArray() ?? [];
if (!in_array((int) $payload['os_id'], $allowedOsIds, true)) {
return response()->json(['success' => false, 'message' => 'OS không hợp lệ cho sản phẩm hiện tại'], 422);
@@ -269,31 +331,34 @@ class ServiceController extends Controller
}
DB::table('reinstall_logs')->insert([
'product_id' => $vm->id,
'product_id' => $vm->id,
'product_type' => 4,
'custome_id' => $customer->id,
'os_id' => $payload['os_id'],
'status' => 'pending',
'initiator' => 'client',
'note' => 'Client rebuild via service UUID: ' . $uuid,
'created_at' => now(),
'updated_at' => now(),
'custome_id' => $customer->id,
'os_id' => $payload['os_id'],
'status' => 'pending',
'initiator' => 'client',
'note' => 'Client rebuild via service UUID: ' . $uuid,
'created_at' => now(),
'updated_at' => now(),
]);
$result = $serviceVCenter->reBuildVPS((string) $vm->name);
if (is_string($result) || ($result['error'] ?? false)) {
return response()->json([
'success' => false,
'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể rebuild VM'),
], 400);
return response()->json(
[
'success' => false,
'message' => is_string($result) ? $result : $result['message'] ?? 'Không thể rebuild VM',
],
400,
);
}
$this->activityLogger->log(
event: 'rebuild',
logName: 'VM',
event: 'rebuild',
logName: 'VM',
description: "Customer {$customer->email} rebuild VM: {$vm->name} với OS ID: {$payload['os_id']} qua service UUID: {$uuid}",
subject: $vm,
newValues: ['os_id' => $payload['os_id'], 'service_uuid' => $uuid],
subject: $vm,
newValues: ['os_id' => $payload['os_id'], 'service_uuid' => $uuid],
);
return response()->json([
@@ -301,5 +366,4 @@ class ServiceController extends Controller
'message' => 'Đã gửi yêu cầu rebuild VM',
]);
}
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ class DomainRequest extends FormRequest
'domain_name' => 'required|string|max:255',
'dns_manager_server_id' => 'required|integer|exists:dns_manager_servers,id',
'user_id' => 'nullable|array',
'user_id.*' => 'integer|exists:users,id',
'user_id.*' => 'integer|exists:customers,id',
];
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources\Api\Service;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class DomainCollection extends ResourceCollection
{
public $collects = DomainResource::class;
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
$data = [
'items' => $this->collection,
];
if (method_exists($this->resource, 'total')) {
$data['pagination'] = [
'total' => $this->resource->total(),
'per_page' => $this->resource->perPage(),
'current_page' => $this->resource->currentPage(),
'last_page' => $this->resource->lastPage(),
];
}
return $data;
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Resources\Api\Service;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class DomainDetailResource extends JsonResource
{
public function __construct($resource, protected mixed $records = null)
{
parent::__construct($resource);
}
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'domain' => new DomainResource($this->resource),
'ns_count' => (int) ($this->record_count ?? 0),
'records' => $this->records ? new DomainNSCollection($this->records) : [
'items' => [],
'pagination' => [
'total' => 0,
'per_page' => 0,
'current_page' => 1,
'last_page' => 1,
],
],
];
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources\Api\Service;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class DomainNSCollection extends ResourceCollection
{
public $collects = DomainNSResource::class;
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
$data = [
'items' => $this->collection,
];
if (method_exists($this->resource, 'total')) {
$data['pagination'] = [
'total' => $this->resource->total(),
'per_page' => $this->resource->perPage(),
'current_page' => $this->resource->currentPage(),
'last_page' => $this->resource->lastPage(),
];
}
return $data;
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Resources\Api\Service;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class DomainNSResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'dns_manager_domain_id' => $this->dns_manager_domain_id,
'name' => $this->name,
'type' => $this->type,
'type_valid' => $this->type_valid,
'priority' => $this->priority,
'weight' => $this->weight,
'port' => $this->port,
'service_name' => $this->service_name,
'protocol' => $this->protocol,
'value' => $this->value,
'ttl' => isset($this->ttl) ? (int) $this->ttl : null,
'plesk_id' => $this->plesk_id,
'created_at' => optional($this->created_at)->toISOString(),
'updated_at' => optional($this->updated_at)->toISOString(),
];
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Resources\Api\Service;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class DomainResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'domain_name' => $this->domain_name,
'dns_manager_server_id' => $this->dns_manager_server_id,
'server_name' => $this->server?->name,
'server_ip' => $this->server?->ip_address,
'is_created' => isset($this->is_created) ? (int) $this->is_created : null,
'plesk_id' => $this->plesk_id,
'record_count' => $this->whenCounted('record'),
'created_at' => optional($this->created_at)->toISOString(),
'updated_at' => optional($this->updated_at)->toISOString(),
];
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources\Api\Service;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class VpsServiceCollection extends ResourceCollection
{
public $collects = VpsServiceResource::class;
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
$data = [
'items' => $this->collection,
];
if (method_exists($this->resource, 'total')) {
$data['pagination'] = [
'total' => $this->resource->total(),
'per_page' => $this->resource->perPage(),
'current_page' => $this->resource->currentPage(),
'last_page' => $this->resource->lastPage(),
];
}
return $data;
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Http\Resources\Api\Service;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class VpsServiceResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
$orderItem = $this->orderItem;
$product = $orderItem?->product;
return [
'id' => $this->id,
'uuid' => $this->uuid,
'server_name' => $this->server_name,
'status' => $this->status,
'status_label' => $this->status_label,
'auto_renew' => (bool) $this->auto_renew,
'started_at' => $this->started_at?->toISOString(),
'expires_at' => $this->expires_at?->toISOString(),
'os' => [
'version' => $orderItem?->os?->version,
'group' => $orderItem?->os?->osGroup?->name,
],
'product' => $product ? [
'id' => $product->id,
'name' => $product->name,
'slug' => $product->slug,
'product_type' => $product->product_type,
'page_type' => 'service_vps',
] : null,
'specs' => $product ? [
'cpu' => (int) ($product->cpu ?? 0),
'ram' => (int) ($product->ram ?? 0),
'disk' => (int) ($product->disk ?? 0),
'bandwidth' => (int) ($product->bandwidth ?? 0),
'gpu' => $product->gpu,
] : null,
];
}
}
+10
View File
@@ -26,6 +26,7 @@ class Customer extends Authenticatable implements JWTSubject
protected $fillable = [
'fullname',
'username',
'avatar',
'address',
'state',
@@ -35,6 +36,7 @@ class Customer extends Authenticatable implements JWTSubject
'company',
'phone',
'email',
'email_verified_at',
'email_verify',
'email_hash',
'email_marketing',
@@ -43,6 +45,7 @@ class Customer extends Authenticatable implements JWTSubject
'front_identity_number',
'back_identity_number',
'password',
'type',
'credit',
'total',
'total_pending',
@@ -55,6 +58,7 @@ class Customer extends Authenticatable implements JWTSubject
protected $updatable = [
'fullname',
'username',
'phone',
'company',
'address',
@@ -82,6 +86,7 @@ class Customer extends Authenticatable implements JWTSubject
protected $casts = [
'gender' => \App\Enums\Client\ClientGender::class,
'email_verified_at' => 'datetime',
];
/**
@@ -215,6 +220,11 @@ class Customer extends Authenticatable implements JWTSubject
return (string) floor($this->total / 100);
}
public function getNameAttribute()
{
return $this->fullname;
}
public function getCmndBeforeAttribute()
{
return $this->front_identity_number ?? '';
+10
View File
@@ -30,11 +30,21 @@ class DnsDomain extends Model
return $this->belongsToMany(Customer::class, 'dns_domain_user', 'domain_id', 'user_id')->withTimestamps();
}
public function users()
{
return $this->customer();
}
public function server()
{
return $this->belongsTo(DnsServer::class, 'dns_manager_server_id', 'id');
}
public function ns()
{
return $this->hasMany(DnsManagerNS::class, 'dns_manager_domain_id', 'id');
}
public function record()
{
return $this->hasMany(DnsRecord::class, 'dns_manager_domain_id', 'id');
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class DnsManagerNS extends Model
{
use HasFactory;
protected $table = 'dns_manager_ns';
protected $fillable = [
'dns_manager_domain_id',
'name',
'type',
'type_valid',
'priority',
'weight',
'port',
'service_name',
'protocol',
'value',
'ttl',
'plesk_id',
];
public function managerDomain()
{
return $this->belongsTo(DnsDomain::class, 'dns_manager_domain_id', 'id');
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class DnsManagerUser extends Model
{
use HasFactory;
protected $table = 'dns_domain_user';
protected $fillable = [
'domain_id',
'user_id',
];
public function customer()
{
return $this->belongsTo(Customer::class, 'user_id', 'id');
}
public function domain()
{
return $this->belongsTo(DnsDomain::class, 'domain_id', 'id');
}
}
@@ -15,13 +15,21 @@ class ServiceClientRepository extends BaseApiRepository implements ServiceClient
/**
* Lấy toàn bộ dịch vụ của customer (mọi trạng thái)
*/
public function getActiveServices(int $limit = 10)
public function getActiveServices(int $customerId, ?string $productType = null, int $limit = 10)
{
return $this->model
$query = $this->model
->with(['orderItem' => fn($q) => $q->with(['product', 'os.osGroup'])])
->where('customer_id', $customerId)
->orderByRaw("FIELD(status, 'active', 'pending', 'suspended', 'expired', 'cancelled', 'terminated')")
->orderBy('started_at', 'desc')
->paginate($limit);
->orderBy('started_at', 'desc');
if ($productType) {
$query->whereHas('orderItem.product', function ($builder) use ($productType) {
$builder->where('product_type', $productType);
});
}
return $query->paginate($limit);
}
public function findActiveBySlug(string $slug)
@@ -44,7 +52,7 @@ class ServiceClientRepository extends BaseApiRepository implements ServiceClient
return $pricing ? $pricing->price : null;
}
public function getDetailService(string $uuid)
public function getDetailService(string $uuid, int $customerId)
{
return $this->model
->with([
@@ -54,6 +62,7 @@ class ServiceClientRepository extends BaseApiRepository implements ServiceClient
'vmware.ipAddress',
])
->where('uuid', $uuid)
->where('customer_id', $customerId)
->first();
}
}
@@ -9,11 +9,11 @@ interface ServiceClientRepositoryInterface extends BaseRepositoryInterface
/**
* Lấy danh sách sản phẩm đang hoạt động
*/
public function getActiveServices(int $limit = 10);
public function getActiveServices(int $customerId, ?string $productType = null, int $limit = 10);
public function findActiveBySlug(string $slug);
public function getPricing(int $productId, string $billingCycle);
public function getDetailService(string $uuid);
public function getDetailService(string $uuid, int $customerId);
}
@@ -7,6 +7,16 @@ use App\Repositories\BaseRepository;
class CustomerRepository extends BaseRepository implements CustomerRepositoryInterface
{
private const STATUS_MAP = [
'active' => 1,
'inactive' => 2,
'de_active' => 2,
'pending' => 0,
'closed' => 2,
'suspended' => 3,
'banned' => 3,
];
public function getModel()
{
return Customer::class;
@@ -19,6 +29,16 @@ class CustomerRepository extends BaseRepository implements CustomerRepositoryInt
bool $returnQuery = false
) {
$model = $this->model->with($relationships)->select($select);
if (isset($data['filter']) && ! empty($data['filter'])) {
$model = $model->where(function ($query) use ($data) {
$filter = trim($data['filter']);
$query->where('fullname', 'LIKE', "%{$filter}%")
->orWhere('username', 'LIKE', "%{$filter}%")
->orWhere('company', 'LIKE', "%{$filter}%")
->orWhere('email', 'LIKE', "%{$filter}%")
->orWhere('phone', 'LIKE', "%{$filter}%");
});
}
if (isset($data['name']) && ! empty($data['name'])) {
$model = $model->where(function ($query) use ($data) {
$name = trim($data['name']);
@@ -33,11 +53,14 @@ class CustomerRepository extends BaseRepository implements CustomerRepositoryInt
$model = $model->where("phone", "LIKE", "%" . $data['phone'] . "%");
}
if (isset($data['status']) && ! empty($data['status']) && $data['status'] != 'full') {
$model = $model->where("status", match ($data['status']) {
'active' => 1,
'de_active' => 0,
'closed' => 2,
});
$status = (string) $data['status'];
$statusValue = is_numeric($status)
? (int) $status
: (self::STATUS_MAP[$status] ?? null);
if ($statusValue !== null) {
$model = $model->where('status', $statusValue);
}
}
if (isset($data['relations'])) {
foreach ($data['relations'] as $key => $value) {
@@ -35,7 +35,7 @@ class DnsDomainRepository extends BaseRepository implements DnsDomainRepositoryI
if (isset($data['user_id']) && $data['user_id']) {
$model = $model->whereHas('users', function ($query) use ($data) {
$query->where('users.id', $data['user_id']);
$query->where('customers.id', $data['user_id']);
});
}
@@ -16,9 +16,9 @@ class ServiceClientService
/**
* Lấy danh sách dịch vụ đang hoạt động
*/
public function getActiveServices(int $limit = 10)
public function getActiveServices(int $customerId, ?string $productType = null, int $limit = 10)
{
return $this->serviceClientRepository->getActiveServices($limit);
return $this->serviceClientRepository->getActiveServices($customerId, $productType, $limit);
}
public function findActiveBySlug(string $slug)
@@ -26,8 +26,8 @@ class ServiceClientService
return $this->serviceClientRepository->findActiveBySlug($slug);
}
public function getDetailService(string $uuid)
public function getDetailService(string $uuid, int $customerId)
{
return $this->serviceClientRepository->getDetailService($uuid);
return $this->serviceClientRepository->getDetailService($uuid, $customerId);
}
}
@@ -0,0 +1,615 @@
<?php
namespace App\Services\Imports;
use App\Models\Customer;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use RuntimeException;
class AdminZoneDnsSqlImporter
{
private ?int $canonicalServerId = null;
public function import(
string $path,
bool $preview = false,
bool $importCustomers = true,
bool $importDns = true,
): array {
if (! is_file($path)) {
throw new RuntimeException("SQL file not found: {$path}");
}
$this->assertRequiredSchema();
$tables = $this->parseSqlDump($path, [
'users',
'dns_manager_domain',
'dns_domain_user',
'dns_manager_ns',
]);
$summary = [
'source' => [
'users' => count($tables['users']),
'dns_manager_domain' => count($tables['dns_manager_domain']),
'dns_domain_user' => count($tables['dns_domain_user']),
'dns_manager_ns' => count($tables['dns_manager_ns']),
],
'customers' => [
'created' => 0,
'updated' => 0,
'restored' => 0,
'skipped' => 0,
],
'dns_domains' => [
'created' => 0,
'updated' => 0,
'skipped' => 0,
],
'dns_records' => [
'created' => 0,
'updated' => 0,
'skipped' => 0,
],
'dns_links' => [
'created' => 0,
'updated' => 0,
'skipped' => 0,
],
];
if (! $importCustomers && ! $importDns) {
return $summary;
}
$callback = function () use ($tables, &$summary, $preview, $importCustomers, $importDns) {
$customerIdMap = [];
$domainIdMap = [];
if ($importCustomers) {
$customerIdMap = $this->importCustomers($tables['users'], $summary, $preview);
}
if ($importDns) {
$domainIdMap = $this->importDnsDomains($tables['dns_manager_domain'], $summary, $preview);
$this->importDnsRecords($tables['dns_manager_ns'], $domainIdMap, $summary, $preview);
$this->importDnsCustomerLinks($tables['dns_domain_user'], $customerIdMap, $domainIdMap, $summary, $preview);
}
};
if ($preview) {
$callback();
return $summary;
}
DB::transaction($callback);
return $summary;
}
private function assertRequiredSchema(): void
{
foreach (['username', 'email_verified_at', 'type'] as $column) {
if (! Schema::hasColumn('customers', $column)) {
throw new RuntimeException(
"Missing customers.{$column}. Run migrations before importing admin_zonedns data."
);
}
}
}
private function importCustomers(array $rows, array &$summary, bool $preview): array
{
$idMap = [];
foreach ($rows as $row) {
$sourceId = (int) ($row['id'] ?? 0);
if ($sourceId <= 0) {
$summary['customers']['skipped']++;
continue;
}
$match = $this->findCustomerMatch($row);
$payload = $this->buildCustomerPayload($row);
if (! $match) {
$existingById = Customer::withTrashed()->find($sourceId);
if ($preview) {
$summary['customers']['created']++;
$idMap[$sourceId] = $existingById?->id ?? $sourceId;
continue;
}
if (! $existingById) {
$payload['id'] = $sourceId;
}
$payload['uuid'] = (string) Str::uuid();
$customerId = DB::table('customers')->insertGetId($payload);
$summary['customers']['created']++;
$idMap[$sourceId] = $customerId;
continue;
}
$idMap[$sourceId] = $match->id;
if ($preview) {
$summary['customers']['updated']++;
continue;
}
$restored = method_exists($match, 'trashed') && $match->trashed();
if ($restored) {
$match->restore();
$summary['customers']['restored']++;
}
DB::table('customers')
->where('id', $match->id)
->update($payload);
$summary['customers']['updated']++;
}
return $idMap;
}
private function importDnsDomains(array $rows, array &$summary, bool $preview): array
{
$idMap = [];
foreach ($rows as $row) {
$sourceId = (int) ($row['id'] ?? 0);
if ($sourceId <= 0) {
$summary['dns_domains']['skipped']++;
continue;
}
$domainName = trim((string) ($row['domain_name'] ?? ''));
if ($domainName === '') {
$summary['dns_domains']['skipped']++;
continue;
}
$existing = DB::table('dns_manager_domain')->where('id', $sourceId)->first();
if (! $existing) {
$existing = DB::table('dns_manager_domain')->where('domain_name', $domainName)->first();
}
$targetId = $existing?->id ?? $sourceId;
$idMap[$sourceId] = (int) $targetId;
$payload = [
'domain_name' => $domainName,
'dns_manager_server_id' => $this->resolveDnsServerId((int) ($row['dns_manager_server_id'] ?? 0)),
'is_created' => (int) ($row['is_created'] ?? 0),
'plesk_id' => $this->nullableInt($row['plesk_id'] ?? null),
'created_at' => $this->normalizeDateTime($row['created_at'] ?? null) ?? now(),
'updated_at' => $this->normalizeDateTime($row['updated_at'] ?? null) ?? now(),
];
if (Schema::hasColumn('dns_manager_domain', 'user_id')) {
$payload['user_id'] = $existing?->user_id ?? 0;
}
if ($preview) {
$summary['dns_domains'][$existing ? 'updated' : 'created']++;
continue;
}
if ($existing) {
DB::table('dns_manager_domain')->where('id', $existing->id)->update($payload);
$summary['dns_domains']['updated']++;
continue;
}
$payload['id'] = $sourceId;
DB::table('dns_manager_domain')->insert($payload);
$summary['dns_domains']['created']++;
}
return $idMap;
}
private function importDnsRecords(array $rows, array $domainIdMap, array &$summary, bool $preview): void
{
foreach ($rows as $row) {
$sourceId = (int) ($row['id'] ?? 0);
$sourceDomainId = (int) ($row['dns_manager_domain_id'] ?? 0);
$targetDomainId = $domainIdMap[$sourceDomainId] ?? null;
if ($sourceId <= 0 || ! $targetDomainId) {
$summary['dns_records']['skipped']++;
continue;
}
$existing = DB::table('dns_manager_ns')->where('id', $sourceId)->first();
$payload = [
'dns_manager_domain_id' => $targetDomainId,
'name' => (string) ($row['name'] ?? ''),
'type' => (string) ($row['type'] ?? 'A'),
'priority' => $this->nullableString($row['priority'] ?? null, '0'),
'weight' => $this->nullableString($row['weight'] ?? null, '0'),
'port' => $this->nullableString($row['port'] ?? null, '0'),
'service_name' => $this->nullableString($row['service_name'] ?? null),
'protocol' => $this->nullableString($row['protocol'] ?? null),
'value' => $this->nullableString($row['value'] ?? null),
'plesk_id' => $this->nullableInt($row['plesk_id'] ?? null),
'ttl' => (int) ($row['ttl'] ?? 300),
'created_at' => $this->normalizeDateTime($row['created_at'] ?? null) ?? now(),
'updated_at' => $this->normalizeDateTime($row['updated_at'] ?? null) ?? now(),
];
if (Schema::hasColumn('dns_manager_ns', 'type_valid')) {
$payload['type_valid'] = $existing?->type_valid ?? (string) ($row['type'] ?? 'A');
}
if ($preview) {
$summary['dns_records'][$existing ? 'updated' : 'created']++;
continue;
}
if ($existing) {
DB::table('dns_manager_ns')->where('id', $sourceId)->update($payload);
$summary['dns_records']['updated']++;
continue;
}
$payload['id'] = $sourceId;
DB::table('dns_manager_ns')->insert($payload);
$summary['dns_records']['created']++;
}
}
private function importDnsCustomerLinks(
array $rows,
array $customerIdMap,
array $domainIdMap,
array &$summary,
bool $preview,
): void {
foreach ($rows as $row) {
$domainId = $domainIdMap[(int) ($row['domain_id'] ?? 0)] ?? null;
$customerId = $customerIdMap[(int) ($row['user_id'] ?? 0)] ?? null;
if (! $domainId || ! $customerId) {
$summary['dns_links']['skipped']++;
continue;
}
$existing = DB::table('dns_domain_user')
->where('domain_id', $domainId)
->where('user_id', $customerId)
->first();
$payload = [
'domain_id' => $domainId,
'user_id' => $customerId,
'created_at' => $this->normalizeDateTime($row['created_at'] ?? null) ?? now(),
'updated_at' => $this->normalizeDateTime($row['updated_at'] ?? null) ?? now(),
];
if ($preview) {
$summary['dns_links'][$existing ? 'updated' : 'created']++;
continue;
}
if ($existing) {
DB::table('dns_domain_user')
->where('id', $existing->id)
->update(['updated_at' => $payload['updated_at']]);
$summary['dns_links']['updated']++;
continue;
}
DB::table('dns_domain_user')->insert($payload);
$summary['dns_links']['created']++;
}
}
private function findCustomerMatch(array $row): ?Customer
{
$email = $this->nullableString($row['email'] ?? null);
if ($email) {
$match = Customer::withTrashed()->where('email', $email)->first();
if ($match) {
return $match;
}
}
$username = $this->nullableString($row['username'] ?? null);
if ($username && Schema::hasColumn('customers', 'username')) {
return Customer::withTrashed()->where('username', $username)->first();
}
return null;
}
private function buildCustomerPayload(array $row): array
{
$email = $this->nullableString($row['email'] ?? null);
return [
'fullname' => $this->nullableString($row['name'] ?? null) ?? $this->nullableString($row['username'] ?? null) ?? $email,
'username' => $this->nullableString($row['username'] ?? null),
'email' => $email,
'email_verified_at' => $this->normalizeDateTime($row['email_verified_at'] ?? null),
'password' => $this->nullableString($row['password'] ?? null),
'type' => $this->nullableString($row['type'] ?? null, 'guest'),
'remember_token' => $this->nullableString($row['remember_token'] ?? null),
'status' => $this->mapCustomerStatus($row['status'] ?? null),
'created_at' => $this->normalizeDateTime($row['created_at'] ?? null) ?? now(),
'updated_at' => $this->normalizeDateTime($row['updated_at'] ?? null) ?? now(),
];
}
private function mapCustomerStatus(mixed $status): int
{
return match (strtolower(trim((string) $status))) {
'verified', 'active', 'activated', '1' => 1,
'unverified', 'inactive', 'deactivated', '0', '2' => 2,
default => 2,
};
}
private function resolveDnsServerId(int $sourceServerId): int
{
if ($sourceServerId > 0 && DB::table('dns_manager_servers')->where('id', $sourceServerId)->exists()) {
return $sourceServerId;
}
if ($this->canonicalServerId !== null) {
return $this->canonicalServerId;
}
$server = DB::table('dns_manager_servers')
->select('id')
->orderByRaw('CASE WHEN id = 1 THEN 0 ELSE 1 END')
->orderBy('id')
->first();
if (! $server) {
throw new RuntimeException('No dns_manager_servers available to map imported domains.');
}
return $this->canonicalServerId = (int) $server->id;
}
private function parseSqlDump(string $path, array $tables): array
{
$result = [];
foreach ($tables as $table) {
$result[$table] = [];
}
$handle = fopen($path, 'rb');
if (! $handle) {
throw new RuntimeException("Unable to open SQL file: {$path}");
}
$activeTable = null;
$activeColumns = [];
$buffer = '';
$state = ['in_string' => false, 'escaped' => false];
while (($line = fgets($handle)) !== false) {
if ($activeTable === null) {
$trimmed = trim($line);
if (
preg_match('/^INSERT INTO `([^`]+)` \((.+)\) VALUES$/', $trimmed, $matches)
&& in_array($matches[1], $tables, true)
) {
$activeTable = $matches[1];
$activeColumns = $this->parseColumns($matches[2]);
$buffer = '';
$state = ['in_string' => false, 'escaped' => false];
}
continue;
}
$buffer .= $line;
if (! $this->lineEndsStatement($line, $state)) {
continue;
}
$valueBlock = preg_replace('/;\s*$/', '', trim($buffer));
$result[$activeTable] = array_merge(
$result[$activeTable],
$this->parseValueBlock($valueBlock, $activeColumns)
);
$activeTable = null;
$activeColumns = [];
$buffer = '';
$state = ['in_string' => false, 'escaped' => false];
}
fclose($handle);
return $result;
}
private function parseColumns(string $columnSegment): array
{
preg_match_all('/`([^`]+)`/', $columnSegment, $matches);
return $matches[1] ?? [];
}
private function lineEndsStatement(string $line, array &$state): bool
{
$length = strlen($line);
for ($i = 0; $i < $length; $i++) {
$char = $line[$i];
if ($state['in_string']) {
if ($state['escaped']) {
$state['escaped'] = false;
continue;
}
if ($char === '\\') {
$state['escaped'] = true;
continue;
}
if ($char === "'") {
$state['in_string'] = false;
}
continue;
}
if ($char === "'") {
$state['in_string'] = true;
continue;
}
if ($char === ';') {
return true;
}
}
return false;
}
private function parseValueBlock(string $valueBlock, array $columns): array
{
$rows = [];
$fields = [];
$field = '';
$depth = 0;
$inString = false;
$escaped = false;
$length = strlen($valueBlock);
for ($i = 0; $i < $length; $i++) {
$char = $valueBlock[$i];
if ($inString) {
$field .= $char;
if ($escaped) {
$escaped = false;
continue;
}
if ($char === '\\') {
$escaped = true;
continue;
}
if ($char === "'") {
$inString = false;
}
continue;
}
if ($char === "'") {
$inString = true;
$field .= $char;
continue;
}
if ($char === '(') {
if ($depth > 0) {
$field .= $char;
}
$depth++;
continue;
}
if ($char === ')') {
if ($depth > 1) {
$field .= $char;
$depth--;
continue;
}
$fields[] = $this->normalizeSqlValue($field);
$rows[] = array_combine($columns, $fields);
$fields = [];
$field = '';
$depth = 0;
continue;
}
if ($char === ',' && $depth === 1) {
$fields[] = $this->normalizeSqlValue($field);
$field = '';
continue;
}
if ($depth >= 1) {
$field .= $char;
}
}
return $rows;
}
private function normalizeSqlValue(string $value): mixed
{
$value = trim($value);
if (strcasecmp($value, 'NULL') === 0) {
return null;
}
if ($value === '') {
return '';
}
if ($value[0] === "'" && substr($value, -1) === "'") {
return stripcslashes(substr($value, 1, -1));
}
if (is_numeric($value)) {
return str_contains($value, '.') ? (float) $value : (int) $value;
}
return $value;
}
private function normalizeDateTime(mixed $value): ?string
{
$value = $this->nullableString($value);
if (! $value || $value === '0000-00-00 00:00:00') {
return null;
}
return $value;
}
private function nullableString(mixed $value, ?string $default = null): ?string
{
if ($value === null) {
return $default;
}
$value = trim((string) $value);
if ($value === '') {
return $default;
}
return $value;
}
private function nullableInt(mixed $value): ?int
{
if ($value === null || $value === '') {
return null;
}
return (int) $value;
}
}
+14
View File
@@ -165,6 +165,20 @@ class ServerService
}
}
public function checkConnectionByServer(DnsServer $server): bool
{
try {
return $this->checkConnection([
'username' => $server->username,
'password' => Crypt::decryptString($server->password),
'ip_address' => $server->ip_address,
]);
} catch (\Throwable $e) {
Log::error($e->getMessage());
return false;
}
}
public function getDomainInfo(string $domain = ''): array|false
{
if (empty($domain)) {
+197
View File
@@ -0,0 +1,197 @@
# Preview: admin_zonedns -> customers
## Nguon doi chieu
- SQL dump: `portal/admin_zonedns (1).sql`
- DB hien tai tu `portal/.env`: `tgs_portal`
- Model doi chieu: `portal/app/Models/Customer.php`
## Xac nhan mapping
- `tbl.users` trong file SQL duoc map sang bang `customers` cua portal.
- Vi vay, khong tao bang `users` moi trong portal.
- Muc tieu la:
- giu `dns_domain_user`, `dns_manager_domain`, `dns_manager_ns`
- bo sung cac field con thieu cho `customers`
- cap nhat `Customer.php` chi o phan replace/createUpdate
## Ket qua doi chieu cac bang duoc yeu cau
| Bang/nguon | Trong SQL | Trong DB `tgs_portal` | Ket luan |
|---|---:|---:|---|
| `users` | Co | Khong | Khong can tao, vi duoc map vao `customers` |
| `customers` | Khong | Co | La bang dich de nhan schema tu `tbl.users` |
| `dns_domain_users` | Khong | Khong | Dump va DB dang dung `dns_domain_user` |
| `dns_domain_user` | Co | Co | Da ton tai |
| `dns_manager_domain` | Co | Co | Da ton tai |
| `dns_manager_ns` | Co | Co | Da ton tai |
## Lech schema hien tai
### `tbl.users` trong SQL
```text
id
name
username
email
email_verified_at
password
type
status
remember_token
created_at
updated_at
```
### `customers` hien tai
```text
id
uuid
fullname
avatar
gender
address
state
city
postcode
country
company
phone
email
email_verify
email_hash
email_marketing
receive_deposit
password
credit
total
total_pending
identity_number
front_identity_number
back_identity_number
province_code
ward_code
remember_token
status
created_at
updated_at
deleted_at
```
### Field can bo sung vao `customers` tu `tbl.users`
| Field tu SQL `users` | Cach map vao `customers` | Trang thai |
|---|---|---|
| `name` | map vao `fullname` | Da co, khong tao moi |
| `username` | tao cot `username` | Thieu |
| `email` | giu `email` | Da co |
| `email_verified_at` | tao cot `email_verified_at` | Thieu |
| `password` | giu `password` | Da co |
| `type` | tao cot `type` | Thieu |
| `status` | giu `status` | Da co |
| `remember_token` | giu `remember_token` | Da co |
| `created_at` | giu `created_at` | Da co |
| `updated_at` | giu `updated_at` | Da co |
## Lech schema cac bang DNS
### `dns_manager_domain`
- SQL dump:
- `id`, `domain_name`, `dns_manager_server_id`, `is_created`, `plesk_id`, `created_at`, `updated_at`
- DB hien tai:
- co them `user_id`
- Ket luan:
- khong thieu bang
- khong nen remove `user_id`
### `dns_manager_ns`
- SQL dump:
- `id`, `dns_manager_domain_id`, `name`, `type`, `service_name`, `protocol`, `priority`, `weight`, `port`, `value`, `plesk_id`, `ttl`, `created_at`, `updated_at`
- DB hien tai:
- co them `type_valid`
- Ket luan:
- khong thieu bang
- khong nen remove `type_valid`
### `dns_domain_user`
- SQL dump va DB hien tai deu dang dung `dns_domain_user`
- Khong co bang `dns_domain_users`
- Neu ban muon chuyen qua ten plural, can migration rieng
## Preview migration cho `customers`
```php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('customers', function (Blueprint $table) {
if (! Schema::hasColumn('customers', 'username')) {
$table->string('username', 50)->nullable()->after('fullname');
}
if (! Schema::hasColumn('customers', 'email_verified_at')) {
$table->timestamp('email_verified_at')->nullable()->after('email');
}
if (! Schema::hasColumn('customers', 'type')) {
$table->string('type')->default('guest')->after('password');
}
});
}
public function down(): void
{
Schema::table('customers', function (Blueprint $table) {
if (Schema::hasColumn('customers', 'type')) {
$table->dropColumn('type');
}
if (Schema::hasColumn('customers', 'email_verified_at')) {
$table->dropColumn('email_verified_at');
}
if (Schema::hasColumn('customers', 'username')) {
$table->dropColumn('username');
}
});
}
};
```
## Preview cap nhat `Customer.php`
### Replace
- Dung `fullname` thay cho `users.name`
- Khong tao them cot `name`
### Them vao `$fillable`
```php
'username',
'email_verified_at',
'type',
```
### Them vao `$updatable`
```php
'username',
```
## Tom tat sau khi duyet preview
1. Khong tao bang `users`.
2. Tao 1 migration add field vao `customers`: `username`, `email_verified_at`, `type`.
3. Khong tao moi `dns_domain_user`, `dns_manager_domain`, `dns_manager_ns`.
4. Chi cap nhat `Customer.php` o phan fillable/updatable va replace mapping `name` -> `fullname`.
@@ -0,0 +1,67 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$hasUsername = Schema::hasColumn('customers', 'username');
$hasEmailVerifiedAt = Schema::hasColumn('customers', 'email_verified_at');
$hasType = Schema::hasColumn('customers', 'type');
if (! $hasUsername || ! $hasEmailVerifiedAt || ! $hasType) {
Schema::table('customers', function (Blueprint $table) use ($hasUsername, $hasEmailVerifiedAt, $hasType) {
if (! $hasUsername) {
$table->string('username', 50)->nullable()->after('fullname');
$table->unique('username');
}
if (! $hasEmailVerifiedAt) {
$table->timestamp('email_verified_at')->nullable()->after('email');
}
if (! $hasType) {
$table->string('type', 255)->default('guest')->after('password');
}
});
}
}
public function down(): void
{
$hasUsername = Schema::hasColumn('customers', 'username');
$hasEmailVerifiedAt = Schema::hasColumn('customers', 'email_verified_at');
$hasType = Schema::hasColumn('customers', 'type');
if (! $hasUsername && ! $hasEmailVerifiedAt && ! $hasType) {
return;
}
Schema::table('customers', function (Blueprint $table) use ($hasUsername, $hasEmailVerifiedAt, $hasType) {
if ($hasUsername) {
$table->dropUnique('customers_username_unique');
}
$columns = [];
if ($hasUsername) {
$columns[] = 'username';
}
if ($hasEmailVerifiedAt) {
$columns[] = 'email_verified_at';
}
if ($hasType) {
$columns[] = 'type';
}
if ($columns !== []) {
$table->dropColumn($columns);
}
});
}
};
@@ -0,0 +1,116 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
$serverId = $this->resolveCanonicalServerId();
if ($serverId === null) {
throw new RuntimeException('Không tìm thấy dns_manager_servers để sync foreign key.');
}
DB::statement("
UPDATE dns_manager_domain d
LEFT JOIN dns_manager_servers s ON s.id = d.dns_manager_server_id
SET d.dns_manager_server_id = {$serverId}
WHERE d.dns_manager_server_id = 0 OR s.id IS NULL
");
DB::statement('ALTER TABLE dns_manager_domain MODIFY dns_manager_server_id BIGINT UNSIGNED NOT NULL');
DB::statement('ALTER TABLE dns_manager_ns MODIFY dns_manager_domain_id BIGINT UNSIGNED NOT NULL');
$this->addForeignKeyIfMissing(
'dns_manager_domain',
'dns_manager_domain_dns_manager_server_id_foreign',
'ALTER TABLE dns_manager_domain
ADD CONSTRAINT dns_manager_domain_dns_manager_server_id_foreign
FOREIGN KEY (dns_manager_server_id) REFERENCES dns_manager_servers(id)
ON UPDATE CASCADE
ON DELETE RESTRICT'
);
$this->addForeignKeyIfMissing(
'dns_domain_user',
'dns_domain_user_domain_id_foreign',
'ALTER TABLE dns_domain_user
ADD CONSTRAINT dns_domain_user_domain_id_foreign
FOREIGN KEY (domain_id) REFERENCES dns_manager_domain(id)
ON UPDATE CASCADE
ON DELETE CASCADE'
);
$this->addForeignKeyIfMissing(
'dns_domain_user',
'dns_domain_user_user_id_foreign',
'ALTER TABLE dns_domain_user
ADD CONSTRAINT dns_domain_user_user_id_foreign
FOREIGN KEY (user_id) REFERENCES customers(id)
ON UPDATE CASCADE
ON DELETE CASCADE'
);
$this->addForeignKeyIfMissing(
'dns_manager_ns',
'dns_manager_ns_dns_manager_domain_id_foreign',
'ALTER TABLE dns_manager_ns
ADD CONSTRAINT dns_manager_ns_dns_manager_domain_id_foreign
FOREIGN KEY (dns_manager_domain_id) REFERENCES dns_manager_domain(id)
ON UPDATE CASCADE
ON DELETE CASCADE'
);
}
public function down(): void
{
$this->dropForeignKeyIfExists('dns_manager_ns', 'dns_manager_ns_dns_manager_domain_id_foreign');
$this->dropForeignKeyIfExists('dns_domain_user', 'dns_domain_user_user_id_foreign');
$this->dropForeignKeyIfExists('dns_domain_user', 'dns_domain_user_domain_id_foreign');
$this->dropForeignKeyIfExists('dns_manager_domain', 'dns_manager_domain_dns_manager_server_id_foreign');
DB::statement('ALTER TABLE dns_manager_ns MODIFY dns_manager_domain_id INT NOT NULL');
DB::statement('ALTER TABLE dns_manager_domain MODIFY dns_manager_server_id INT NOT NULL DEFAULT 0');
}
private function resolveCanonicalServerId(): ?int
{
$server = DB::table('dns_manager_servers')
->select('id')
->orderByRaw('CASE WHEN id = 1 THEN 0 ELSE 1 END')
->orderBy('id')
->first();
return $server ? (int) $server->id : null;
}
private function addForeignKeyIfMissing(string $table, string $constraint, string $statement): void
{
$exists = DB::table('information_schema.TABLE_CONSTRAINTS')
->whereRaw('TABLE_SCHEMA = DATABASE()')
->where('TABLE_NAME', $table)
->where('CONSTRAINT_NAME', $constraint)
->where('CONSTRAINT_TYPE', 'FOREIGN KEY')
->exists();
if (! $exists) {
DB::statement($statement);
}
}
private function dropForeignKeyIfExists(string $table, string $constraint): void
{
$exists = DB::table('information_schema.TABLE_CONSTRAINTS')
->whereRaw('TABLE_SCHEMA = DATABASE()')
->where('TABLE_NAME', $table)
->where('CONSTRAINT_NAME', $constraint)
->where('CONSTRAINT_TYPE', 'FOREIGN KEY')
->exists();
if ($exists) {
DB::statement("ALTER TABLE {$table} DROP FOREIGN KEY {$constraint}");
}
}
};
+10
View File
@@ -19,6 +19,7 @@ use App\Http\Controllers\Api\Admin\VPS\VPSController;
use App\Http\Controllers\Api\Admin\VCenter\VCenterController;
use App\Http\Controllers\Api\Admin\DnsManager\ServerController as DnsServerController;
use App\Http\Controllers\Api\Admin\DnsManager\DomainController as DnsDomainController;
use App\Http\Controllers\Api\Admin\DnsManager\RecordController as DnsRecordController;
use App\Http\Controllers\Api\Admin\OsManager\OsGroupController;
use App\Http\Controllers\Api\Admin\OsManager\OsController;
use App\Http\Controllers\Api\Admin\IpPool\IpPoolController;
@@ -344,10 +345,19 @@ Route::prefix('v1')
Route::get('/', 'index')->middleware('perm:domain.manager');
Route::post('/', 'store')->middleware('perm:domain.manager');
Route::get('{id}', 'show')->middleware('perm:domain.manager');
Route::post('{id}/check-connection', 'checkConnection')->middleware('perm:domain.manager');
Route::put('{id}', 'update')->middleware('perm:domain.manager');
Route::delete('{id}', 'destroy')->middleware('perm:domain.manager');
});
Route::prefix('dns-records')
->controller(DnsRecordController::class)
->group(function () {
Route::get('/', 'index')->middleware('perm:domain.manager');
Route::get('{id}', 'show')->middleware('perm:domain.manager');
Route::put('{id}', 'update')->middleware('perm:domain.manager');
});
// InfraManagement (jumphost, vcenter config)
Route::prefix('infra-management')
->controller(InfraManagementController::class)
+17 -1
View File
@@ -2,6 +2,7 @@
use App\Http\Controllers\Api\Setting\SettingController;
use App\Http\Controllers\Api\Client\ClientController;
use App\Http\Controllers\Api\Client\Service\DomainController as ClientDomainController;
use App\Http\Controllers\Api\Client\Service\ServiceController as ClientServiceController;
use App\Http\Controllers\Api\Service\OrderController;
use App\Http\Controllers\Api\Service\ProductCategoryController;
@@ -55,12 +56,27 @@ Route::prefix('v1')
Route::post('dich-vu/{id}/renew', 'renewService')->name('services.renew');
});
Route::middleware(['api.auth'])
->prefix('client-service/domains')
->controller(ClientDomainController::class)
->as('client-service.domains.')
->group(function () {
Route::get('/', 'index')->name('list');
Route::get('{domainId}', 'show')->name('detail');
Route::get('{domainId}/records', 'show')->name('records');
Route::post('{domainId}/records', 'storeRecord')->name('records.store');
Route::put('{domainId}/records/{recordId}', 'updateRecord')->name('records.update');
Route::delete('{domainId}/records/{recordId}', 'destroyRecord')->name('records.destroy');
});
Route::middleware(['api.auth'])
->prefix('client-service')
->controller(ClientServiceController::class)
->as('client-service.')
->group(function () {
Route::get('/', 'getService')->name('list');
Route::get('vps', 'indexVps')->name('vps.list');
Route::get('hosting', 'indexHosting')->name('hosting.list');
Route::get('web-design', 'indexWebDesign')->name('web-design.list');
Route::get('{uuid}', 'getDetail')->name('detail');
Route::get('{uuid}/console-ticket', 'getConsoleTicket')->name('console-ticket');
Route::get('{uuid}/available-os', 'getAvailableOs')->name('available-os');