Files
baodev0andClaude Sonnet 5.5 55a834a2f8 feat: nhắc gia hạn QR theo dịch vụ, failover server hosting, CCCD admin, log cấp phát dịch vụ
- Mail campaign: mẫu nhắc gia hạn thanh toán QR, chọn dịch vụ + chu kỳ từng dòng (renewal_services)
- Hosting: nhiều server Plesk cho một gói (service_plan_servers), chuyển server khi server chết
- Admin xem/sửa giấy tờ định danh khách, tuỳ chọn gia hạn cho khách, nhật ký cấp phát dịch vụ
- Đánh dấu mẫu email dịch vụ là non-marketing

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 18:36:41 +07:00

373 lines
22 KiB
PHP

<?php
use App\Support\ServiceGroup;
use App\Http\Controllers\Api\Setting\SettingController;
use App\Http\Controllers\Api\Client\ClientController;
use App\Http\Controllers\Api\Client\Domain\DomainOrderController as ClientDomainOrderController;
use App\Http\Controllers\Api\Client\Domain\DomainPricingController;
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\Client\Service\SslArchiveController;
use App\Http\Controllers\Api\Client\Service\ServiceRenewalHistoryController;
use App\Http\Controllers\Api\Client\Service\ServiceRenewalOptionsController;
use App\Http\Controllers\Api\Client\Service\HostingPasswordController;
use App\Http\Controllers\Api\Client\Service\ServiceCancellationController;
use App\Http\Controllers\Api\Service\CouponPreviewController;
use App\Http\Controllers\Api\Service\OrderControllerRefactor;
use App\Http\Controllers\Api\Service\ProductCategoryController;
use App\Http\Controllers\Api\Service\ServiceController;
use App\Http\Controllers\Api\Service\SnapshotController;
use App\Http\Controllers\Api\Service\VmController;
use App\Http\Controllers\Api\Service\VmRebuildController;
use App\Http\Controllers\Api\Admin\Domain\InetDomainController;
use App\Http\Controllers\Api\Client\AdministrativeDivisionController;
use App\Http\Controllers\Api\Client\BillingProfileController;
use App\Http\Controllers\Api\Client\InvoiceLookupController;
use App\Http\Controllers\Api\Client\BankController as ClientBankController;
use App\Http\Controllers\Api\Client\Docs\PostCategoryController as ClientPostCategoryController;
use App\Http\Controllers\Api\Client\Docs\PostController as ClientPostController;
use App\Http\Controllers\Api\Client\Ticket\TicketController as ClientTicketController;
use App\Http\Controllers\Api\NotificationController;
use App\Http\Controllers\Api\Transaction\ManualDepositController;
use App\Http\Controllers\Api\Transaction\TransactionController;
use Illuminate\Support\Facades\Route;
Route::prefix('v1')
->as('api.')
->middleware(['client.access-token'])
->group(function () {
// Auth
Route::prefix('client')
->as('client.')
->group(function () {
Route::controller(ClientController::class)->group(function () {
Route::post('login', 'login')->middleware(['throttle:client-auth', 'turnstile'])->name('login');
Route::post('register', 'register')->middleware(['throttle:client-auth', 'turnstile'])->name('register');
Route::post('verify-email', 'verifyEmail')->middleware('throttle:client-auth')->name('verify-email');
Route::post('resend-verify', 'resendVerifyCode')->middleware('throttle:client-auth')->name('resend-verify');
Route::post('refresh', 'refresh')->name('refresh');
Route::post('forgot-password', 'forgotPassword')->middleware(['throttle:client-auth', 'turnstile'])->name('forgot-password');
Route::post('reset-password', 'resetPassword')->middleware(['throttle:client-auth', 'turnstile'])->name('reset-password');
Route::get('reset-password/{token}', 'showResetToken')->middleware('throttle:client-auth')->name('reset-password.show');
Route::post('impersonate/exchange', 'impersonateExchange')->name('impersonate.exchange');
Route::post('login/two-factor', 'verifyLoginTwoFactor')->middleware('throttle:client-auth')->name('login.two-factor');
Route::post('login/two-factor/resend', 'resendLoginTwoFactor')->middleware('throttle:client-auth')->name('login.two-factor.resend');
Route::group(['middleware' => ['api.auth']], function () {
Route::get('me', 'me')->name('me');
Route::post('logout', 'logout')->name('logout');
Route::put('update', 'update')->name('update');
Route::get('login-history', 'loginHistory')->name('login-history');
Route::put('preferences', 'updatePreferences')->name('preferences.update');
Route::get('billing-profile', [BillingProfileController::class, 'show'])->name('billing-profile.show');
Route::put('billing-profile', [BillingProfileController::class, 'update'])->name('billing-profile.update');
Route::post('update-cccd', 'updateCccd')->name('update-cccd');
Route::put('change-password', 'changePassword')->name('change-password');
Route::get('two-factor', 'twoFactorStatus')->name('two-factor.status');
Route::post('two-factor/setup-code', 'sendTwoFactorSetupCode')->middleware('throttle:client-auth')->name('two-factor.setup-code');
Route::post('two-factor/enable', 'enableTwoFactor')->middleware('throttle:client-auth')->name('two-factor.enable');
Route::post('two-factor/disable', 'disableTwoFactor')->middleware('throttle:client-auth')->name('two-factor.disable');
Route::delete('two-factor/devices', 'revokeTrustedDevices')->name('two-factor.devices.revoke');
});
});
Route::middleware(['api.auth'])
->controller(OrderControllerRefactor::class)
->group(function () {
Route::get('orders', 'getClientOrders')->name('orders.list');
Route::get('orders/num/{orderNum}', 'getClientOrderDetailByOrderNum')->name('orders.detailByOrderNum');
Route::get('orders/{id}', 'getClientOrderDetail')->name('orders.detail');
Route::post('orders/{id}/retry', 'retryOrderProvisioning')->name('orders.retry');
Route::get('invoices', 'getClientInvoices')->name('invoices.list');
Route::get('invoices/number/{invoiceNumber}', 'getClientInvoiceDetailByNumber')->name(
'invoices.detailByNumber',
);
Route::post('invoices/number/{invoiceNumber}/pay', 'payClientInvoiceByNumber')->name(
'invoices.payByNumber',
);
Route::get('invoices/order/{orderNum}', 'getClientInvoiceDetailByOrderNum')->name(
'invoices.detailByOrderNum',
);
Route::get('invoices/order/{orderNum}/pdf', 'downloadInvoicePdfByOrderNum')->name(
'invoices.pdfByOrderNum',
);
Route::get('orders/{orderNum}/contracts/{attachmentId}', 'downloadOrderContract')->name(
'orders.contract',
);
Route::get('invoices/{id}', 'getClientInvoiceDetail')->name('invoices.detail');
Route::get('dich-vu', 'getClientServices')->name('services.list');
Route::patch('dich-vu/{id}/auto-renew', 'updateClientServiceAutoRenew')->name(
'services.autoRenew.update',
);
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('{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'])
->get('overview', [ClientServiceController::class, 'overview'])
->name('overview');
Route::middleware(['api.auth'])
->prefix('client-service')
->controller(ClientServiceController::class)
->as('client-service.')
->group(function () {
Route::get('/', 'index')->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('domain', 'indexDomain')->name('domain.list');
Route::get('group/{group}', 'indexGroup')->whereIn('group', ServiceGroup::ALL)->name('group.list');
Route::get('{uuid}', 'getDetail')->name('detail');
Route::get('{uuid}/attachments/{attachmentId}', 'downloadAttachment')->whereNumber('attachmentId')->name('attachments.download');
Route::post('{uuid}/cancel-request', [ServiceCancellationController::class, 'store'])->middleware('throttle:5,1')->name('cancel-request.store');
Route::delete('{uuid}/cancel-request', [ServiceCancellationController::class, 'destroy'])->name('cancel-request.destroy');
Route::get('{uuid}/renewals', ServiceRenewalHistoryController::class)->name('renewals');
Route::get('{uuid}/renewal-options', ServiceRenewalOptionsController::class)->name('renewal-options');
Route::get('{uuid}/ssl-archive', SslArchiveController::class)->name('ssl-archive');
Route::put('{uuid}/hosting-password', HostingPasswordController::class)->middleware('throttle:5,1')->name('hosting-password.update');
Route::get('{uuid}/console-ticket', 'getConsoleTicket')->name('console-ticket');
Route::get('{uuid}/available-os', 'getAvailableOs')->name('available-os');
Route::post('{uuid}/rebuild', 'rebuild')->name('rebuild');
Route::patch('{uuid}/auto-renew', 'updateAutoRenew')->name('auto-renew.update');
});
Route::middleware(['api.auth'])
->get('get-ticket-console', [ClientServiceController::class, 'getTicketByOrderNum'])
->name('console-ticket');
Route::middleware(['api.auth'])
->prefix('service')
->controller(ServiceController::class)
->as('service.')
->group(function () {
Route::get('/', 'getService')->name('list');
Route::get('{uuid}', 'getDetailServiceCustomer')->name('detail');
});
Route::middleware(['api.auth'])
->controller(TransactionController::class)
->group(function () {
Route::get('payment-history', 'getClientPaymentHistory')->name('payment-history.list');
Route::get('payment-history/{uuid}', 'getClientTransactionDetail')->whereUuid('uuid')->name('payment-history.detail');
});
Route::middleware(['api.auth'])
->prefix('notifications')
->controller(NotificationController::class)
->group(function () {
Route::get('/', fn () => app(NotificationController::class)->index(request(), 'client'));
Route::get('{uuid}', fn ($uuid) => app(NotificationController::class)->show(request(), 'client', $uuid))->whereUuid('uuid');
Route::patch('{uuid}/read', fn ($uuid) => app(NotificationController::class)->markRead(request(), 'client', $uuid))->whereUuid('uuid');
Route::patch('read-all', fn () => app(NotificationController::class)->markAllRead(request(), 'client'));
});
Route::middleware(['api.auth'])
->prefix('tickets')
->controller(ClientTicketController::class)
->as('tickets.')
->group(function () {
Route::get('/', 'index')->name('index');
Route::post('/', 'store')->name('store');
Route::get('{ticket_num}', 'show')->name('show');
Route::get('{ticket_num}/images/{index}', 'image')->whereNumber('index')->name('image');
Route::post('{ticket_num}/reply', 'reply')->middleware('throttle:10,1')->name('reply');
Route::patch('{ticket_num}/close', 'close')->name('close');
});
// Domain Orders (auth required)
Route::middleware(['api.auth'])
->prefix('domain-orders')
->controller(ClientDomainOrderController::class)
->as('domain-orders.')
->group(function () {
Route::post('/', 'store')->name('store');
});
});
// Services (Products) - Public & Protected Routes
Route::prefix('services')
->as('services.')
->group(function () {
Route::controller(ServiceController::class)->group(function () {
Route::get('get-list', 'getService')->name('getList');
Route::get('{product:slug}', 'getDetailService')->name('detail');
});
});
Route::middleware(['api.auth'])
->prefix('orders')
->as('orders.')
->group(function () {
Route::controller(OrderControllerRefactor::class)->group(function () {
Route::get('/', 'getList')->name('list');
Route::get('check-server-name', 'checkServerName')->name('check-server-name');
Route::post('/', 'storeOrder')->name('store');
Route::post('coupon-preview', CouponPreviewController::class)->middleware('throttle:20,1')->name('coupon-preview');
Route::post('{orderNum}/pay', 'payOrder')->name('pay');
});
});
Route::prefix('setting')
->as('setting.')
->group(function () {
Route::controller(SettingController::class)->group(function () {
Route::get('/', 'getSetting')->name('get');
Route::get('email', 'getEmailSetting')->name('email');
Route::get('turnstile', 'getTurnstileSetting')->name('turnstile');
Route::get('menu', 'getMenu')->name('menu');
Route::get('popups', 'getPopups')->name('popups');
});
});
// Public bank list for deposit form
Route::get('banks', [ClientBankController::class, 'index'])->name('banks.list');
Route::controller(AdministrativeDivisionController::class)->group(function () {
Route::get('provinces', 'provinces')->name('provinces');
Route::get('provinces/{province}/wards', 'wards')->where('province', '[0-9]+')->name('provinces.wards');
});
Route::prefix('post-categories')
->controller(ClientPostCategoryController::class)
->as('client.docs.categories.')
->group(function () {
Route::get('/', 'index')->name('index');
});
Route::prefix('posts')
->controller(ClientPostController::class)
->as('client.docs.posts.')
->group(function () {
Route::get('/', 'index')->name('index');
Route::get('featured', 'featured')->name('featured');
Route::get('{slug}', 'show')->name('show');
});
// Domain Pricing - Public Routes (no auth required)
Route::prefix('domain-pricing')
->as('domain-pricing.')
->controller(DomainPricingController::class)
->group(function () {
Route::get('/', 'index')->name('index');
});
// iNET Domain Tools - Public Routes (no auth required)
Route::prefix('inet-domain')
->as('inet-domain.')
->controller(InetDomainController::class)
->group(function () {
Route::get('whois/{domain}', 'whois')->name('whois')->middleware('throttle:20,1');
Route::get('check-available', 'checkAvailable')->name('check-available')->middleware('throttle:30,1');
Route::post('bulk-check-available', 'bulkCheckAvailable')->name('bulk-check-available')->middleware('throttle:10,1');
Route::post('ai-suggest', 'aiSuggest')->name('ai-suggest')->middleware('throttle:5,1');
Route::get('suffixes', 'suffixes')->name('suffixes');
});
// Product Categories - Public Routes (no auth required)
Route::prefix('group')
->as('product-categories.')
->group(function () {
Route::controller(ProductCategoryController::class)->group(function () {
Route::get('get-list', 'getCategories')->name('getList');
Route::get('{product_categories:slug}', 'getDetailCategory')->name('detail');
Route::get('{parentSlug}/{childSlug}', 'getChildCategoryWithParent')->name('childWithParent');
});
});
// Addons - Get available addons for a product
Route::prefix('products')
->as('products.')
->group(function () {
Route::get('{productId}/addons', [ServiceController::class, 'getProductAddons'])->name('addons');
});
Route::middleware(['api.auth'])
->prefix('transaction')
->as('transaction.')
->group(function () {
Route::controller(TransactionController::class)->group(function () {
Route::get('get-list', 'getTransactions')->name('getList');
Route::post('pay', 'handle')->name('pay');
Route::get('verify', 'verifyPayment')->name('verify');
Route::get('detail-by-code', 'getByPublicCode')->name('detailByCode');
});
Route::get('manual-deposit/pending', [ManualDepositController::class, 'pending'])->name('manualDeposit.pending');
Route::post('manual-deposit/prepare', [ManualDepositController::class, 'prepare'])->name('manualDeposit.prepare');
Route::post('manual-deposit/confirm', [ManualDepositController::class, 'confirm'])->name('manualDeposit.confirm');
Route::delete('manual-deposit/{uuid}/cancel', [ManualDepositController::class, 'cancel'])->name('manualDeposit.cancel');
Route::post('manual-deposit/upload-proof', [ManualDepositController::class, 'uploadProof'])->name('manualDeposit.uploadProof');
Route::get('manual-deposit/{uuid}', [ManualDepositController::class, 'show'])->whereUuid('uuid')->name('manualDeposit.show');
});
});
Route::prefix('v1/invoice-lookup')
->middleware(['client.access-token', 'throttle:invoice-lookup', 'turnstile'])
->controller(InvoiceLookupController::class)
->as('api.invoice-lookup.')
->group(function () {
Route::post('/', 'show')->name('show');
Route::post('pdf', 'pdf')->name('pdf');
});
Route::get('v1/payment/vnpay/return', [TransactionController::class, 'returnCallback']);
Route::prefix('v1/system')
->middleware(['client.access-token', 'api.auth'])
->group(function () {
// VM power & network operations
Route::controller(VmController::class)->group(function () {
Route::get('vm-power-state', 'powerState')->name('vm.power-state');
Route::get('vm-metrics', 'metrics')->name('vm.metrics');
Route::get('vm-metrics/history', 'metricsHistory')->name('vm.metrics.history');
Route::post('vm-power-on', 'powerOn')->name('vm.power-on');
Route::post('vm-power-off', 'powerOff')->name('vm.power-off');
Route::get('vm-ip-options', 'ipOptions')->name('vm.ip-options');
Route::post('vm-change-ip', 'changeIp')->name('vm.change-ip');
Route::post('vm-change-port', 'changePort')->name('vm.change-port');
Route::post('vm-change-password', 'changePassword')->name('vm.change-password');
Route::get('get-ticket-console', 'consoleTicket')->name('vm.console-ticket');
Route::post('vm-restart', 'restart')->name('vm.restart');
Route::post('vm-action-click', 'actionClick')->name('vm.action-click');
});
// VM Snapshot operations
Route::controller(SnapshotController::class)->group(function () {
Route::get('vm-snapshots', 'index')->name('vm.snapshots');
Route::post('vm-create-snapshot', 'store')->name('vm.create-snapshot');
Route::post('vm-revert-snapshot', 'revert')->name('vm.revert-snapshot');
Route::post('vm-delete-snapshot', 'destroy')->name('vm.delete-snapshot');
});
// VM Rebuild & Addon operations
Route::controller(VmRebuildController::class)->group(function () {
Route::get('vm-rebuild-options', 'options')->name('vm.rebuild-options');
Route::post('vm-rebuild', 'rebuild')->name('vm.rebuild');
Route::get('vm-addon-options', 'addonOptions')->name('vm.addon-options');
Route::post('vm-upgrade-addons', 'upgradeAddons')->name('vm.upgrade-addons');
});
});
Route::any('{any}', function () {
return response()->json(
[
'error' => true,
'status' => 404,
'message' => 'API Route not found',
],
404,
);
})->where('any', '^v1/.*');