Keep an audit log of who did what, and how they came in

Written from model events, spatie's role events and panel sign-ins, only for
what a person did, with secrets masked; pruned after a year.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 10:08:01 +02:00
parent 9bdbf834f6
commit 1fe1e67773
18 changed files with 744 additions and 1 deletions

View File

@@ -8,10 +8,13 @@ use App\Auth\PireonTokenGuard;
use App\Models\Application;
use App\Models\ReviewComment;
use App\Models\Revision;
use App\Models\User;
use App\Services\Git\GitManager;
use App\Services\Pireon\PireonClient;
use App\Services\Pireon\TokenStore;
use App\Support\Audit\Auditor;
use App\Support\Webhooks\Webhooks;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Database\Eloquent\Model;
@@ -22,6 +25,9 @@ use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Spatie\Permission\Events\RoleAttached;
use Spatie\Permission\Events\RoleDetached;
use Spatie\Permission\Models\Role;
class AppServiceProvider extends ServiceProvider
{
@@ -38,6 +44,19 @@ class AppServiceProvider extends ServiceProvider
$this->app->singleton(GitManager::class);
}
/**
* @return array<int, string>
*/
protected static function roleNames(mixed $rolesOrIds): array
{
return Role::query()
->whereIn('id', collect(is_iterable($rolesOrIds) ? $rolesOrIds : [$rolesOrIds])
->map(fn ($role) => $role instanceof Role ? $role->id : $role)
->all())
->pluck('name')
->all();
}
public function boot(): void
{
// Outgoing webhooks, fed by the records themselves so the panel, the
@@ -46,6 +65,24 @@ class AppServiceProvider extends ServiceProvider
Revision::created(fn (Revision $revision) => Webhooks::revisionRecorded($revision));
ReviewComment::created(fn (ReviewComment $comment) => Webhooks::reviewRecorded($comment));
// The audit log: every change a person makes to an audited record,
// role changes, and signing in to the panel.
foreach (['created', 'updated', 'deleted', 'restored'] as $event) {
Event::listen("eloquent.{$event}: *", function (string $name, array $payload) use ($event): void {
if (($payload[0] ?? null) instanceof Model) {
Auditor::modelEvent($event, $payload[0]);
}
});
}
Event::listen(RoleAttached::class, fn (RoleAttached $e) => Auditor::record('users.roles_added', $e->model, ['roles' => self::roleNames($e->rolesOrIds)]));
Event::listen(RoleDetached::class, fn (RoleDetached $e) => Auditor::record('users.roles_removed', $e->model, ['roles' => self::roleNames($e->rolesOrIds)]));
Event::listen(Login::class, function (Login $e): void {
if ($e->user instanceof User) {
Auditor::record('auth.login', $e->user, null, $e->user);
}
});
// Backs the "api" guard declared in config/auth.php.
Auth::viaRequest('pireon', fn (Request $request) => $this->app
->make(PireonTokenGuard::class)($request));