diff --git a/app/Http/Controllers/Api/Admin/ActivityLog/ActivityLogController.php b/app/Http/Controllers/Api/Admin/ActivityLog/ActivityLogController.php new file mode 100644 index 0000000..0120a36 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/ActivityLog/ActivityLogController.php @@ -0,0 +1,111 @@ +get('per_page', 20), 200); + + $filters = $request->only([ + 'log_name', + 'event', + 'causer_type', + 'causer_id', + 'causer_name', + 'subject_type', + 'subject_id', + 'ip_address', + 'date_from', + 'date_to', + 'search', + 'page', + ]); + + $cacheKey = CacheKey::build('activity_logs', 'list', array_merge( + $filters, + ['per_page' => $perPage, 'page' => (int) $request->get('page', 1)] + )); + + $data = Cache::remember($cacheKey, self::CACHE_TTL, function () use ($filters, $perPage) { + $paginator = $this->activityLogRepository->filter($filters, $perPage); + + return [ + 'items' => $paginator->items(), + 'total' => $paginator->total(), + 'per_page' => $paginator->perPage(), + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + ]; + }); + + return response()->json(['error' => false, 'data' => $data]); + } + + public function show(int $id): JsonResponse + { + $cacheKey = CacheKey::build('activity_logs', 'detail', ['id' => $id]); + + $log = Cache::remember($cacheKey, self::CACHE_TTL * 5, function () use ($id) { + return $this->activityLogRepository->find($id); + }); + + if (!$log) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy log'], 404); + } + + return response()->json(['error' => false, 'data' => $log]); + } + + public function modules(): JsonResponse + { + $cacheKey = CacheKey::build('activity_logs', 'modules'); + + $modules = Cache::remember($cacheKey, self::CACHE_TTL * 10, function () { + return ActivityLog::query() + ->select('log_name') + ->distinct() + ->orderBy('log_name') + ->pluck('log_name'); + }); + + return response()->json(['error' => false, 'data' => $modules]); + } + + public function stats(): JsonResponse + { + $cacheKey = CacheKey::build('activity_logs', 'stats'); + + $stats = Cache::remember($cacheKey, self::CACHE_TTL * 2, function () { + return [ + 'total' => ActivityLog::count(), + 'today' => ActivityLog::whereDate('created_at', today())->count(), + 'by_event' => ActivityLog::selectRaw('event, COUNT(*) as count') + ->groupBy('event') + ->orderByDesc('count') + ->limit(10) + ->pluck('count', 'event'), + 'by_causer' => ActivityLog::selectRaw('causer_type, COUNT(*) as count') + ->groupBy('causer_type') + ->pluck('count', 'causer_type'), + ]; + }); + + return response()->json(['error' => false, 'data' => $stats]); + } +} diff --git a/app/Http/Controllers/Api/Admin/Auth/AuthController.php b/app/Http/Controllers/Api/Admin/Auth/AuthController.php index a64a17b..a4c8ea3 100644 --- a/app/Http/Controllers/Api/Admin/Auth/AuthController.php +++ b/app/Http/Controllers/Api/Admin/Auth/AuthController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Admin\Auth; use App\Http\Controllers\Controller; use App\Http\Resources\Api\Admin\AdminUserResource; +use App\Services\ActivityLogger; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -12,6 +13,8 @@ use Tymon\JWTAuth\Exceptions\JWTException; class AuthController extends Controller { + public function __construct(private readonly ActivityLogger $activityLogger) {} + public function login(Request $request): JsonResponse { $request->validate([ @@ -22,6 +25,11 @@ class AuthController extends Controller $token = Auth::guard('admin')->attempt($request->only('username', 'password')); if (!$token) { + $this->activityLogger->log( + event: 'login_failed', + logName: 'auth', + description: '[admin] Đăng nhập thất bại với username: ' . $request->username, + ); return response()->json([ 'error' => true, 'message' => 'Thông tin đăng nhập không chính xác.', @@ -30,6 +38,8 @@ class AuthController extends Controller $user = Auth::guard('admin')->user(); + $this->activityLogger->logAuth('login', 'admin', $user->id, $user->username ?? $user->email); + return response()->json([ 'error' => false, 'data' => [ @@ -76,6 +86,11 @@ class AuthController extends Controller public function logout(): JsonResponse { + $user = Auth::guard('admin')->user(); + if ($user) { + $this->activityLogger->logAuth('logout', 'admin', $user->id, $user->username ?? $user->email); + } + Auth::guard('admin')->logout(); return response()->json([ diff --git a/app/Http/Controllers/Api/Client/ClientController.php b/app/Http/Controllers/Api/Client/ClientController.php index a70cf9f..3a4db1d 100644 --- a/app/Http/Controllers/Api/Client/ClientController.php +++ b/app/Http/Controllers/Api/Client/ClientController.php @@ -7,6 +7,7 @@ use App\Http\Requests\Api\Client\ClientAuthRequest; use App\Http\Requests\Api\Client\ClientRegisterRequest; use App\Http\Requests\Api\Client\ClientUpdateRequest; use App\Http\Resources\Api\Client\ClientResource; +use App\Services\ActivityLogger; use App\Services\Api\Client\ClientAuthService; use Illuminate\Http\Request; use Tymon\JWTAuth\Facades\JWTAuth; @@ -21,10 +22,12 @@ use Tymon\JWTAuth\Exceptions\JWTException; class ClientController extends Controller { protected $authService; + protected ActivityLogger $activityLogger; - public function __construct(ClientAuthService $authService) + public function __construct(ClientAuthService $authService, ActivityLogger $activityLogger) { $this->authService = $authService; + $this->activityLogger = $activityLogger; } /** @@ -130,10 +133,13 @@ class ClientController extends Controller { $result = $this->authService->authenticateClient($request->validated()); + $client = $result['client']; + $this->activityLogger->logAuth('login', 'customer', $client->id, $client->email); + return response()->json([ 'error' => false, 'data' => [ - 'client' => new ClientResource($result['client']->loadCount(['vps', 'hosting', 'domain'])), + 'client' => new ClientResource($client->loadCount(['vps', 'hosting', 'domain'])), 'token' => $result['token'], 'refresh_token' => $result['refresh_token'], 'expires_in' => config('jwt.ttl', 60) * 60, // seconds @@ -182,10 +188,13 @@ class ClientController extends Controller { $result = $this->authService->registerClient($request->validated()); + $client = $result['client']; + $this->activityLogger->logAuth('register', 'customer', $client->id, $client->email); + return response()->json([ 'error' => false, 'data' => [ - 'client' => new ClientResource($result['client']->loadCount(['vps', 'hosting', 'domain'])), + 'client' => new ClientResource($client->loadCount(['vps', 'hosting', 'domain'])), 'token' => $result['token'], 'refresh_token' => $result['refresh_token'], 'expires_in' => config('jwt.ttl', 60) * 60, @@ -222,6 +231,11 @@ class ClientController extends Controller */ public function logout(Request $request) { + $customer = auth('client')->user(); + if ($customer) { + $this->activityLogger->logAuth('logout', 'customer', $customer->id, $customer->email); + } + if ($rawToken = $request->input('refresh_token')) { $this->authService->revokeRefreshToken($rawToken); } @@ -297,9 +311,22 @@ class ClientController extends Controller { $user = auth('client')->user(); + $oldValues = array_intersect_key($user->toArray(), $request->validated()); $user->fill($request->validated()); + $dirty = $user->getDirty(); $user->save(); + if (!empty($dirty)) { + $this->activityLogger->log( + event: 'profile_update', + logName: 'Customer', + description: "Customer {$user->email} cập nhật thông tin cá nhân: " . implode(', ', array_keys($dirty)), + subject: $user, + oldValues: array_intersect_key($oldValues, $dirty), + newValues: array_diff_key($dirty, array_flip(['password', 'remember_token'])), + ); + } + return response()->json([ 'error' => false, 'message' => 'Cập nhật thông tin thành công', diff --git a/app/Http/Controllers/Api/Client/Service/ServiceController.php b/app/Http/Controllers/Api/Client/Service/ServiceController.php index 249c215..bb11ba7 100644 --- a/app/Http/Controllers/Api/Client/Service/ServiceController.php +++ b/app/Http/Controllers/Api/Client/Service/ServiceController.php @@ -9,6 +9,7 @@ use App\Http\Services\vCenter; use App\Models\Order; use App\Models\Service; use App\Repositories\Api\ServiceClient\ServiceClientRepository; +use App\Services\ActivityLogger; use App\Services\Api\ServiceClient\ServiceClientService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -17,13 +18,16 @@ class ServiceController extends Controller { protected $serviceClientService; protected $serviceClientRepository; + protected ActivityLogger $activityLogger; public function __construct( ServiceClientService $serviceClientService, - ServiceClientRepository $serviceClientRepository + ServiceClientRepository $serviceClientRepository, + ActivityLogger $activityLogger, ) { $this->serviceClientService = $serviceClientService; $this->serviceClientRepository = $serviceClientRepository; + $this->activityLogger = $activityLogger; } public function getService(Request $request) @@ -99,6 +103,14 @@ class ServiceController extends Controller $ticket = basename(parse_url((string) $consoleTicket['data'], PHP_URL_PATH)); + $this->activityLogger->log( + event: 'console_access', + logName: 'VM', + description: "Customer {$customer->email} truy cập console VM: {$vm->name} (service: {$uuid})", + subject: $vm, + newValues: ['service_uuid' => $uuid, 'vm_name' => $vm->name], + ); + return response()->json([ 'error' => false, 'message' => $ticket, @@ -154,6 +166,14 @@ class ServiceController extends Controller return response()->json(['error' => true, 'message' => $consoleTicket['data']]); } + $this->activityLogger->log( + event: 'console_access', + logName: 'VM', + description: "Customer {$customer->email} truy cập console VM: {$vm->name} (order: {$order->order_num})", + subject: $vm, + newValues: ['order_num' => $order->order_num, 'vm_name' => $vm->name], + ); + return response()->json([ 'error' => false, 'data' => $consoleTicket['data'], diff --git a/app/Http/Controllers/Api/Service/OrderController.php b/app/Http/Controllers/Api/Service/OrderController.php index ae4ee0a..eb70e4c 100644 --- a/app/Http/Controllers/Api/Service/OrderController.php +++ b/app/Http/Controllers/Api/Service/OrderController.php @@ -19,6 +19,7 @@ use App\Models\Order; use App\Models\Product; use App\Models\Service; use App\Repositories\Api\Order\OrderRepository; +use App\Services\ActivityLogger; use App\Services\Api\Order\OrderService; use Carbon\Carbon; use Illuminate\Http\JsonResponse; @@ -31,8 +32,9 @@ use Illuminate\Support\Facades\DB; class OrderController extends Controller { public function __construct( - protected OrderService $orderService, - protected OrderRepository $orderRepo, + protected OrderService $orderService, + protected OrderRepository $orderRepo, + protected ActivityLogger $activityLogger, ) {} // ========================================================================= @@ -147,6 +149,14 @@ class OrderController extends Controller $order = $result['order']->load(['orderItems.product', 'invoice', 'customer']); $invoice = $result['invoice']; + $this->activityLogger->log( + event: 'created', + logName: 'Order', + description: "Customer {$customer->email} tạo đơn hàng #{$order->order_num} - Tổng: " . number_format($order->total) . ' VNĐ', + subject: $order, + newValues: ['order_num' => $order->order_num, 'total' => $order->total, 'product_id' => $data['product_id']], + ); + return response()->json([ 'success' => true, 'message' => 'Tạo đơn hàng thành công.', @@ -190,6 +200,14 @@ class OrderController extends Controller $this->orderService->pay($order, $request->payment_method); + $this->activityLogger->log( + event: 'payment', + logName: 'Order', + description: "Customer {$request->user()->email} thanh toán đơn hàng #{$order->order_num} qua {$request->payment_method}", + subject: $order, + newValues: ['payment_method' => $request->payment_method, 'total' => $order->total], + ); + return response()->json(['success' => true, 'message' => 'Thanh toán thành công.']); } catch (\Exception $e) { return $this->error($e->getMessage()); @@ -222,6 +240,13 @@ class OrderController extends Controller RetryProvisioningOrchestrator::dispatch($order->id, $request->user()->id); + $this->activityLogger->log( + event: 'retry_provisioning', + logName: 'Order', + description: "Customer {$request->user()->email} yêu cầu khởi tạo lại dịch vụ cho đơn hàng #{$order->order_num}", + subject: $order, + ); + return response()->json([ 'success' => true, 'message' => 'Đã yêu cầu khởi tạo lại dịch vụ. Hệ thống sẽ kiểm tra và xử lý trong vài phút.', @@ -470,6 +495,14 @@ class OrderController extends Controller $service->update(['auto_renew' => (bool) $validated['auto_renew']]); + $this->activityLogger->log( + event: 'updated', + logName: 'Service', + description: "Customer {$request->user()->email} " . ($validated['auto_renew'] ? 'bật' : 'tắt') . " tự động gia hạn dịch vụ #{$service->id}", + subject: $service, + newValues: ['auto_renew' => (bool) $validated['auto_renew']], + ); + return response()->json([ 'success' => true, 'message' => 'Cập nhật tự động gia hạn thành công.', @@ -597,6 +630,14 @@ class OrderController extends Controller ->delay(now()->addSeconds(5)); } + $this->activityLogger->log( + event: 'renew', + logName: 'Service', + description: "Customer {$customer->email} gia hạn dịch vụ #{$service->id} ({$service->server_name}) - {$months} tháng - " . number_format($price) . ' VNĐ', + subject: $service, + newValues: ['months' => $months, 'price' => $price, 'invoice_number' => $invoice->invoice_number], + ); + return response()->json([ 'success' => true, 'message' => 'Gia hạn thành công. Dịch vụ đã được kích hoạt lại.', diff --git a/app/Http/Controllers/Api/Service/SnapshotController.php b/app/Http/Controllers/Api/Service/SnapshotController.php index 584907f..7b26e08 100644 --- a/app/Http/Controllers/Api/Service/SnapshotController.php +++ b/app/Http/Controllers/Api/Service/SnapshotController.php @@ -10,6 +10,7 @@ use App\Jobs\VirtualMachine\Snapshot\RevertSnapshot; use App\Jobs\VirtualMachine\Snapshot\RemoveSnapshot; use App\Models\ModuleSnapshot; use App\Models\ModuleVmware; +use App\Services\ActivityLogger; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -17,6 +18,8 @@ class SnapshotController extends Controller { use ResolvesVm; + public function __construct(protected ActivityLogger $activityLogger) {} + public function store(Request $request): JsonResponse { $customer = $request->user(); @@ -47,6 +50,14 @@ class SnapshotController extends Controller CreateSnapshot::dispatch($snapshot->id); + $this->activityLogger->log( + event: 'created', + logName: 'Snapshot', + description: "Customer {$customer->email} tạo snapshot: {$snapshot->name} cho VM #{$vm->id}", + subject: $snapshot, + newValues: ['name' => $snapshot->name, 'vm_id' => $vm->id], + ); + return response()->json([ 'error' => false, 'message' => 'Đã gửi yêu cầu tạo snapshot vào hàng đợi', @@ -159,6 +170,13 @@ class SnapshotController extends Controller RevertSnapshot::dispatch($vm->id, $snapshot->id); + $this->activityLogger->log( + event: 'revert', + logName: 'Snapshot', + description: "Customer {$customer->email} revert snapshot: {$snapshot->name} (ID: {$snapshot->id}) cho VM #{$vm->id}", + subject: $snapshot, + ); + return response()->json([ 'error' => false, 'message' => 'Đã gửi yêu cầu revert snapshot vào hàng đợi', @@ -189,6 +207,13 @@ class SnapshotController extends Controller RemoveSnapshot::dispatch($vm->id, $snapshot->id); + $this->activityLogger->log( + event: 'deleted', + logName: 'Snapshot', + description: "Customer {$customer->email} xóa snapshot: {$snapshot->name} (ID: {$snapshot->id}) của VM #{$vm->id}", + subject: $snapshot, + ); + return response()->json([ 'error' => false, 'message' => 'Đã gửi yêu cầu xóa snapshot vào hàng đợi', diff --git a/app/Http/Controllers/Api/Service/VmController.php b/app/Http/Controllers/Api/Service/VmController.php index a842578..a834ac8 100644 --- a/app/Http/Controllers/Api/Service/VmController.php +++ b/app/Http/Controllers/Api/Service/VmController.php @@ -14,6 +14,7 @@ use App\Jobs\VirtualMachine\ChangeVmPasswordJob; use App\Models\ModuleVmware; use App\Models\Os; use App\Models\Service; +use App\Services\ActivityLogger; use App\Services\VmProvisionService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -24,6 +25,8 @@ class VmController extends Controller use ResolvesVm { ResolvesVm::resolveVmById as resolveVmForClient; } + + public function __construct(protected ActivityLogger $activityLogger) {} // ------------------------------------------------------------------------- // Power ON // ------------------------------------------------------------------------- @@ -47,6 +50,13 @@ class VmController extends Controller $result = $action->execute($vm); + $this->activityLogger->log( + event: 'power_on', + logName: 'VM', + description: "Customer {$request->user()->email} bật VM: {$vm->name} (ID: {$vm->vmware_id})", + subject: $vm, + ); + return $this->vmResponse($result, 'Đã gửi lệnh bật VM.'); } @@ -73,6 +83,13 @@ class VmController extends Controller $result = $action->execute($vm); + $this->activityLogger->log( + event: 'power_off', + logName: 'VM', + description: "Customer {$request->user()->email} tắt VM: {$vm->name} (ID: {$vm->vmware_id})", + subject: $vm, + ); + return $this->vmResponse($result, 'Đã gửi lệnh tắt VM.'); } @@ -189,6 +206,14 @@ class VmController extends Controller . "ℹ️ Kiểm tra lại VM để xác nhận IP đã thay đổi chưa."); } + $this->activityLogger->log( + event: 'change_ip', + logName: 'VM', + description: "Customer {$request->user()->email} đổi IP VM: {$vm->name} → {$validated['ip_address']}", + subject: $vm, + newValues: ['ip_address' => $validated['ip_address'], 'gateway' => $gateway, 'os_type' => $osType], + ); + return response()->json([ 'success' => true, 'message' => 'Đã gửi lệnh đổi IP.', @@ -234,6 +259,16 @@ class VmController extends Controller ? (bool) $result['success'] : !($result['error'] ?? true); + if ($ok) { + $this->activityLogger->log( + event: 'change_port', + logName: 'VM', + description: "Customer {$request->user()->email} đổi cổng RDP VM: {$vm->name} → port {$validated['new_port']}", + subject: $vm, + newValues: ['new_port' => $validated['new_port']], + ); + } + return response()->json([ 'success' => $ok, 'message' => $ok ? 'Đổi cổng RDP thành công.' : ($result['message'] ?? 'Đổi cổng thất bại.'), @@ -269,6 +304,14 @@ class VmController extends Controller return $result; } + $this->activityLogger->log( + event: 'console_access', + logName: 'VM', + description: "Customer {$request->user()->email} truy cập console VM: {$vm->name} (ID: {$vm->vmware_id})", + subject: $vm, + newValues: ['product_id' => $productId, 'vm_name' => $vm->name], + ); + return response()->json([ 'error' => false, 'data' => $result['data'], @@ -300,6 +343,13 @@ class VmController extends Controller ], 400); } + $this->activityLogger->log( + event: 'restart', + logName: 'VM', + description: "Customer {$request->user()->email} restart VM: {$vm->name} (ID: {$vm->vmware_id})", + subject: $vm, + ); + return response()->json([ 'error' => false, 'message' => 'Đã gửi lệnh restart VM', @@ -345,6 +395,14 @@ class VmController extends Controller $validated['current_password'], ); + $this->activityLogger->log( + event: 'change_password', + logName: 'VM', + description: "Customer {$request->user()->email} yêu cầu đổi mật khẩu VM: {$vm->name}", + subject: $vm, + newValues: ['os_family' => $osFamily], + ); + return response()->json([ 'success' => true, 'message' => 'Yêu cầu đổi mật khẩu đã được ghi nhận. Hệ thống sẽ xử lý trong vài phút.', diff --git a/app/Http/Controllers/Api/Service/VmRebuildController.php b/app/Http/Controllers/Api/Service/VmRebuildController.php index d1920b7..88d371b 100644 --- a/app/Http/Controllers/Api/Service/VmRebuildController.php +++ b/app/Http/Controllers/Api/Service/VmRebuildController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Controller; use App\Http\Controllers\Api\Service\Concerns\ResolvesVm; use App\Http\Services\vCenter; use App\Models\ModuleVmware; +use App\Services\ActivityLogger; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -14,6 +15,8 @@ class VmRebuildController extends Controller { use ResolvesVm; + public function __construct(protected ActivityLogger $activityLogger) {} + public function options(Request $request): JsonResponse { $productId = (int) $request->query('product'); @@ -103,6 +106,14 @@ class VmRebuildController extends Controller ], 400); } + $this->activityLogger->log( + event: 'rebuild', + logName: 'VM', + description: "Customer {$customer->email} rebuild VM: {$vm->name} với OS ID: {$payload['os_id']}", + subject: $vm, + newValues: ['os_id' => $payload['os_id']], + ); + return response()->json([ 'error' => false, 'message' => 'Đã gửi yêu cầu rebuild VM', @@ -207,6 +218,14 @@ class VmRebuildController extends Controller } } + $this->activityLogger->log( + event: 'upgrade_addons', + logName: 'VM', + description: "Customer {$customer->email} nâng cấp addons VM: {$vm->name} - áp dụng: " . implode(', ', $applied ?: ['none']), + subject: $vm, + newValues: array_filter(['cpu' => $payload['cpu'] ?? null, 'ram' => $payload['ram'] ?? null, 'disk' => $payload['disk'] ?? null]), + ); + return response()->json([ 'error' => false, 'message' => 'Đã gửi yêu cầu nâng cấp addons', diff --git a/app/Http/Controllers/Api/Transaction/ManualDepositController.php b/app/Http/Controllers/Api/Transaction/ManualDepositController.php index ed47967..e504d30 100644 --- a/app/Http/Controllers/Api/Transaction/ManualDepositController.php +++ b/app/Http/Controllers/Api/Transaction/ManualDepositController.php @@ -8,6 +8,7 @@ use App\Http\Requests\DepositRequest; use App\Http\Services\Telegram; use App\Models\Log; use App\Models\Transaction; +use App\Services\ActivityLogger; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -16,6 +17,7 @@ use Illuminate\Support\Str; class ManualDepositController extends Controller { + public function __construct(protected ActivityLogger $activityLogger) {} /** * GET /v1/transaction/manual-deposit/pending * Kiểm tra xem user có GD manual đang chờ duyệt (đã submit proof) không. @@ -111,6 +113,13 @@ class ManualDepositController extends Controller return response()->json(['error' => true, 'message' => 'Có lỗi xảy ra, vui lòng thử lại'], 500); } + $this->activityLogger->log( + event: 'deposit_prepare', + logName: 'Transaction', + description: "Customer {$user->email} tạo yêu cầu nạp tiền thủ công: " . number_format($request->amount) . ' VNĐ - Mã: ' . $publicCode, + newValues: ['public_code' => $publicCode, 'amount' => $request->amount, 'type' => 'manual'], + ); + return response()->json([ 'error' => false, 'data' => [ @@ -136,12 +145,22 @@ class ManualDepositController extends Controller ->whereNull('proof_image') ->firstOrFail(); + $amount = $transaction->amount; + $publicCode = $transaction->public_code; + DB::transaction(function () use ($transaction, $user) { $user->total_pending = max(0, $user->total_pending - $transaction->amount); $user->save(); $transaction->forceDelete(); }); + $this->activityLogger->log( + event: 'deposit_cancel', + logName: 'Transaction', + description: "Customer {$user->email} huỷ yêu cầu nạp tiền thủ công: " . number_format($amount) . ' VNĐ - Mã: ' . $publicCode, + newValues: ['public_code' => $publicCode, 'amount' => $amount], + ); + return response()->json(['error' => false, 'message' => 'Đã huỷ yêu cầu']); } @@ -188,6 +207,13 @@ class ManualDepositController extends Controller 'customer_id' => $user->id, ]); + $this->activityLogger->log( + event: 'deposit_confirm', + logName: 'Transaction', + description: "Customer {$user->email} nộp chứng từ nạp tiền thủ công: " . number_format($transaction->amount) . ' VNĐ - Mã: ' . $transaction->public_code, + newValues: ['public_code' => $transaction->public_code, 'amount' => $transaction->amount, 'has_proof' => true], + ); + return response()->json([ 'error' => false, 'message' => 'Yêu cầu nạp tiền đã được ghi nhận, vui lòng chờ admin xác nhận', diff --git a/app/Http/Controllers/Api/Transaction/TransactionController.php b/app/Http/Controllers/Api/Transaction/TransactionController.php index e47d768..0fcffe2 100644 --- a/app/Http/Controllers/Api/Transaction/TransactionController.php +++ b/app/Http/Controllers/Api/Transaction/TransactionController.php @@ -11,6 +11,7 @@ use App\Models\Customer; use App\Models\Invoice; use App\Models\Log; use App\Models\Transaction; +use App\Services\ActivityLogger; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -20,10 +21,12 @@ use Illuminate\Support\Str; class TransactionController extends Controller { protected VNPayService $vnpayService; + protected ActivityLogger $activityLogger; - public function __construct(VNPayService $vnpayService) + public function __construct(VNPayService $vnpayService, ActivityLogger $activityLogger) { $this->vnpayService = $vnpayService; + $this->activityLogger = $activityLogger; } public function handle(DepositRequest $request) @@ -48,6 +51,14 @@ class TransactionController extends Controller 'transaction_uuid' => $uuid, ]); + $this->activityLogger->log( + event: 'deposit_vnpay', + logName: 'Transaction', + description: "Customer {$user->email} tạo yêu cầu nạp tiền VNPay: " . number_format($request->amount) . ' VNĐ - Mã: ' . $publicCode, + subject: $transaction, + newValues: ['public_code' => $publicCode, 'amount' => $request->amount, 'type' => 'vnpay'], + ); + return response()->json([ 'error' => false, 'message' => 'Tạo yêu cầu nạp tiền thành công', @@ -151,6 +162,14 @@ class TransactionController extends Controller 'customer_id' => $transaction->customer_id, ]); + $this->activityLogger->log( + event: 'deposit_success', + logName: 'Transaction', + description: "Customer {$customer->email} nạp tiền VNPay thành công: " . number_format($transaction->amount) . ' VNĐ - Mã: ' . $transaction->public_code, + subject: $transaction, + newValues: ['amount' => $transaction->amount, 'new_total' => $customer->total, 'public_code' => $transaction->public_code], + ); + DepositSuccessJob::dispatch([ 'customer_email' => $customer->email, 'customer_name' => $customer->fullname, diff --git a/app/Models/ActivityLog.php b/app/Models/ActivityLog.php new file mode 100644 index 0000000..f5b9bf6 --- /dev/null +++ b/app/Models/ActivityLog.php @@ -0,0 +1,50 @@ + 'array', + 'new_values' => 'array', + 'created_at' => 'datetime', + ]; + + protected static function booting(): void + { + static::creating(function (self $model) { + $model->created_at = now(); + }); + } + + public function causer() + { + return match ($this->causer_type) { + 'admin' => $this->belongsTo(User::class, 'causer_id'), + 'customer' => $this->belongsTo(Customer::class, 'causer_id'), + default => null, + }; + } +} diff --git a/app/Models/Addon.php b/app/Models/Addon.php index 27189ee..fc06491 100644 --- a/app/Models/Addon.php +++ b/app/Models/Addon.php @@ -3,12 +3,13 @@ namespace App\Models; use App\Enums\AddonType; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Addon extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'product_addon'; diff --git a/app/Models/Bank.php b/app/Models/Bank.php index 4977113..93180be 100644 --- a/app/Models/Bank.php +++ b/app/Models/Bank.php @@ -2,13 +2,14 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Bank extends Model { - use HasFactory, SoftDeletes; + use HasFactory, SoftDeletes, LogsActivity; protected $table = 'banks'; diff --git a/app/Models/Customer.php b/app/Models/Customer.php index c739870..417855e 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -10,7 +11,7 @@ use Tymon\JWTAuth\Contracts\JWTSubject; class Customer extends Authenticatable implements JWTSubject { - use HasFactory, HasUuids, SoftDeletes; + use HasFactory, HasUuids, SoftDeletes, LogsActivity; protected $table = 'customers'; diff --git a/app/Models/Discount.php b/app/Models/Discount.php index fb1db13..3488f18 100644 --- a/app/Models/Discount.php +++ b/app/Models/Discount.php @@ -2,12 +2,13 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Discount extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'discount'; protected $fillable = [ diff --git a/app/Models/DnsDomain.php b/app/Models/DnsDomain.php index f4f7a5c..6de0cd7 100644 --- a/app/Models/DnsDomain.php +++ b/app/Models/DnsDomain.php @@ -2,12 +2,13 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class DnsDomain extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'dns_manager_domain'; diff --git a/app/Models/DnsRecord.php b/app/Models/DnsRecord.php index 2be4402..2255419 100644 --- a/app/Models/DnsRecord.php +++ b/app/Models/DnsRecord.php @@ -2,12 +2,13 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class DnsRecord extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'dns_manager_ns'; diff --git a/app/Models/DnsServer.php b/app/Models/DnsServer.php index f4836b4..a92d94b 100644 --- a/app/Models/DnsServer.php +++ b/app/Models/DnsServer.php @@ -2,12 +2,13 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class DnsServer extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'dns_manager_servers'; diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 874514e..7dbafc8 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -3,13 +3,14 @@ namespace App\Models; use App\Enums\Invoice\InvoiceStatus; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Invoice extends Model { - use HasFactory, SoftDeletes; + use HasFactory, SoftDeletes, LogsActivity; protected $table = 'invoices'; diff --git a/app/Models/IpPool.php b/app/Models/IpPool.php index cb1ecbf..1d898f2 100644 --- a/app/Models/IpPool.php +++ b/app/Models/IpPool.php @@ -2,13 +2,14 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class IpPool extends Model { - use HasFactory, SoftDeletes; + use HasFactory, SoftDeletes, LogsActivity; protected $fillable = [ 'ip_address', diff --git a/app/Models/ModuleDomain.php b/app/Models/ModuleDomain.php index b4107a1..8ab975b 100644 --- a/app/Models/ModuleDomain.php +++ b/app/Models/ModuleDomain.php @@ -2,12 +2,13 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class ModuleDomain extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'module_domain'; diff --git a/app/Models/ModuleHosting.php b/app/Models/ModuleHosting.php index 9f3db74..84b0e3c 100644 --- a/app/Models/ModuleHosting.php +++ b/app/Models/ModuleHosting.php @@ -2,13 +2,14 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class ModuleHosting extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'module_hosting'; diff --git a/app/Models/ModuleIpAddress.php b/app/Models/ModuleIpAddress.php index 74e829e..4333208 100644 --- a/app/Models/ModuleIpAddress.php +++ b/app/Models/ModuleIpAddress.php @@ -2,13 +2,14 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class ModuleIpAddress extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'module_ip_address'; diff --git a/app/Models/ModuleSnapshot.php b/app/Models/ModuleSnapshot.php index d588c0d..d0b2602 100644 --- a/app/Models/ModuleSnapshot.php +++ b/app/Models/ModuleSnapshot.php @@ -2,12 +2,13 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class ModuleSnapshot extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'module_snapshot'; diff --git a/app/Models/ModuleVmware.php b/app/Models/ModuleVmware.php index d4e8a12..01bf20c 100644 --- a/app/Models/ModuleVmware.php +++ b/app/Models/ModuleVmware.php @@ -2,12 +2,13 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class ModuleVmware extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'module_vmware'; diff --git a/app/Models/Order.php b/app/Models/Order.php index 65c0728..ecfcb75 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -9,7 +10,7 @@ use App\Models\ModuleVmware; class Order extends Model { - use HasFactory, SoftDeletes; + use HasFactory, SoftDeletes, LogsActivity; protected $table = 'orders'; diff --git a/app/Models/Payment.php b/app/Models/Payment.php index e19d5f5..05df2f6 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -2,12 +2,13 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Payment extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'payment'; diff --git a/app/Models/Product.php b/app/Models/Product.php index 257d62d..9569037 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -3,13 +3,14 @@ namespace App\Models; use App\Traits\HasUuid; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Product extends Model { - use HasFactory, HasUuid, SoftDeletes; + use HasFactory, HasUuid, SoftDeletes, LogsActivity; protected array $uuidColumns = ['uuid']; protected string $slugColumn = 'name'; diff --git a/app/Models/Service.php b/app/Models/Service.php index 2564db4..0e491c1 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\LogsActivity; use App\View\Components\Module\OsVersion; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; class Service extends Model { - use HasFactory, HasUuids, SoftDeletes; + use HasFactory, HasUuids, SoftDeletes, LogsActivity; public function uniqueIds(): array { diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index a2ef66e..0d77e47 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -2,12 +2,13 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Ticket extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $table = 'ticket'; diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 2f0aa17..6c62368 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -9,7 +10,7 @@ use App\Traits\HasUuid; class Transaction extends Model { - use HasFactory, SoftDeletes, HasUuid; + use HasFactory, SoftDeletes, HasUuid, LogsActivity; protected $table = 'transactions'; protected $fillable = [ diff --git a/app/Models/User.php b/app/Models/User.php index 0293551..02c5c20 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -10,7 +11,7 @@ use Tymon\JWTAuth\Contracts\JWTSubject; class User extends Authenticatable implements JWTSubject { - use HasFactory, Notifiable; + use HasFactory, Notifiable, LogsActivity; use HasRoles; protected $table = 'user'; diff --git a/app/Models/VirtualMachines.php b/app/Models/VirtualMachines.php index 7bf74b7..5c0e3a2 100644 --- a/app/Models/VirtualMachines.php +++ b/app/Models/VirtualMachines.php @@ -2,12 +2,13 @@ namespace App\Models; +use App\Traits\LogsActivity; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class VirtualMachines extends Model { - use HasFactory; + use HasFactory, LogsActivity; protected $fillable = [ 'uuid', diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 8815cb9..01b499e 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,12 +4,13 @@ namespace App\Providers; use App\Http\Helpers\Helper; use App\Http\Services\SignRSA; +use App\Models\ModuleIpAddress; +use App\Models\ModuleVmware; +use App\Observers\ModuleIpAddressObserver; +use App\Observers\ModuleVmwareObserver; +use App\Services\ActivityLogger; use App\Services\Cache\CacheService; use App\Services\Cache\CacheServiceInterface; -use App\Models\ModuleVmware; -use App\Models\ModuleIpAddress; -use App\Observers\ModuleVmwareObserver; -use App\Observers\ModuleIpAddressObserver; use Illuminate\Pagination\Paginator; use Illuminate\Support\Facades\Blade; use Illuminate\Support\ServiceProvider; @@ -24,6 +25,7 @@ class AppServiceProvider extends ServiceProvider public function register() { $this->app->singleton(CacheServiceInterface::class, CacheService::class); + $this->app->singleton(ActivityLogger::class); $this->app->bind('signrsa', function () { return new SignRSA(); diff --git a/app/Providers/RepositoryProvider.php b/app/Providers/RepositoryProvider.php index 59ad53a..663f1a5 100644 --- a/app/Providers/RepositoryProvider.php +++ b/app/Providers/RepositoryProvider.php @@ -106,6 +106,8 @@ use App\Repositories\DnsManager\Domain\DnsDomainRepository; use App\Repositories\DnsManager\Domain\DnsDomainRepositoryInterface; use App\Repositories\DnsManager\Server\DnsServerRepository as DnsManagerServerRepository; use App\Repositories\DnsManager\Server\DnsServerRepositoryInterface as DnsManagerServerRepositoryInterface; +use App\Repositories\ActivityLog\ActivityLogRepository; +use App\Repositories\ActivityLog\ActivityLogRepositoryInterface; use App\Repositories\DnsManager\Record\DnsRecordRepository; use App\Repositories\DnsManager\Record\DnsRecordRepositoryInterface; @@ -146,6 +148,8 @@ class RepositoryProvider extends ServiceProvider $this->app->singleton(InvoiceAttachmentRepositoryInterface::class, InvoiceAttachmentRepository::class); //Invoice Item $this->app->singleton(InvoiceItemRepositoryInterface::class, InvoiceItemCachedRepository::class); + //Activity Log + $this->app->singleton(ActivityLogRepositoryInterface::class, ActivityLogRepository::class); //Log $this->app->singleton(LogRepositoryInterface::class, LogCachedRepository::class); //Mail diff --git a/app/Repositories/ActivityLog/ActivityLogRepository.php b/app/Repositories/ActivityLog/ActivityLogRepository.php new file mode 100644 index 0000000..c7a5b46 --- /dev/null +++ b/app/Repositories/ActivityLog/ActivityLogRepository.php @@ -0,0 +1,66 @@ +model->newQuery(); + + if (!empty($filters['log_name'])) { + $query->where('log_name', $filters['log_name']); + } + + if (!empty($filters['event'])) { + $query->where('event', $filters['event']); + } + + if (!empty($filters['causer_type'])) { + $query->where('causer_type', $filters['causer_type']); + } + + if (!empty($filters['causer_id'])) { + $query->where('causer_id', $filters['causer_id']); + } + + if (!empty($filters['causer_name'])) { + $query->where('causer_name', 'like', '%' . $filters['causer_name'] . '%'); + } + + if (!empty($filters['subject_type'])) { + $query->where('subject_type', 'like', '%' . $filters['subject_type'] . '%'); + } + + if (!empty($filters['subject_id'])) { + $query->where('subject_id', $filters['subject_id']); + } + + if (!empty($filters['ip_address'])) { + $query->where('ip_address', $filters['ip_address']); + } + + if (!empty($filters['date_from'])) { + $query->where('created_at', '>=', $filters['date_from'] . ' 00:00:00'); + } + + if (!empty($filters['date_to'])) { + $query->where('created_at', '<=', $filters['date_to'] . ' 23:59:59'); + } + + if (!empty($filters['search'])) { + $query->where('description', 'like', '%' . $filters['search'] . '%'); + } + + return $query->latest('id')->paginate($perPage); + } +} diff --git a/app/Repositories/ActivityLog/ActivityLogRepositoryInterface.php b/app/Repositories/ActivityLog/ActivityLogRepositoryInterface.php new file mode 100644 index 0000000..268cc15 --- /dev/null +++ b/app/Repositories/ActivityLog/ActivityLogRepositoryInterface.php @@ -0,0 +1,11 @@ +resolveCauser(); + + ActivityLog::create([ + 'log_name' => $logName, + 'event' => $event, + 'description' => $description, + 'subject_type' => $subject ? get_class($subject) : null, + 'subject_id' => $subject?->getKey(), + 'causer_type' => $causerType, + 'causer_id' => $causerId, + 'causer_name' => $causerName, + 'old_values' => $oldValues ?: null, + 'new_values' => $newValues ?: null, + 'ip_address' => request()->getClientIp(), + 'user_agent' => request()->userAgent(), + ]); + } catch (\Throwable $e) { + Log::warning('[ActivityLogger] Failed to write log', [ + 'error' => $e->getMessage(), + 'event' => $event, + 'log_name' => $logName, + ]); + } + } + + public function logCreated(Model $model, string $logName): void + { + $causer = $this->getCauserName(); + $id = $model->getKey(); + + $this->log( + event: 'created', + logName: $logName, + description: "$causer tạo mới $logName" . ($id ? " #$id" : ''), + subject: $model, + newValues: $this->filterValues($model->getAttributes()), + ); + } + + public function logUpdated(Model $model, string $logName): void + { + $dirty = $model->getDirty(); + if (empty($dirty)) { + return; + } + + $dirty = $this->filterValues($dirty); + $original = $this->filterValues(array_intersect_key($model->getOriginal(), $dirty)); + + if (empty($dirty)) { + return; + } + + $causer = $this->getCauserName(); + $id = $model->getKey(); + $fields = implode(', ', array_keys($dirty)); + + $this->log( + event: 'updated', + logName: $logName, + description: "$causer cập nhật $logName" . ($id ? " #$id" : '') . " [$fields]", + subject: $model, + oldValues: $original, + newValues: $dirty, + ); + } + + public function logDeleted(Model $model, string $logName): void + { + $causer = $this->getCauserName(); + $id = $model->getOriginal($model->getKeyName()) ?? $model->getKey(); + + $this->log( + event: 'deleted', + logName: $logName, + description: "$causer xóa $logName" . ($id ? " #$id" : ''), + subject: $model, + ); + } + + public function logRestored(Model $model, string $logName): void + { + $causer = $this->getCauserName(); + $id = $model->getKey(); + + $this->log( + event: 'restored', + logName: $logName, + description: "$causer khôi phục $logName" . ($id ? " #$id" : ''), + subject: $model, + ); + } + + public function logAuth(string $event, string $causerType, $causerId, string $causerName): void + { + $action = match ($event) { + 'login' => 'đăng nhập', + 'logout' => 'đăng xuất', + 'register' => 'đăng ký tài khoản', + default => $event, + }; + + $this->log( + event: $event, + logName: 'auth', + description: "[$causerType] $causerName $action", + subject: null, + oldValues: [], + newValues: ['causer_type' => $causerType, 'causer_id' => $causerId, 'causer_name' => $causerName], + ); + } + + private function resolveCauser(): array + { + if ($admin = auth()->guard('admin')->user()) { + return ['admin', $admin->id, $admin->username ?? $admin->email]; + } + + if ($customer = auth()->guard('client')->user()) { + return ['customer', $customer->id, $customer->email ?? $customer->name]; + } + + return ['system', null, 'System']; + } + + public function getCauserName(): string + { + [, , $name] = $this->resolveCauser(); + return $name; + } + + private function filterValues(array $values): array + { + return array_diff_key($values, array_flip($this->hiddenFields)); + } +} diff --git a/app/Traits/LogsActivity.php b/app/Traits/LogsActivity.php new file mode 100644 index 0000000..ab9b308 --- /dev/null +++ b/app/Traits/LogsActivity.php @@ -0,0 +1,45 @@ +logCreated($model, $model->getLogName()); + }); + + static::updated(function ($model) { + app(ActivityLogger::class)->logUpdated($model, $model->getLogName()); + }); + + static::deleted(function ($model) { + app(ActivityLogger::class)->logDeleted($model, $model->getLogName()); + }); + + if (in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses_recursive(static::class))) { + static::restored(function ($model) { + app(ActivityLogger::class)->logRestored($model, $model->getLogName()); + }); + } + } +} diff --git a/database/migrations/2026_05_16_000001_create_activity_logs_table.php b/database/migrations/2026_05_16_000001_create_activity_logs_table.php new file mode 100644 index 0000000..be0ada7 --- /dev/null +++ b/database/migrations/2026_05_16_000001_create_activity_logs_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('log_name', 100)->index(); + $table->string('event', 30)->index(); + $table->text('description'); + $table->string('subject_type', 150)->nullable()->index(); + $table->string('subject_id', 100)->nullable()->index(); + $table->string('causer_type', 20)->nullable()->index(); // admin | customer | system + $table->unsignedBigInteger('causer_id')->nullable()->index(); + $table->string('causer_name', 150)->nullable(); + $table->json('old_values')->nullable(); + $table->json('new_values')->nullable(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->timestamp('created_at')->useCurrent()->index(); + }); + } + + public function down(): void + { + Schema::dropIfExists('activity_logs'); + } +}; diff --git a/memory/MEMORY.md b/memory/MEMORY.md new file mode 100644 index 0000000..f673dca --- /dev/null +++ b/memory/MEMORY.md @@ -0,0 +1,3 @@ +# Memory Index + +- [project_activity_log.md](project_activity_log.md) — Hệ thống activity log toàn diện đã implement: bảng activity_logs, trait LogsActivity, service ActivityLogger, API admin diff --git a/memory/project_activity_log.md b/memory/project_activity_log.md new file mode 100644 index 0000000..3f2de01 --- /dev/null +++ b/memory/project_activity_log.md @@ -0,0 +1,39 @@ +--- +name: Activity Log System +description: Hệ thống ghi log toàn diện mọi hành động trên portal - đã implement 2026-05-16 +type: project +--- + +Đã implement comprehensive activity log system ghi tất cả hành động create/update/delete/restore/login/logout cho cả admin và customer. + +**Files đã tạo:** +- `database/migrations/2026_05_16_000001_create_activity_logs_table.php` +- `app/Models/ActivityLog.php` +- `app/Services/ActivityLogger.php` (singleton service) +- `app/Traits/LogsActivity.php` (trait gắn vào Eloquent model) +- `app/Repositories/ActivityLog/ActivityLogRepositoryInterface.php` +- `app/Repositories/ActivityLog/ActivityLogRepository.php` +- `app/Http/Controllers/Api/Admin/ActivityLog/ActivityLogController.php` + +**Models đã gắn trait LogsActivity:** Customer, Order, Invoice, Transaction, Service, Payment, VirtualMachines, ModuleVmware, ModuleHosting, ModuleDomain, IpPool, ModuleSnapshot, DnsDomain, DnsRecord, DnsServer, ModuleIpAddress, Product, Bank, User, Ticket, Addon, Discount + +**Auth logging:** Admin AuthController + Client ClientController đã log login/logout/login_failed/register + +**Customer endpoint logging (explicit calls, ngoài trait):** +- `ClientController`: register, login, logout +- `OrderController`: storeOrder, payOrder, retryOrderProvisioning, updateClientServiceAutoRenew, renewService +- `VmController`: powerOn, powerOff, changeIp, changePort, restart, changePassword +- `SnapshotController`: store, revert, destroy +- `VmRebuildController`: rebuild, upgradeAddons +- `ManualDepositController`: prepare, cancel, confirm +- `TransactionController`: handle (tạo VNPay), returnCallback (VNPay success) + +**API Endpoints (admin):** +- `GET /api/v1/administrator/activity-logs` — danh sách log với filter +- `GET /api/v1/administrator/activity-logs/modules` — danh sách module names +- `GET /api/v1/administrator/activity-logs/{id}` — chi tiết 1 log + +**Filter params:** log_name, event, causer_type (admin/customer/system), causer_id, causer_name, subject_type, subject_id, ip_address, date_from, date_to, search, per_page + +**Why:** User yêu cầu log tất cả hành động từ admin và customer để audit. +**How to apply:** Khi thêm model mới, thêm `use LogsActivity;` vào model. Service ActivityLogger được resolve qua IoC container. diff --git a/routes/api.php b/routes/api.php index aa7a896..e828690 100644 --- a/routes/api.php +++ b/routes/api.php @@ -23,6 +23,7 @@ use App\Http\Controllers\Api\Admin\OsManager\OsController; use App\Http\Controllers\Api\Admin\IpPool\IpPoolController; use App\Http\Controllers\Api\Admin\Bank\BankController; use App\Http\Controllers\Api\Admin\EmailTemplate\EmailTemplateController; +use App\Http\Controllers\Api\Admin\ActivityLog\ActivityLogController; use App\Http\Controllers\Api\Admin\Transaction\AdminTransactionController; use App\Http\Controllers\Api\Admin\InfraManagement\InfraManagementController; use App\Http\Services\Media; @@ -390,6 +391,16 @@ Route::prefix('v1') Route::post('vm/snapshot', 'snapshot')->middleware('perm:module-vcenter.edit'); Route::get('vms/powered-off', 'getPoweredOffVms')->middleware('perm:module-vcenter.view'); }); + + // Activity Logs + Route::prefix('activity-logs') + ->controller(ActivityLogController::class) + ->group(function () { + Route::get('/', 'index'); + Route::get('modules', 'modules'); + Route::get('stats', 'stats'); + Route::get('{id}', 'show'); + }); }); }); diff --git a/storage/app/public/deposit-proofs/HlJO6zk5k3HuBhqmluQT57LrAgSc5BdM7hmTJAlp.png b/storage/app/public/deposit-proofs/HlJO6zk5k3HuBhqmluQT57LrAgSc5BdM7hmTJAlp.png new file mode 100644 index 0000000..9325504 Binary files /dev/null and b/storage/app/public/deposit-proofs/HlJO6zk5k3HuBhqmluQT57LrAgSc5BdM7hmTJAlp.png differ