Skip to content

Commit 46db117

Browse files
committed
feat(core): Phase 93 — Audit Log & Activity Tracking
https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent fc7fb8c commit 46db117

11 files changed

Lines changed: 423 additions & 117 deletions

File tree

erp/app/Modules/Core/Http/Controllers/AuditLogController.php

Lines changed: 17 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,36 @@
22

33
namespace App\Modules\Core\Http\Controllers;
44

5+
use App\Http\Controllers\Controller;
56
use App\Modules\Core\Models\AuditLog;
67
use Illuminate\Http\Request;
7-
use Illuminate\Routing\Controller;
88
use Inertia\Inertia;
99
use Inertia\Response;
1010

1111
class AuditLogController extends Controller
1212
{
1313
public function index(Request $request): Response
1414
{
15-
// Only super-admin can view the cross-tenant audit log
16-
if (! $request->user()->hasRole('super-admin')) {
17-
abort(403);
18-
}
15+
$this->authorize('viewAny', AuditLog::class);
1916

20-
$query = AuditLog::with('user')
21-
->orderByDesc('created_at');
22-
23-
if ($event = $request->query('event')) {
24-
$query->where('event', $event);
25-
}
26-
27-
if ($userId = $request->query('user_id')) {
28-
$query->where('user_id', $userId);
29-
}
30-
31-
if ($auditableType = $request->query('auditable_type')) {
32-
$query->where('auditable_type', 'like', "%{$auditableType}%");
33-
}
34-
35-
if ($tenantId = $request->query('tenant_id')) {
36-
$query->where('tenant_id', $tenantId);
37-
}
38-
39-
if ($dateFrom = $request->query('date_from')) {
40-
$query->whereDate('created_at', '>=', $dateFrom);
41-
}
42-
43-
if ($dateTo = $request->query('date_to')) {
44-
$query->whereDate('created_at', '<=', $dateTo);
45-
}
46-
47-
$logs = $query->paginate(30)->withQueryString();
48-
49-
$filters = $request->only([
50-
'event',
51-
'user_id',
52-
'auditable_type',
53-
'tenant_id',
54-
'date_from',
55-
'date_to',
56-
]);
17+
$logs = AuditLog::with('user')
18+
->when($request->action, fn ($q) => $q->where('action', $request->action))
19+
->when($request->module, fn ($q) => $q->where('module', $request->module))
20+
->when($request->user_id, fn ($q) => $q->where('user_id', $request->user_id))
21+
->latest('created_at')
22+
->paginate(50)
23+
->withQueryString();
5724

5825
return Inertia::render('Core/AuditLogs/Index', [
5926
'logs' => $logs,
60-
'filters' => $filters,
27+
'filters' => $request->only(['action', 'module', 'user_id']),
6128
]);
6229
}
30+
31+
public function show(AuditLog $auditLog): Response
32+
{
33+
$this->authorize('view', $auditLog);
34+
$auditLog->load('user');
35+
return Inertia::render('Core/AuditLogs/Show', ['log' => $auditLog]);
36+
}
6337
}

erp/app/Modules/Core/Models/AuditLog.php

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ class AuditLog extends Model
1616
'user_id',
1717
'tenant_id',
1818
'event',
19+
'action',
1920
'auditable_type',
2021
'auditable_id',
22+
'auditable_label',
2123
'old_values',
2224
'new_values',
2325
'ip_address',
2426
'user_agent',
27+
'url',
28+
'module',
2529
'created_at',
2630
];
2731

@@ -31,6 +35,8 @@ class AuditLog extends Model
3135
'created_at' => 'datetime',
3236
];
3337

38+
protected $dates = ['created_at'];
39+
3440
public function auditable(): MorphTo
3541
{
3642
return $this->morphTo();
@@ -49,31 +55,73 @@ public function tenant(): BelongsTo
4955
/**
5056
* Record an audit log entry.
5157
*
52-
* @param string $event
53-
* @param Model|null $auditable
54-
* @param array $oldValues
55-
* @param array $newValues
56-
* @param int|null $tenantId
58+
* Supports two call styles:
59+
* record(string $action, $model, array $old, array $new, string $module) -- new style
60+
* record(string $event, $model, array $old, array $new, int $tenantId) -- legacy style
61+
*
62+
* @param string $action
63+
* @param Model|null $model
64+
* @param array $oldValues
65+
* @param array $newValues
66+
* @param string|int|null $moduleOrTenantId
5767
* @return static
5868
*/
5969
public static function record(
60-
string $event,
61-
?Model $auditable = null,
70+
string $action,
71+
$model = null,
6272
array $oldValues = [],
6373
array $newValues = [],
64-
?int $tenantId = null
74+
$moduleOrTenantId = null
6575
): static {
76+
$tenantId = null;
77+
$module = '';
78+
79+
if (is_int($moduleOrTenantId)) {
80+
$tenantId = $moduleOrTenantId;
81+
} elseif (is_string($moduleOrTenantId)) {
82+
$module = $moduleOrTenantId;
83+
}
84+
85+
if ($tenantId === null) {
86+
$tenantId = auth()->user()?->tenant_id ?? 0;
87+
}
88+
6689
return static::create([
67-
'user_id' => auth()->id(),
68-
'tenant_id' => $tenantId,
69-
'event' => $event,
70-
'auditable_type' => $auditable ? get_class($auditable) : null,
71-
'auditable_id' => $auditable?->getKey(),
72-
'old_values' => $oldValues ?: null,
73-
'new_values' => $newValues ?: null,
74-
'ip_address' => request()->ip(),
75-
'user_agent' => request()->userAgent(),
76-
'created_at' => now(),
90+
'tenant_id' => $tenantId,
91+
'user_id' => auth()->id(),
92+
'event' => $action,
93+
'action' => $action,
94+
'auditable_type' => $model ? get_class($model) : null,
95+
'auditable_id' => $model?->getKey(),
96+
'auditable_label' => $model?->name ?? $model?->title ?? $model?->subject ?? null,
97+
'old_values' => $oldValues ?: null,
98+
'new_values' => $newValues ?: null,
99+
'ip_address' => request()?->ip(),
100+
'user_agent' => request()?->userAgent(),
101+
'url' => request()?->fullUrl(),
102+
'module' => $module,
103+
'created_at' => now(),
77104
]);
78105
}
106+
107+
/**
108+
* Get a human-readable summary of changes.
109+
*/
110+
public function getChangeSummaryAttribute(): string
111+
{
112+
if ($this->old_values && $this->new_values) {
113+
$changedKeys = array_keys(array_diff_assoc(
114+
(array) $this->new_values,
115+
(array) $this->old_values
116+
));
117+
118+
if (empty($changedKeys)) {
119+
$changedKeys = array_keys((array) $this->new_values);
120+
}
121+
122+
return implode(', ', $changedKeys) . ' changed';
123+
}
124+
125+
return $this->action ?? $this->event ?? '';
126+
}
79127
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace App\Modules\Core\Policies;
4+
5+
use App\Models\User;
6+
7+
class AuditLogPolicy
8+
{
9+
public function viewAny(User $user): bool
10+
{
11+
return $user->hasRole(['super-admin', 'admin']);
12+
}
13+
14+
public function view(User $user, $model): bool
15+
{
16+
return $user->hasRole(['super-admin', 'admin']);
17+
}
18+
}

erp/app/Modules/Core/Providers/CoreServiceProvider.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
namespace App\Modules\Core\Providers;
44

5+
use App\Modules\Core\Models\AuditLog;
6+
use App\Modules\Core\Policies\AuditLogPolicy;
57
use App\Modules\Finance\Providers\FinanceServiceProvider;
68
use App\Modules\HR\Providers\HRServiceProvider;
79
use App\Modules\Inventory\Providers\InventoryServiceProvider;
10+
use Illuminate\Support\Facades\Gate;
811
use Illuminate\Support\ServiceProvider;
912

1013
class CoreServiceProvider extends ServiceProvider
@@ -19,5 +22,6 @@ public function register(): void
1922
public function boot(): void
2023
{
2124
$this->loadRoutesFrom(__DIR__ . '/../routes/core.php');
25+
Gate::policy(AuditLog::class, AuditLogPolicy::class);
2226
}
2327
}

erp/app/Modules/Core/routes/core.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

3-
use App\Http\Controllers\Admin\AuditLogController;
3+
use App\Modules\Core\Http\Controllers\AuditLogController;
4+
use App\Http\Controllers\Admin\AuditLogController as AdminAuditLogController;
45
use App\Http\Controllers\Admin\UserController;
56
use App\Http\Controllers\AnalyticsController;
67
use App\Http\Controllers\DashboardController;
@@ -33,6 +34,10 @@
3334

3435
Route::prefix('admin')->name('admin.')->group(function () {
3536
Route::resource('users', UserController::class)->names('users');
36-
Route::get('audit-log', [AuditLogController::class, 'index'])->name('audit-log.index');
37+
Route::get('audit-log', [AdminAuditLogController::class, 'index'])->name('audit-log.index');
38+
});
39+
40+
Route::middleware(['web', 'auth', 'verified'])->prefix('core')->name('core.')->group(function () {
41+
Route::resource('audit-logs', AuditLogController::class)->only(['index', 'show']);
3742
});
3843
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up(): void
10+
{
11+
Schema::table('audit_logs', function (Blueprint $table) {
12+
$table->string('action')->nullable()->after('event');
13+
$table->string('auditable_label')->nullable()->after('auditable_id');
14+
$table->string('url')->nullable()->after('user_agent');
15+
$table->string('module')->nullable()->after('url');
16+
});
17+
}
18+
19+
public function down(): void
20+
{
21+
Schema::table('audit_logs', function (Blueprint $table) {
22+
$table->dropColumn(['action', 'auditable_label', 'url', 'module']);
23+
});
24+
}
25+
};

erp/resources/js/Components/Layout/Sidebar.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,19 @@ const navItems: NavItem[] = [
183183
href: '/analytics',
184184
icon: analyticsIcon,
185185
},
186+
{
187+
label: 'System',
188+
href: '/core/audit-logs',
189+
icon: (
190+
<svg className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={1.75} viewBox="0 0 24 24">
191+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
192+
</svg>
193+
),
194+
permission: 'finance.view',
195+
children: [
196+
{ label: 'Audit Log', href: '/core/audit-logs', icon: <span /> },
197+
],
198+
},
186199
{
187200
label: 'Admin',
188201
href: '/admin/users',

0 commit comments

Comments
 (0)