From 2289de4489acef4a009f07ffc9e6ef62e579f2a6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:42:04 +0200 Subject: [PATCH] fix(audit): prevent duplicate API update events Suppress model audit logging during API application saves while preserving the explicit event, and improve audit log actor display spacing and token tooltips. --- .../Api/ApplicationsController.php | 2 +- app/Traits/Auditable.php | 16 +++++++++- .../views/livewire/team/audit-log.blade.php | 9 +++--- tests/Feature/AuditEventsTest.php | 32 +++++++++++++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index b47db0e26f..aa5bceebb4 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -3122,7 +3122,7 @@ class ApplicationsController extends Controller if ($application->settings->is_container_label_readonly_enabled && ($requestHasDomains || $requestHasNoindexDomains || $requestHasHttpBasicAuth) && $server->isProxyShouldRun()) { $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); } - $application->save(); + $application->withoutAuditLogging(fn () => $application->save()); auditLog('api.application.updated', [ 'team_id' => $teamId, diff --git a/app/Traits/Auditable.php b/app/Traits/Auditable.php index f0873be377..0b065db44e 100644 --- a/app/Traits/Auditable.php +++ b/app/Traits/Auditable.php @@ -4,11 +4,14 @@ namespace App\Traits; use App\Models\PersonalAccessToken; use App\Models\Team; +use Closure; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; trait Auditable { + private bool $auditLoggingEnabled = true; + public static function bootAuditable(): void { static::created(fn (Model $model) => $model->recordAuditMutation('created')); @@ -18,7 +21,7 @@ trait Auditable private function recordAuditMutation(string $action): void { - if (! auth()->check()) { + if (! $this->auditLoggingEnabled || ! auth()->check()) { return; } @@ -54,6 +57,17 @@ trait Auditable ]); } + public function withoutAuditLogging(Closure $callback): mixed + { + $this->auditLoggingEnabled = false; + + try { + return $callback(); + } finally { + $this->auditLoggingEnabled = true; + } + } + private function auditTeamId(): ?int { if ($this instanceof Team) { diff --git a/resources/views/livewire/team/audit-log.blade.php b/resources/views/livewire/team/audit-log.blade.php index d561e586a6..64bd1fa75c 100644 --- a/resources/views/livewire/team/audit-log.blade.php +++ b/resources/views/livewire/team/audit-log.blade.php @@ -37,7 +37,7 @@
-
+
Actor Activity Source @@ -45,7 +45,7 @@
@foreach ($events as $event)
+ class="grid grid-cols-[14rem_minmax(0,1fr)_12rem_9rem] gap-4 border-b border-neutral-200 px-4 py-3 last:border-b-0 dark:border-white/[0.07]">
{{ $event->actor_name ?: Str::headline($event->actor_type) }} @@ -56,7 +56,8 @@
@endif @if ($event->actor_token_name) -
+
Token: {{ $event->actor_token_name }}
@endif @@ -77,7 +78,7 @@
diff --git a/tests/Feature/AuditEventsTest.php b/tests/Feature/AuditEventsTest.php index 923d36c0e6..ebe7d8f94d 100644 --- a/tests/Feature/AuditEventsTest.php +++ b/tests/Feature/AuditEventsTest.php @@ -19,6 +19,7 @@ use App\Models\Server; use App\Models\Service; use App\Models\SharedEnvironmentVariable; use App\Models\StandaloneClickhouse; +use App\Models\StandaloneDocker; use App\Models\StandaloneDragonfly; use App\Models\StandaloneKeydb; use App\Models\StandaloneMariadb; @@ -412,6 +413,33 @@ test('API model mutations produce one audit event', function () { ->count())->toBe(1); }); +test('API application updates produce one audit event', function () { + $server = Server::factory()->create(['team_id' => $this->team->id]); + $destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail(); + $project = Project::factory()->create(['team_id' => $this->team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $application = Application::factory()->create([ + 'environment_id' => $environment->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]); + AuditEvent::query()->delete(); + + $token = $this->user->createToken('audit-api', ['root']); + $token->accessToken->forceFill(['team_id' => $this->team->id])->save(); + auth()->logout(); + auth()->forgetGuards(); + + $this->withToken($token->plainTextToken) + ->patchJson("/api/v1/applications/{$application->uuid}", ['description' => 'Updated through API']) + ->assertOk(); + + expect(AuditEvent::query() + ->where('event', 'api.application.updated') + ->where('resource_uuid', $application->uuid) + ->count())->toBe(1); +}); + test('deployment queue records rollback and cancellation operations', function () { $project = Project::factory()->create(['team_id' => $this->team->id]); $environment = Environment::factory()->create(['project_id' => $project->id]); @@ -778,11 +806,15 @@ test('audit log table keeps actor details visible in a mobile scroll area', func AuditEvent::factory()->create([ 'team_id' => $this->team->id, 'actor_name' => 'Visible Actor', + 'actor_token_name' => 'visible-audit-token', ]); Livewire::test(AuditLog::class) ->assertSeeHtml('class="overflow-x-auto"') ->assertSeeHtml('min-w-[760px]') + ->assertSeeHtml('grid-cols-[14rem_minmax(0,1fr)_12rem_9rem]') + ->assertSeeHtml('class="self-center text-right text-[11px]') + ->assertSeeHtml('title="Token: visible-audit-token"') ->assertSee('Actor') ->assertSee('Visible Actor'); });