Skip to content
prod e051e98
Browse

3 · Activity logging

Objective — give security-relevant events a durable audit trail so who did what, when always has an answer: install Spatie Activity Log, capture model changes, log auth and GDPR actions, and auto-prune old records on a schedule.

When something goes wrong — a permission change, a mass deletion, a suspicious login — the first question is who did what, when. Activity logging answers it. Three sources feed one activity_log table; a scheduled job keeps it bounded.

flowchart LR
M["Model changes<br/>(LogsActivity trait)"] --> L[("activity_log table")]
A["Auth events<br/>login · logout · failed"] --> L
G["GDPR actions<br/>export · deletion request"] --> L
L --> C["activitylog:clean<br/>daily · 90-day retention"]

1. Audit, then install Spatie Activity Log

Section titled “1. Audit, then install Spatie Activity Log”

A CodeCanyon app may already ship its own logging. Check first — installing Spatie on top of a built-in logger just creates duplicate, conflicting trails.

  1. Check for existing logging before you install.

    Terminal window
    composer show | grep spatie/laravel-activitylog
    php artisan tinker --execute="echo Schema::hasTable('activity_log') ? 'EXISTS' : 'NONE';"
    grep -r "LogsActivity" app/Models/
    # Expected: shows whether Spatie, the activity_log table, or LogsActivity already exist
    • ✅ You know which of the three states you’re in.
    FindingAction
    No logging existsFull install (substep 2)
    Spatie installed, not configuredSkip to section 2
    Vendor has built-in loggingHybrid — add Spatie only for the gaps
  2. Install and configure Spatie (only when no logger exists).

    Terminal window
    composer require spatie/laravel-activitylog
    php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-config"
    php artisan migrate
    # Expected: package installs, config publishes, activity_log table is created

    In config/activitylog.php, gate it behind an env flag and set retention, then add ACTIVITY_LOG_ENABLED=true to .env:

    'enabled' => env('ACTIVITY_LOG_ENABLED', true),
    'delete_records_older_than_days' => 90,
    • ✅ Spatie is installed, env-gated, and migrated.

Add the LogsActivity trait to the models that matter for an audit — User, Invoice, Subscription, Settings. Log only the fields you care about, and only when they actually change.

  1. Add the LogsActivity trait with scoped, dirty-only options.

    app/Models/User.php
    use Spatie\Activitylog\Traits\LogsActivity;
    use Spatie\Activitylog\LogOptions;
    class User extends Authenticatable
    {
    use LogsActivity;
    public function getActivitylogOptions(): LogOptions
    {
    return LogOptions::defaults()
    ->logOnly(['name', 'email', 'role', 'status'])
    ->logOnlyDirty()
    ->dontSubmitEmptyLogs();
    }
    }
    • ✅ Key models log only the selected fields, only when they change.

Create listeners for Laravel’s auth events and capture IP + user agent — the context you need to spot credential stuffing or session hijacking.

  1. Write a login listener that records the user, IP, and user agent.

    app/Listeners/LogSuccessfulLogin.php
    activity('auth')
    ->causedBy($event->user)
    ->withProperties(['ip' => request()->ip(), 'user_agent' => request()->userAgent()])
    ->log('User logged in');
    • ✅ Login activity records the actor + request context.
  2. Write a failed-login listenerFailed events have no $event->user; log the attempted email instead.

    app/Listeners/LogFailedLogin.php
    activity('auth')
    ->withProperties([
    'email' => $event->credentials['email'] ?? 'unknown',
    'ip' => request()->ip(),
    'user_agent' => request()->userAgent(),
    ])
    ->log('Failed login attempt');
    • ✅ Failed-login activity captures email + request context (not causedBy($event->user)).
  3. Register the listeners in EventServiceProvider.

    protected $listen = [
    \Illuminate\Auth\Events\Login::class => [LogSuccessfulLogin::class],
    \Illuminate\Auth\Events\Failed::class => [LogFailedLogin::class],
    \Illuminate\Auth\Events\Logout::class => [LogLogout::class],
    ];
    • ✅ Login, failed-login, and logout each fire their listener.

Data export and account deletion are exactly the events a regulator (or a dispute) will ask about. Log deletion before the data is gone.

  1. Log export and deletion-request events (deletion logged before the data is removed).

    // AccountController::exportData()
    activity('gdpr')->causedBy(auth()->user())->log('User exported personal data');
    // AccountController::deleteAccount() — log BEFORE deletion runs
    activity('gdpr')->causedBy($user)->log('User requested account deletion');
    • ✅ Both GDPR actions land in the log, with deletion logged pre-removal.

These wire directly into the GDPR endpoints built in Legal, privacy & GDPR.

In app/Console/Kernel.php, clean the log nightly so it never balloons.

  1. Schedule the nightly clean.

    $schedule->command('activitylog:clean')->dailyAt('04:00');
    • activitylog:clean is scheduled daily.
  2. Confirm the server cron drives the scheduler — install once per server in Phase 5 · Secure & verify schema (idempotent grep -v 'schedule:run' pattern). Here, only verify:

    Terminal window
    ssh <deploy-alias> 'crontab -l 2>/dev/null | grep -c "schedule:run"' # must print exactly 1
    # Expected: 1 — if 0, go back to Phase 5; if 2+, dedupe with the Phase 5 idempotent install
    • ✅ Exactly one schedule:run line is present on the server cron.

Confirm entries are actually landing in the table.

  1. Inspect the most recent entries in tinker.

    Terminal window
    php artisan tinker
    >>> Activity::latest()->first(); // shows the most recent entry
    >>> Activity::where('log_name', 'auth')->count(); // auth events recorded
    # Expected: a recent Activity row, and a non-zero auth count
    • ✅ The latest activity row returns and the auth count is non-zero.

Do not mark this step done until every box below is checked.

  • 🤖 Activity log installed — Spatie installed (or vendor logger confirmed); ACTIVITY_LOG_ENABLED=true.
  • 🤖 Model changes loggedLogsActivity on key models, logging only-dirty selected fields.
  • 🤖 Auth events logged — login / logout / failed-login listeners registered, capturing IP + user agent.
  • 🤖 GDPR actions logged — export + deletion-request recorded (deletion logged before data removal).
  • 🤖 Retention scheduledactivitylog:clean daily; server cron drives schedule:run.