feat: rework logging

This commit is contained in:
Nicholas Ciechanowski 2023-10-11 22:28:14 +11:00
parent 87bdbeee05
commit 2d5991988d
15 changed files with 204 additions and 95 deletions

22
app/Enums/LogAction.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace App\Enums;
use App\Traits\EnumOptions;
use App\Traits\EnumValues;
enum LogAction: string
{
use EnumValues;
use EnumOptions;
case CREATE = 'create';
case DELETE = 'delete';
case REQUEST = 'request';
case REJECT = 'reject';
case APPROVE = 'approve';
case SEND = 'send';
case UPDATE = 'update';
case ACCESS = 'access';
}

View File

@ -2,13 +2,13 @@
namespace App\Http\Controllers\Api;
use App\Enums\LogAction;
use App\Http\Controllers\Controller;
use App\Models\Log;
use App\Models\Quote;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class WebHookController extends Controller
{
public function webHookSend(string $payload)
@ -27,8 +27,12 @@ class WebHookController extends Controller
}
Log::create([
'user_id' => auth()?->user()?->id,
'content' => "Quote sent. {$quote}",
'user_id' => auth()?->user()?->id ?? 1,
'loggable_type' => Quote::class,
'loggable_id' => null,
'action' => LogAction::SEND,
'content' => $quote,
'ip' => request()->ip(),
]);
$this->webHookSend($quote);
@ -39,9 +43,12 @@ class WebHookController extends Controller
$quote = Quote::inRandomOrder()->first();
Log::create([
'user_id' => auth()?->user()?->id,
'quote_id' => $quote->id,
'content' => 'Quote sent.',
'user_id' => auth()?->user()?->id ?? 1,
'loggable_type' => Quote::class,
'loggable_id' => $quote->id,
'action' => LogAction::REQUEST,
'content' => 'Random quote requested',
'ip' => request()->ip(),
]);
$this->webHookSend($quote->quote);

View File

@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* @mixin IdeHelperLog
@ -12,9 +13,11 @@ class Log extends Model
{
protected $fillable = [
'user_id',
'quote_id',
'requested_quote_id',
'loggable_type',
'loggable_id',
'action',
'content',
'ip',
];
public function user(): BelongsTo
@ -22,13 +25,11 @@ class Log extends Model
return $this->belongsTo(User::class);
}
public function quote(): BelongsTo
/**
* Get the parent loggable model (user, quote or requested quote).
*/
public function loggable(): MorphTo
{
return $this->belongsTo(Quote::class);
}
public function requestedQuote(): BelongsTo
{
return $this->belongsTo(RequestedQuote::class);
return $this->morphTo();
}
}

View File

@ -2,8 +2,9 @@
namespace App\Models;
use App\Enums\LogAction;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@ -20,37 +21,47 @@ class Quote extends Model
Log::create([
'user_id' => auth()?->user()?->id,
'quote_id' => $this->id,
'content' => 'Quote requested.',
'loggable_type' => self::class,
'loggable_id' => $this->id,
'action' => LogAction::REQUEST,
'content' => $this->quote,
'ip' => request()->ip(),
]);
}
protected $fillable = [
'user_id',
'quote',
];
public function logs(): hasMany
public function logs(): MorphOne
{
return $this->hasMany(Log::class);
return $this->morphOne(Log::class, 'loggable');
}
public static function boot(): void
{
parent::boot();
self::created(function ($model) {
self::created(function (Quote $model) {
Log::create([
'user_id' => auth()?->user()?->id,
'quote_id' => $model->id,
'content' => 'Quote created.',
'loggable_type' => self::class,
'loggable_id' => $model->id,
'action' => LogAction::CREATE,
'content' => $model->quote,
'ip' => request()->ip(),
]);
});
self::deleted(function ($model) {
self::deleted(function (Quote $model) {
Log::create([
'user_id' => auth()?->user()?->id,
'quote_id' => $model->id,
'content' => 'Quote deleted.',
'loggable_type' => self::class,
'loggable_id' => $model->id,
'action' => LogAction::DELETE,
'content' => $model->quote,
'ip' => request()->ip(),
]);
});
}

View File

@ -2,8 +2,9 @@
namespace App\Models;
use App\Enums\LogAction;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@ -15,20 +16,22 @@ class RequestedQuote extends Model
protected $fillable = [
'quote',
'user_id',
];
public function logs(): hasMany
public function logs(): MorphOne
{
return $this->hasMany(Log::class);
return $this->morphOne(Log::class, 'loggable');
}
public function approve(): void
{
Log::create([
'user_id' => auth()?->user()?->id,
'requested_quote_id' => $this->id,
'content' => 'Quote approved.',
'loggable_type' => self::class,
'loggable_id' => $this->id,
'action' => LogAction::APPROVE,
'content' => $this->quote,
'ip' => request()->ip(),
]);
Quote::create([
@ -42,8 +45,11 @@ class RequestedQuote extends Model
{
Log::create([
'user_id' => auth()?->user()?->id,
'requested_quote_id' => $this->id,
'content' => 'Quote rejected.',
'loggable_type' => self::class,
'loggable_id' => $this->id,
'action' => LogAction::REJECT,
'content' => $this->quote,
'ip' => request()->ip(),
]);
$this->delete();

View File

@ -2,18 +2,20 @@
namespace App\Models;
use App\Enums\LogAction;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
/**
* @mixin IdeHelperUser
*/
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
use HasFactory, Notifiable;
/**
* The model's default values for attributes.
@ -62,27 +64,33 @@ class User extends Authenticatable
'password' => 'hashed',
];
public function logs(): hasMany
public function logAction(): HasMany
{
return $this->hasMany(Log::class);
}
public function fullName(): string
public function logs(): MorphOne
{
return "{$this->firstname} {$this->lastname}";
return $this->morphOne(Log::class, 'loggable');
}
public function getFullNameAttribute(): string
{
return "$this->firstname $this->lastname";
}
public static function boot(): void
{
parent::boot();
// These will only ever get created by the api/bot.
// If we want to add something like by whom later on,
// we can just append it to the content e.g auth()?->user()?->id ?? 'API'.
self::created(function ($model) {
Log::create([
'user_id' => $model->id,
'content' => 'User created.',
'user_id' => auth()?->user()?->id ?? 1,
'loggable_type' => self::class,
'loggable_id' => $model->id,
'action' => LogAction::CREATE,
'content' => $model->full_name,
'ip' => request()->ip(),
]);
});
}

View File

@ -14,9 +14,11 @@ return new class extends Migration
Schema::create('logs', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(User::class)->nullable();
$table->foreignIdFor(Quote::class)->nullable();
$table->foreignIdFor(RequestedQuote::class)->nullable();
$table->string('content');
$table->string('loggable_type');
$table->integer('loggable_id');
$table->string('action');
$table->text('content');
$table->string('ip');
$table->timestamps();
});
}

View File

@ -12,7 +12,7 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
$this->call(BaseQuotesSeeder::class);
$this->call(PriceyBotSeeder::class);
$this->call(BaseQuotesSeeder::class);
}
}

View File

@ -1,9 +1,5 @@
<x-app-layout>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="p-6 text-nexi-black dark:text-gray-100">
<livewire:pages.admin.logs/>
</div>
</div>
</div>
</x-app-layout>

View File

@ -1,23 +1,23 @@
<?php
use App\Models\Log;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Layout;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new #[Layout('layouts.app')] class extends Component
{
public Collection $logs;
use WithPagination;
public function mount(): void
public function with(): array
{
$this->getLogs();
}
$logs = Log::with(['user', 'loggable'])
->orderByDesc('created_at')
->paginate(15);
public function getLogs(): void
{
// TODO: look into pagination for this.
$this->logs = Log::with(['user', 'quote', 'requestedQuote'])->get()->sortByDesc('created_at');
return [
'logs' => $logs,
];
}
}; ?>
@ -30,38 +30,56 @@ new #[Layout('layouts.app')] class extends Component
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-nexi-black dark:text-gray-200 sm:pl-6">
User
</th>
<th scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-nexi-black dark:text-gray-200">
Type
</th>
<th scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-nexi-black dark:text-gray-200">
Entity ID
</th>
<th scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-nexi-black dark:text-gray-200">
Action
</th>
<th scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-nexi-black dark:text-gray-200">
Logs
</th>
<th scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-nexi-black dark:text-gray-200">
Type
IP
</th>
</tr>
</thead>
<tbody>
@foreach ($logs as $log)
<tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm text-nexi-black dark:text-gray-200 sm:pl-0">
{{ $log?->user->fullName() ?? 'N/A' }}
<td class="relative py-4 px-4 pr-3 text-sm sm:pl-6 border-t text-nexi-black dark:text-gray-200">
{{ $log?->user?->full_name ?? 'Anonymous' }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-nexi-black dark:text-gray-200">
<td class="relative py-4 pr-3 text-sm sm:pl-6 border-t text-nexi-black dark:text-gray-200">
{{ str_replace('App\Models\\', '', $log->loggable_type) }}
</td>
<td class="relative py-4 pr-3 text-sm sm:pl-6 border-t text-nexi-black dark:text-gray-200">
{{ $log->loggable_id }}
</td>
<td class="relative py-4 pr-3 text-sm sm:pl-6 border-t text-nexi-black dark:text-gray-200">
{{ $log->action }}
</td>
<td class="relative py-4 pr-3 text-sm sm:pl-6 border-t text-nexi-black dark:text-gray-200">
{{ $log->content }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-nexi-black dark:text-gray-200">
@if (!empty($log->quote_id))
Quote - {{ $log->quote_id }}
@elseif (!empty($log->requested_quote_id))
Requsted Quote - ID: {{ $log->requested_quote_id }}
@else
N/A
@endif
<td class="relative py-4 pr-3 text-sm sm:pl-6 border-t text-nexi-black dark:text-gray-200">
{{ $log->ip }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="mt-4">
{{ $logs->links() }}
</div>
</div>

View File

@ -1,5 +1,6 @@
<?php
use App\Enums\LogAction;
use App\Models\Log;
use Illuminate\Support\Facades\Http;
use Livewire\Attributes\Layout;
@ -19,8 +20,12 @@ new #[Layout('layouts.app')] class extends Component
$response = Http::post(config('bot.webhook'), ['text' => $validated['quote']]);
Log::create([
'user_id' => auth()?->user()?->id,
'content' => "Quote sent. {$validated['quote']}",
'user_id' => auth()->user()->id,
'loggable_type' => Log::class,
'loggable_id' => null,
'action' => LogAction::SEND,
'content' => $validated['quote'],
'ip' => request()->ip(),
]);
$this->quote = '';

View File

@ -1,6 +1,8 @@
<?php
use App\Enums\LogAction;
use App\Models\Log;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\RateLimiter;
@ -41,8 +43,12 @@ new #[Layout('layouts.guest')] class extends Component
session()->regenerate();
Log::create([
'user_id' => auth()?->user()?->id,
'content' => "Quote sent. {$quote}",
'user_id' => auth()->user()->id,
'loggable_type' => User::class,
'loggable_id' => auth()->user()->id,
'action' => LogAction::ACCESS,
'content' => 'User logged in',
'ip' => request()->ip(),
]);
$this->redirect(

View File

@ -1,5 +1,7 @@
<?php
use App\Enums\LogAction;
use App\Models\User;
use App\Models\Log;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Support\Facades\Hash;
@ -60,8 +62,12 @@ new #[Layout('layouts.guest')] class extends Component
session()->flash('status', __($status));
Log::create([
'user_id' => auth()?->user()?->id,
'content' => "Quote sent. {$quote}",
'user_id' => auth()->user()->id,
'loggable_type' => User::class,
'loggable_id' => auth()?->user()?->id,
'action' => LogAction::UPDATE,
'content' => 'User reset password via reset',
'ip' => request()->ip(),
]);
$this->redirectRoute('login', navigate: true);

View File

@ -1,6 +1,8 @@
<?php
use App\Enums\LogAction;
use App\Models\Log;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
@ -30,8 +32,12 @@ new class extends Component
]);
Log::create([
'user_id' => auth()?->user()?->id,
'content' => "Quote sent. {$quote}",
'user_id' => auth()->user()->id,
'loggable_type' => User::class,
'loggable_id' => auth()?->user()?->id,
'action' => LogAction::UPDATE,
'content' => 'User updated password',
'ip' => request()->ip(),
]);
$this->reset('current_password', 'password', 'password_confirmation');
@ -53,25 +59,41 @@ new class extends Component
<form wire:submit="updatePassword" class="mt-6 space-y-6">
<div>
<x-input-label for="current_password" :value="__('Current Password')" />
<x-text-input wire:model="current_password" id="current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
<x-input-error :messages="$errors->get('current_password')" class="mt-2" />
<x-input-label for="current_password" :value="__('Current Password')"/>
<x-text-input wire:model="current_password"
id="current_password"
name="current_password"
type="password"
class="mt-1 block w-full"
autocomplete="current-password"/>
<x-input-error :messages="$errors->get('current_password')" class="mt-2"/>
</div>
<div>
<x-input-label for="password" :value="__('New Password')" />
<x-text-input wire:model="password" id="password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
<x-input-label for="password" :value="__('New Password')"/>
<x-text-input wire:model="password"
id="password"
name="password"
type="password"
class="mt-1 block w-full"
autocomplete="new-password"/>
<x-input-error :messages="$errors->get('password')" class="mt-2"/>
</div>
<div>
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input wire:model="password_confirmation" id="password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
<x-input-label for="password_confirmation" :value="__('Confirm Password')"/>
<x-text-input wire:model="password_confirmation"
id="password_confirmation"
name="password_confirmation"
type="password"
class="mt-1 block w-full"
autocomplete="new-password"/>
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2"/>
</div>
<div class="flex items-center gap-4">
<x-primary-button wire:loading.attr="disabled" wire:loading.class="opacity-50">{{ __('Save') }}</x-primary-button>
<x-primary-button wire:loading.attr="disabled"
wire:loading.class="opacity-50">{{ __('Save') }}</x-primary-button>
<x-action-message class="mr-3" on="password-updated">
{{ __('Saved.') }}

View File

@ -7,8 +7,7 @@ export default defineConfig({
input: [
'resources/css/app.css',
'resources/js/app.js',
],
refresh: true,
]
}),
],
});