- Lịch sử thanh toán hoá đơn trả thêm `method` (tự động gia hạn / số dư / admin / chuyển khoản / hoàn tiền), bỏ đuôi `(method)` khỏi ghi chú - `DnsServer::panelUrl()` = https://<domain>/ dùng cho email cấp hosting và nút Plesk của khách - Order item trả thêm `uuid`/`type` của dịch vụ để FE dẫn tới trang quản lý - Yêu cầu huỷ dịch vụ, chứng chỉ SSL, lịch sử gia hạn, đổi mật khẩu hosting cho khách Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
85 lines
3.2 KiB
PHP
85 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Admin\Service;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Service;
|
|
use App\Models\ServiceSslCertificate;
|
|
use App\Support\ServiceGroup;
|
|
use App\Support\SslBundleExtractor;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use InvalidArgumentException;
|
|
|
|
class SslCertificateController extends Controller
|
|
{
|
|
public function show(int $id): JsonResponse
|
|
{
|
|
return $this->respond($this->sslService($id)->sslCertificate?->data);
|
|
}
|
|
|
|
public function update(Request $request, int $id): JsonResponse
|
|
{
|
|
$service = $this->sslService($id);
|
|
$max = ServiceSslCertificate::MAX_ENTRIES;
|
|
|
|
$data = $request->validate([
|
|
'fields' => "present|array|max:{$max}",
|
|
'fields.*.label' => 'required|string|max:100',
|
|
'fields.*.value' => 'nullable|string|max:5000',
|
|
'files' => "present|array|max:{$max}",
|
|
'files.*.name' => 'required|string|max:150',
|
|
'files.*.content' => 'required|string|max:' . ServiceSslCertificate::MAX_FILE_BYTES,
|
|
]);
|
|
|
|
return $this->respond($this->store($service, [
|
|
'fields' => array_map(fn ($field) => ['label' => $field['label'], 'value' => (string) ($field['value'] ?? '')], $data['fields']),
|
|
'files' => array_map(fn ($file) => ['name' => basename($file['name']), 'content' => $file['content']], $data['files']),
|
|
]));
|
|
}
|
|
|
|
public function uploadArchive(Request $request, int $id): JsonResponse
|
|
{
|
|
$service = $this->sslService($id);
|
|
$request->validate(['archive' => 'required|file|mimes:zip|max:10240']);
|
|
|
|
try {
|
|
$extracted = SslBundleExtractor::extract($request->file('archive'));
|
|
} catch (InvalidArgumentException $exception) {
|
|
return response()->json(['error' => true, 'message' => $exception->getMessage()], 422);
|
|
}
|
|
|
|
$current = $service->sslCertificate?->data ?? ServiceSslCertificate::emptyData();
|
|
$byName = collect($current['files'])->keyBy('name');
|
|
foreach ($extracted as $file) {
|
|
$byName[$file['name']] = $file;
|
|
}
|
|
|
|
if ($byName->count() > ServiceSslCertificate::MAX_ENTRIES) {
|
|
return response()->json(['error' => true, 'message' => 'Quá nhiều tệp chứng chỉ, hãy xoá bớt trước khi tải thêm.'], 422);
|
|
}
|
|
|
|
return $this->respond($this->store($service, ['fields' => $current['fields'], 'files' => $byName->values()->all()]));
|
|
}
|
|
|
|
private function sslService(int $id): Service
|
|
{
|
|
$service = Service::with(['product:id,name,product_type', 'sslCertificate'])->findOrFail($id);
|
|
abort_unless(ServiceGroup::of($service->product?->product_type ?? $service->type, $service->product?->name) === ServiceGroup::SSL, 422, 'Dịch vụ này không phải SSL.');
|
|
|
|
return $service;
|
|
}
|
|
|
|
private function store(Service $service, array $data): array
|
|
{
|
|
ServiceSslCertificate::updateOrCreate(['service_id' => $service->id], ['data' => $data]);
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function respond(?array $data): JsonResponse
|
|
{
|
|
return response()->json(['error' => false, 'data' => $data ?? ServiceSslCertificate::emptyData()]);
|
|
}
|
|
}
|