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.
This commit is contained in:
Andras Bacsai
2026-08-24 10:42:04 +02:00
parent 57a322ba11
commit 2289de4489
4 changed files with 53 additions and 6 deletions
@@ -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,
+15 -1
View File
@@ -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) {
@@ -37,7 +37,7 @@
<div class="overflow-x-auto">
<div class="data-table min-w-[760px] transition-opacity" wire:loading.class="opacity-50 pointer-events-none"
wire:target="search,action,source,setPage,previousPage,nextPage">
<div class="grid grid-cols-[10rem_minmax(0,1fr)_12rem_9rem] gap-4 border-b border-neutral-200 px-4 py-2 text-[11px] font-medium uppercase tracking-wide text-neutral-500 dark:border-white/[0.07] dark:text-fg-faint">
<div class="grid grid-cols-[14rem_minmax(0,1fr)_12rem_9rem] gap-4 border-b border-neutral-200 px-4 py-2 text-[11px] font-medium uppercase tracking-wide text-neutral-500 dark:border-white/[0.07] dark:text-fg-faint">
<span>Actor</span>
<span>Activity</span>
<span>Source</span>
@@ -45,7 +45,7 @@
</div>
@foreach ($events as $event)
<div wire:key="audit-event-{{ $event->id }}"
class="grid grid-cols-[10rem_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]">
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]">
<div class="min-w-0">
<div class="truncate text-[12px] font-medium text-black dark:text-fg">
{{ $event->actor_name ?: Str::headline($event->actor_type) }}
@@ -56,7 +56,8 @@
</div>
@endif
@if ($event->actor_token_name)
<div class="truncate text-[11px] text-neutral-500 dark:text-fg-faint">
<div class="truncate text-[11px] text-neutral-500 dark:text-fg-faint"
title="Token: {{ $event->actor_token_name }}">
Token: {{ $event->actor_token_name }}
</div>
@endif
@@ -77,7 +78,7 @@
</div>
<time datetime="{{ $event->created_at->toIso8601String() }}"
title="{{ $event->created_at->toDayDateTimeString() }}"
class="text-right text-[11px] text-neutral-500 dark:text-fg-faint">
class="self-center text-right text-[11px] text-neutral-500 dark:text-fg-faint">
{{ $event->created_at->diffForHumans() }}
</time>
</div>
+32
View File
@@ -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');
});