feat: implement system-wide activity logging with database persistence, repository patterns, and admin controller access.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\ActivityLog;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Repositories\ActivityLog\ActivityLogRepositoryInterface;
|
||||
use App\Support\Cache\CacheKey;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ActivityLogController extends Controller
|
||||
{
|
||||
private const CACHE_TTL = 60; // seconds
|
||||
|
||||
public function __construct(
|
||||
private readonly ActivityLogRepositoryInterface $activityLogRepository,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$perPage = min((int) $request->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]);
|
||||
}
|
||||
}
|
||||
@@ -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([
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ActivityLog extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $table = 'activity_logs';
|
||||
|
||||
protected $fillable = [
|
||||
'log_name',
|
||||
'event',
|
||||
'description',
|
||||
'subject_type',
|
||||
'subject_id',
|
||||
'causer_type',
|
||||
'causer_id',
|
||||
'causer_name',
|
||||
'old_values',
|
||||
'new_values',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'old_values' => '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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
+2
-1
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
+2
-1
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\ActivityLog;
|
||||
|
||||
use App\Models\ActivityLog;
|
||||
use App\Repositories\BaseRepository;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
class ActivityLogRepository extends BaseRepository implements ActivityLogRepositoryInterface
|
||||
{
|
||||
public function getModel(): string
|
||||
{
|
||||
return ActivityLog::class;
|
||||
}
|
||||
|
||||
public function filter(array $filters, int $perPage = 20): LengthAwarePaginator
|
||||
{
|
||||
$query = $this->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\ActivityLog;
|
||||
|
||||
use App\Repositories\BaseRepositoryInterface;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
interface ActivityLogRepositoryInterface extends BaseRepositoryInterface
|
||||
{
|
||||
public function filter(array $filters, int $perPage = 20): LengthAwarePaginator;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ActivityLog;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ActivityLogger
|
||||
{
|
||||
private array $hiddenFields = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'plain_password',
|
||||
'token',
|
||||
'secret',
|
||||
'api_key',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
public function log(
|
||||
string $event,
|
||||
string $logName,
|
||||
string $description,
|
||||
?Model $subject = null,
|
||||
array $oldValues = [],
|
||||
array $newValues = [],
|
||||
): void {
|
||||
try {
|
||||
[$causerType, $causerId, $causerName] = $this->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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Services\ActivityLogger;
|
||||
|
||||
trait LogsActivity
|
||||
{
|
||||
/**
|
||||
* Tên module hiển thị trong log. Override nếu muốn tên khác.
|
||||
*/
|
||||
protected function getLogName(): string
|
||||
{
|
||||
return class_basename(static::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Các field không ghi vào log. Override để thêm field riêng của model.
|
||||
*/
|
||||
protected function getLogHiddenFields(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function bootLogsActivity(): void
|
||||
{
|
||||
static::created(function ($model) {
|
||||
app(ActivityLogger::class)->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());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('activity_logs', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 870 KiB |
Reference in New Issue
Block a user