update
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class DnsSyncCompleted implements ShouldBroadcastNow
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public readonly int $synced,
|
||||
public readonly array $failedDomains,
|
||||
) {}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [new Channel('admin-notifications')];
|
||||
}
|
||||
|
||||
public function broadcastAs(): string
|
||||
{
|
||||
return 'dns.sync.completed';
|
||||
}
|
||||
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'synced' => $this->synced,
|
||||
'failed_domains' => $this->failedDomains,
|
||||
'message' => "Đã đồng bộ {$this->synced} bản ghi DNS từ Plesk",
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,32 @@
|
||||
namespace App\Http\Controllers\Api\Admin\DnsManager;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\DnsManager\SyncDnsFromPlesk;
|
||||
use App\Repositories\DnsManager\Domain\DnsDomainRepositoryInterface;
|
||||
use App\Repositories\DnsManager\Record\DnsRecordRepositoryInterface;
|
||||
use App\Services\ServerService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DomainController extends Controller
|
||||
{
|
||||
private const RECORD_RULES = [
|
||||
'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',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly DnsDomainRepositoryInterface $domainRepository,
|
||||
private readonly DnsRecordRepositoryInterface $recordRepository,
|
||||
private readonly ServerService $serverService,
|
||||
) {}
|
||||
|
||||
// ─── Domains ─────────────────────────────────────────────────────────────
|
||||
@@ -192,6 +207,18 @@ class DomainController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Sync from Plesk ─────────────────────────────────────────────────────
|
||||
|
||||
public function syncDns(): JsonResponse
|
||||
{
|
||||
dispatch(new SyncDnsFromPlesk());
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã đưa lệnh đồng bộ DNS vào hàng đợi, quá trình sẽ chạy nền.',
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── DNS Records ─────────────────────────────────────────────────────────
|
||||
|
||||
public function dnsIndex(int $domainId): JsonResponse
|
||||
@@ -218,21 +245,33 @@ class DomainController extends Controller
|
||||
return response()->json(['error' => true, 'message' => "Không tìm thấy domain #$domainId"], 404);
|
||||
}
|
||||
|
||||
$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',
|
||||
]);
|
||||
$validated = $request->validate(self::RECORD_RULES);
|
||||
|
||||
if (!$this->serverService->addServerById($domain->dns_manager_server_id)) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy server DNS'], 404);
|
||||
}
|
||||
|
||||
$result = $this->serverService
|
||||
->handlePayload(['domain' => $domain->domain_name, ...$validated])
|
||||
->addRecord();
|
||||
|
||||
if ($result['error']) {
|
||||
return response()->json(['error' => true, 'message' => $result['message'] ?? 'Tạo bản ghi DNS trên Plesk thất bại'], 422);
|
||||
}
|
||||
|
||||
$record = $this->recordRepository->create([
|
||||
'dns_manager_domain_id' => $domainId,
|
||||
'type' => $request->type,
|
||||
'name' => $request->name,
|
||||
'value' => $request->value,
|
||||
'ttl' => $request->ttl ?? 300,
|
||||
'priority' => $request->priority,
|
||||
'type' => $validated['type'],
|
||||
'type_valid' => $validated['type'],
|
||||
'name' => $validated['name'],
|
||||
'value' => $validated['value'],
|
||||
'ttl' => $validated['ttl'] ?? 300,
|
||||
'priority' => $validated['priority'] ?? null,
|
||||
'weight' => $validated['weight'] ?? null,
|
||||
'port' => $validated['port'] ?? null,
|
||||
'service_name' => $validated['service_name'] ?? null,
|
||||
'protocol' => $validated['protocol'] ?? null,
|
||||
'plesk_id' => $result['data']['id'] ?? null,
|
||||
]);
|
||||
|
||||
return response()->json(['error' => false, 'data' => $record], 201);
|
||||
@@ -240,6 +279,12 @@ class DomainController extends Controller
|
||||
|
||||
public function dnsUpdate(int $domainId, int $recordId, Request $request): JsonResponse
|
||||
{
|
||||
$domain = $this->domainRepository->find($domainId);
|
||||
|
||||
if (!$domain) {
|
||||
return response()->json(['error' => true, 'message' => "Không tìm thấy domain #$domainId"], 404);
|
||||
}
|
||||
|
||||
$record = $this->recordRepository->model
|
||||
->where('dns_manager_domain_id', $domainId)
|
||||
->find($recordId);
|
||||
@@ -248,21 +293,47 @@ class DomainController extends Controller
|
||||
return response()->json(['error' => true, 'message' => "Không tìm thấy bản ghi DNS #$recordId"], 404);
|
||||
}
|
||||
|
||||
$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',
|
||||
]);
|
||||
$validated = $request->validate(self::RECORD_RULES);
|
||||
|
||||
$this->recordRepository->update($recordId, $request->only(['type', 'name', 'value', 'ttl', 'priority']));
|
||||
// ponytail: records created before Plesk sync was wired up have no plesk_id, update locally only
|
||||
if ($record->plesk_id) {
|
||||
if (!$this->serverService->addServerById($domain->dns_manager_server_id)) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy server DNS'], 404);
|
||||
}
|
||||
|
||||
$result = $this->serverService
|
||||
->handlePayload(['domain' => $domain->domain_name, ...$validated])
|
||||
->updateRecord($record->plesk_id);
|
||||
|
||||
if ($result['error']) {
|
||||
return response()->json(['error' => true, 'message' => $result['message'] ?? 'Cập nhật bản ghi DNS trên Plesk thất bại'], 422);
|
||||
}
|
||||
}
|
||||
|
||||
$this->recordRepository->update($recordId, [
|
||||
'type' => $validated['type'],
|
||||
'type_valid' => $validated['type'],
|
||||
'name' => $validated['name'],
|
||||
'value' => $validated['value'],
|
||||
'ttl' => $validated['ttl'] ?? 300,
|
||||
'priority' => $validated['priority'] ?? null,
|
||||
'weight' => $validated['weight'] ?? null,
|
||||
'port' => $validated['port'] ?? null,
|
||||
'service_name' => $validated['service_name'] ?? null,
|
||||
'protocol' => $validated['protocol'] ?? null,
|
||||
]);
|
||||
|
||||
return response()->json(['error' => false, 'data' => $this->recordRepository->find($recordId)]);
|
||||
}
|
||||
|
||||
public function dnsDestroy(int $domainId, int $recordId): JsonResponse
|
||||
{
|
||||
$domain = $this->domainRepository->find($domainId);
|
||||
|
||||
if (!$domain) {
|
||||
return response()->json(['error' => true, 'message' => "Không tìm thấy domain #$domainId"], 404);
|
||||
}
|
||||
|
||||
$record = $this->recordRepository->model
|
||||
->where('dns_manager_domain_id', $domainId)
|
||||
->find($recordId);
|
||||
@@ -271,6 +342,18 @@ class DomainController extends Controller
|
||||
return response()->json(['error' => true, 'message' => "Không tìm thấy bản ghi DNS #$recordId"], 404);
|
||||
}
|
||||
|
||||
if ($record->plesk_id) {
|
||||
if (!$this->serverService->addServerById($domain->dns_manager_server_id)) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy server DNS'], 404);
|
||||
}
|
||||
|
||||
$result = $this->serverService->deleteRecord($record->plesk_id);
|
||||
|
||||
if ($result['error']) {
|
||||
return response()->json(['error' => true, 'message' => $result['message'] ?? 'Xoá bản ghi DNS trên Plesk thất bại'], 422);
|
||||
}
|
||||
}
|
||||
|
||||
$this->recordRepository->delete($recordId);
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Đã xoá bản ghi DNS']);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Admin\DnsManager;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Repositories\DnsManager\Record\DnsRecordRepositoryInterface;
|
||||
use App\Services\ServerService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -11,6 +12,7 @@ class RecordController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DnsRecordRepositoryInterface $recordRepository,
|
||||
private readonly ServerService $serverService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
@@ -106,6 +108,23 @@ class RecordController extends Controller
|
||||
'protocol' => $validated['protocol'] ?? null,
|
||||
];
|
||||
|
||||
// ponytail: records created before Plesk sync was wired up have no plesk_id, update locally only
|
||||
if ($record->plesk_id) {
|
||||
$domain = $record->domain;
|
||||
|
||||
if (!$domain || !$this->serverService->addServerById($domain->dns_manager_server_id)) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy server DNS'], 404);
|
||||
}
|
||||
|
||||
$result = $this->serverService
|
||||
->handlePayload(['domain' => $domain->domain_name, ...$validated])
|
||||
->updateRecord($record->plesk_id);
|
||||
|
||||
if ($result['error']) {
|
||||
return response()->json(['error' => true, 'message' => $result['message'] ?? 'Cập nhật bản ghi DNS trên Plesk thất bại'], 422);
|
||||
}
|
||||
}
|
||||
|
||||
$this->recordRepository->update($id, $payload);
|
||||
|
||||
return $this->show($id);
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
@@ -82,7 +83,7 @@ class ServerController extends Controller
|
||||
'name' => $validated['name'],
|
||||
'ip_address' => $validated['ip_address'],
|
||||
'username' => $validated['username'],
|
||||
'password' => encrypt($validated['password']),
|
||||
'password' => Crypt::encryptString($validated['password']),
|
||||
'api_key' => $validated['api_key'] ?? null,
|
||||
'status' => $validated['status'] ?? 'activated',
|
||||
]);
|
||||
@@ -125,7 +126,7 @@ class ServerController extends Controller
|
||||
->toArray();
|
||||
|
||||
if ($request->filled('password')) {
|
||||
$payload['password'] = encrypt((string) $request->password);
|
||||
$payload['password'] = Crypt::encryptString((string) $request->password);
|
||||
}
|
||||
|
||||
$this->serverRepository->update($id, $payload);
|
||||
@@ -161,12 +162,12 @@ class ServerController extends Controller
|
||||
return response()->json(['error' => true, 'message' => "Không tìm thấy server #$id"], 404);
|
||||
}
|
||||
|
||||
$connected = $this->serverService->checkConnectionByServer($server);
|
||||
$meta = $this->serverService->checkConnectionByServer($server);
|
||||
|
||||
return response()->json([
|
||||
'error' => !$connected,
|
||||
'data' => ['connected' => $connected],
|
||||
'message' => $connected
|
||||
'error' => $meta === false,
|
||||
'data' => ['connected' => $meta !== false, 'server' => $meta ?: null],
|
||||
'message' => $meta !== false
|
||||
? '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.',
|
||||
]);
|
||||
|
||||
@@ -72,6 +72,42 @@ class MediaController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(string $id): JsonResponse
|
||||
{
|
||||
$path = $this->resolvePathById($id);
|
||||
|
||||
if ($path === null) {
|
||||
return response()->json(['error' => true, 'message' => 'File không tồn tại.'], 404);
|
||||
}
|
||||
|
||||
Storage::disk('local')->delete($path);
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Xoá file thành công.']);
|
||||
}
|
||||
|
||||
public function bulkDestroy(Request $request): JsonResponse
|
||||
{
|
||||
$ids = $request->validate(['ids' => ['required', 'array'], 'ids.*' => ['string']])['ids'];
|
||||
|
||||
$paths = collect($ids)
|
||||
->map(fn (string $id) => $this->resolvePathById($id))
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
Storage::disk('local')->delete($paths->all());
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => "Đã xoá {$paths->count()} file.",
|
||||
]);
|
||||
}
|
||||
|
||||
private function resolvePathById(string $id): ?string
|
||||
{
|
||||
return collect(Storage::disk('local')->allFiles())
|
||||
->first(fn (string $path) => sha1($path) === $id);
|
||||
}
|
||||
|
||||
private function formatFile(string $path): array
|
||||
{
|
||||
$disk = Storage::disk('local');
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\DnsManager;
|
||||
|
||||
use App\Events\DnsSyncCompleted;
|
||||
use App\Repositories\DnsManager\Domain\DnsDomainRepositoryInterface;
|
||||
use App\Repositories\DnsManager\Record\DnsRecordRepositoryInterface;
|
||||
use App\Services\ServerService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SyncDnsFromPlesk implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
private const PLESK_RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'TXT', 'MX', 'NS', 'SRV'];
|
||||
|
||||
public function handle(
|
||||
DnsDomainRepositoryInterface $domainRepository,
|
||||
DnsRecordRepositoryInterface $recordRepository,
|
||||
ServerService $serverService,
|
||||
): void {
|
||||
$domains = $domainRepository->model
|
||||
->whereNotNull('dns_manager_server_id')
|
||||
->get();
|
||||
|
||||
$synced = 0;
|
||||
$failedDomains = [];
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
if (!$serverService->addServerById($domain->dns_manager_server_id)) {
|
||||
$failedDomains[] = $domain->domain_name;
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $serverService->listRecords($domain->domain_name);
|
||||
|
||||
if ($result['error']) {
|
||||
$failedDomains[] = $domain->domain_name;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($result['data'] as $item) {
|
||||
$type = strtoupper($item['type'] ?? '');
|
||||
if (!in_array($type, self::PLESK_RECORD_TYPES, true) || empty($item['id'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$recordRepository->model->updateOrCreate(
|
||||
[
|
||||
'dns_manager_domain_id' => $domain->id,
|
||||
'plesk_id' => $item['id'],
|
||||
],
|
||||
$this->mapPleskRecord($item, $type, $domain->domain_name)
|
||||
);
|
||||
|
||||
$synced++;
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('DNS sync from Plesk finished', ['synced' => $synced, 'failed_domains' => $failedDomains]);
|
||||
|
||||
event(new DnsSyncCompleted($synced, $failedDomains));
|
||||
}
|
||||
|
||||
private function mapPleskRecord(array $item, string $type, string $domainName): array
|
||||
{
|
||||
$host = rtrim($item['host'] ?? '', '.');
|
||||
$domainName = rtrim($domainName, '.');
|
||||
$name = $host === $domainName
|
||||
? '@'
|
||||
: preg_replace('/\.' . preg_quote($domainName, '/') . '$/', '', $host);
|
||||
|
||||
$opt = trim((string) ($item['opt'] ?? ''));
|
||||
$priority = null;
|
||||
$weight = null;
|
||||
$port = null;
|
||||
$serviceName = null;
|
||||
$protocol = null;
|
||||
|
||||
if ($type === 'SRV') {
|
||||
[$priority, $weight, $port] = array_pad(preg_split('/\s+/', $opt), 3, null);
|
||||
if (preg_match('/^_([^.]+)\._([^.]+)\./', $name, $m)) {
|
||||
$serviceName = '_' . $m[1];
|
||||
$protocol = '_' . $m[2];
|
||||
}
|
||||
} elseif ($type === 'MX') {
|
||||
$priority = is_numeric($opt) ? $opt : null;
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $type,
|
||||
'type_valid' => $type,
|
||||
'name' => $name,
|
||||
'value' => $item['value'] ?? '',
|
||||
'ttl' => $item['ttl'] ?? 300,
|
||||
'priority' => $priority,
|
||||
'weight' => $weight,
|
||||
'port' => $port,
|
||||
'service_name' => $serviceName,
|
||||
'protocol' => $protocol,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,30 @@ class ServerService
|
||||
}
|
||||
}
|
||||
|
||||
public function listRecords(string $domain): array
|
||||
{
|
||||
if (empty($domain)) {
|
||||
return ['error' => true, 'message' => 'Invalid domain'];
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withoutVerifying()
|
||||
->withBasicAuth($this->server->username, Crypt::decryptString($this->server->password))
|
||||
->timeout(15)
|
||||
->get(
|
||||
BaseHelper::url($this->server->ip_address) . $this->entry_points['record'],
|
||||
['domain' => $domain]
|
||||
)->throw();
|
||||
|
||||
$this->resetServer();
|
||||
|
||||
return ['error' => false, 'data' => $response->json() ?? []];
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
return ['error' => true, 'message' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public function updateRecord(int $record_id = 0): array
|
||||
{
|
||||
if (empty($record_id)) {
|
||||
@@ -146,7 +170,7 @@ class ServerService
|
||||
}
|
||||
}
|
||||
|
||||
public function checkConnection(array $data = []): bool
|
||||
public function checkConnection(array $data = []): array|false
|
||||
{
|
||||
if (empty($data['username']) || empty($data['password']) || empty($data['ip_address'])) {
|
||||
return false;
|
||||
@@ -158,14 +182,14 @@ class ServerService
|
||||
->timeout(10)
|
||||
->get(BaseHelper::url($data['ip_address']) . $this->entry_points['server']);
|
||||
|
||||
return $response->successful();
|
||||
return $response->successful() ? $response->json() : false;
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function checkConnectionByServer(DnsServer $server): bool
|
||||
public function checkConnectionByServer(DnsServer $server): array|false
|
||||
{
|
||||
try {
|
||||
return $this->checkConnection([
|
||||
|
||||
@@ -389,6 +389,7 @@ Route::prefix('v1')
|
||||
->group(function () {
|
||||
Route::get('/', 'index')->middleware('perm:domain.manager');
|
||||
Route::post('/', 'store')->middleware('perm:domain.manager');
|
||||
Route::post('sync-dns', 'syncDns')->middleware('perm:domain.manager');
|
||||
Route::get('{id}', 'show')->middleware('perm:domain.manager');
|
||||
Route::put('{id}', 'update')->middleware('perm:domain.manager');
|
||||
Route::delete('{id}', 'destroy')->middleware('perm:domain.manager');
|
||||
@@ -476,6 +477,8 @@ Route::prefix('v1')
|
||||
->controller(AdminMediaController::class)
|
||||
->group(function () {
|
||||
Route::get('/', 'index');
|
||||
Route::post('bulk-destroy', 'bulkDestroy');
|
||||
Route::delete('{id}', 'destroy');
|
||||
});
|
||||
|
||||
Route::post('media/upload', [AdminSettingController::class, 'uploadMedia']);
|
||||
|
||||
Reference in New Issue
Block a user