Added user id to logs, implemented filter by user
This commit is contained in:
parent
162116998c
commit
8185a74edc
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Engelsystem\Migrations;
|
||||
|
||||
use Engelsystem\Database\Migration\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class AddUserIdToLogEntries extends Migration
|
||||
{
|
||||
use Reference;
|
||||
|
||||
/**
|
||||
* Run the migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$this->schema->table('log_entries', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('user_id')->after('id')->nullable()->default(null);
|
||||
$table->foreign('user_id')
|
||||
->references('id')->on('users')
|
||||
->onUpdate('cascade')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$this->schema->table('log_entries', function (Blueprint $table): void {
|
||||
$table->dropForeign('log_entries_user_id_foreign');
|
||||
$table->dropColumn('user_id');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Engelsystem\Migrations;
|
||||
|
||||
use Engelsystem\Database\Migration\Migration;
|
||||
|
||||
class AddLogsAllPermission extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$db = $this->schema->getConnection();
|
||||
$db->table('privileges')
|
||||
->insert([
|
||||
['name' => 'logs.all', 'description' => 'View all logs'],
|
||||
]);
|
||||
|
||||
$logsAll = $db->table('privileges')
|
||||
->where('name', 'logs.all')
|
||||
->get(['id'])
|
||||
->first();
|
||||
|
||||
$bureaucrat = 80;
|
||||
$db->table('group_privileges')
|
||||
->insertOrIgnore([
|
||||
['group_id' => $bureaucrat, 'privilege_id' => $logsAll->id],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$db = $this->schema->getConnection();
|
||||
$db->table('privileges')
|
||||
->where('name', 'logs.all')
|
||||
->delete();
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ trait Reference
|
|||
$table->primary($fromColumn);
|
||||
}
|
||||
|
||||
$this->addReference($table, $fromColumn, $targetTable, $targetColumn ?: 'id');
|
||||
$this->addReference($table, $fromColumn, $targetTable, $targetColumn);
|
||||
|
||||
return $col;
|
||||
}
|
||||
|
|
|
@ -1407,6 +1407,9 @@ msgstr "Du kannst hier Markdown verwenden"
|
|||
msgid "form.required"
|
||||
msgstr "Pflichtfeld"
|
||||
|
||||
msgid "form.user_select"
|
||||
msgstr "Wähle einen User"
|
||||
|
||||
msgid "schedule.import"
|
||||
msgstr "Programm importieren"
|
||||
|
||||
|
@ -1531,6 +1534,9 @@ msgstr "Suchen"
|
|||
msgid "log.log"
|
||||
msgstr "Logs"
|
||||
|
||||
msgid "log.only_own"
|
||||
msgstr "Du siehst hier deine eigenen Logs. Die logs anderer User können nur Bürokraten sehen."
|
||||
|
||||
msgid "log.time"
|
||||
msgstr "Zeit"
|
||||
|
||||
|
|
|
@ -115,6 +115,9 @@ msgstr "Required"
|
|||
msgid "form.markdown"
|
||||
msgstr "You can use Markdown here"
|
||||
|
||||
msgid "form.user_select"
|
||||
msgstr "Select a user"
|
||||
|
||||
msgid "schedule.import"
|
||||
msgstr "Import schedule"
|
||||
|
||||
|
@ -244,6 +247,9 @@ msgstr "Search"
|
|||
msgid "log.log"
|
||||
msgstr "Logs"
|
||||
|
||||
msgid "log.only_own"
|
||||
msgstr "You can view your own logs. The logs of other users can be checked by bureaucrats."
|
||||
|
||||
msgid "log.time"
|
||||
msgstr "Time"
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
{% block content %}
|
||||
<div class="col-md-12">
|
||||
<h1>{{ block('title') }}</h1>
|
||||
<h1>{{ block('title') }} <small>({{ entries|length }})</small></h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
@ -14,19 +14,39 @@
|
|||
<form method="post" action="{{ url('/admin/logs') }}" class="form-inline">
|
||||
{{ csrf() }}
|
||||
|
||||
{{ f.input('search', __('form.search'), {
|
||||
'value': search,
|
||||
'hide_label': true,
|
||||
}) }}
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
{{ f.input('search', __('form.search'), {
|
||||
'value': search,
|
||||
'hide_label': true,
|
||||
}) }}
|
||||
</div>
|
||||
|
||||
{% if has_permission_to('logs.all') %}
|
||||
<div class="col-md-4">
|
||||
{{ f.select('search_user_id', __('general.user'), users, {
|
||||
'default_option': __('form.user_select'),
|
||||
'selected': search_user_id,
|
||||
}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{{ f.submit(__('form.search'), {'icon_left': 'search'}) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if not has_permission_to('logs.all') %}
|
||||
<div class="mb-3">
|
||||
{{ m.alert(__('log.only_own')) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{{ __('log.time') }}</th>
|
||||
<th>{{ __('log.level') }}</th>
|
||||
<th>{{ __('general.user') }}</th>
|
||||
<th>{{ __('log.message') }}</th>
|
||||
</tr>
|
||||
{% for entry in entries %}
|
||||
|
@ -47,10 +67,11 @@
|
|||
{%- endif %}
|
||||
|
||||
<tr>
|
||||
<td class="table-{{ td_type }}">{{ entry.created_at.format(__('general.datetime')) }}</td>
|
||||
<td class="table-{{ td_type }}">
|
||||
<td class="table-{{ td_type }} text-nowrap">{{ entry.created_at.format(__('general.datetime')) }}</td>
|
||||
<td class="table-{{ td_type }} text-nowrap">
|
||||
<span class="badge bg-{{ type }}">{{ entry.level|capitalize }}</span> <!-- //todo bs5 -->
|
||||
</td>
|
||||
<td class="text-nowrap">{% if entry.user %}{{ m.user(entry.user) }}{% endif %}</td>
|
||||
<td>{{ entry.message|nl2br }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -5,9 +5,12 @@ declare(strict_types=1);
|
|||
namespace Engelsystem\Controllers\Admin;
|
||||
|
||||
use Engelsystem\Controllers\BaseController;
|
||||
use Engelsystem\Helpers\Authenticator;
|
||||
use Engelsystem\Http\Request;
|
||||
use Engelsystem\Http\Response;
|
||||
use Engelsystem\Models\LogEntry;
|
||||
use Engelsystem\Models\User\User;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class LogsController extends BaseController
|
||||
{
|
||||
|
@ -16,18 +19,33 @@ class LogsController extends BaseController
|
|||
'admin_log',
|
||||
];
|
||||
|
||||
public function __construct(protected LogEntry $log, protected Response $response)
|
||||
public function __construct(protected LogEntry $log, protected Response $response, protected Authenticator $auth)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$searchUserId = (int) $request->input('search_user_id') ?: null;
|
||||
$search = $request->input('search');
|
||||
$entries = $this->log->filter($search);
|
||||
$userId = $this->auth->user()?->id;
|
||||
|
||||
if ($this->auth->can('logs.all')) {
|
||||
$userId = $searchUserId;
|
||||
}
|
||||
|
||||
$entries = $this->log->filter($search, $userId);
|
||||
|
||||
/** @var Collection $users */
|
||||
$users = User::with('personalData')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->mapWithKeys(function (User $u) {
|
||||
return [$u->id => $u->displayName];
|
||||
});
|
||||
|
||||
return $this->response->withView(
|
||||
'admin/log.twig',
|
||||
['entries' => $entries, 'search' => $search]
|
||||
['entries' => $entries, 'search' => $search, 'users' => $users, 'search_user_id' => $searchUserId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ class Logger extends AbstractLogger
|
|||
$message .= $this->formatException($context['exception']);
|
||||
}
|
||||
|
||||
$this->log->create(['level' => $level, 'message' => $message]);
|
||||
$this->createEntry(['level' => $level, 'message' => $message]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,4 +81,9 @@ class Logger extends AbstractLogger
|
|||
{
|
||||
return in_array($level, $this->allowedLevels);
|
||||
}
|
||||
|
||||
protected function createEntry(array $data): void
|
||||
{
|
||||
$this->log->create($data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,24 +5,21 @@ declare(strict_types=1);
|
|||
namespace Engelsystem\Logger;
|
||||
|
||||
use Engelsystem\Helpers\Authenticator;
|
||||
use Psr\Log\InvalidArgumentException;
|
||||
use Stringable;
|
||||
|
||||
class UserAwareLogger extends Logger
|
||||
{
|
||||
protected Authenticator $auth;
|
||||
protected ?Authenticator $auth;
|
||||
|
||||
/**
|
||||
* Logs with an arbitrary level and prepends the user
|
||||
* @throws InvalidArgumentException
|
||||
* Adds the authenticated user to the log message
|
||||
*/
|
||||
public function log(mixed $level, string|Stringable $message, array $context = []): void
|
||||
public function createEntry(array $data): void
|
||||
{
|
||||
if ($this->auth && ($user = $this->auth->user())) {
|
||||
$message = sprintf('%s (%u): %s', $user->name, $user->id, $message);
|
||||
$data['user_id'] = $user->id;
|
||||
}
|
||||
|
||||
parent::log($level, $message, $context);
|
||||
parent::createEntry($data);
|
||||
}
|
||||
|
||||
public function setAuth(Authenticator $auth): void
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Engelsystem\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Engelsystem\Models\User\UsesUserModel;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
|
@ -23,35 +24,48 @@ use Illuminate\Support\Collection as SupportCollection;
|
|||
*/
|
||||
class LogEntry extends BaseModel
|
||||
{
|
||||
use UsesUserModel;
|
||||
|
||||
/** @var bool enable timestamps for created_at */
|
||||
public $timestamps = true; // phpcs:ignore
|
||||
|
||||
/** @var null Disable updated_at */
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected $casts = [ // phpcs:ignore
|
||||
'user_id' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*/
|
||||
protected $fillable = [ // phpcs:ignore
|
||||
'level',
|
||||
'message',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return Builder[]|Collection|SupportCollection|LogEntry[]
|
||||
*/
|
||||
public static function filter(string $keyword = null): array|Collection|SupportCollection
|
||||
public static function filter(?string $keyword = null, ?int $userId = null): array|Collection|SupportCollection
|
||||
{
|
||||
$query = self::query()
|
||||
->select()
|
||||
$query = self::with(['user', 'user.personalData', 'user.state'])
|
||||
->orderByDesc('created_at')
|
||||
->orderByDesc('id')
|
||||
->limit(10000);
|
||||
|
||||
if (!empty($userId)) {
|
||||
$query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
if (!empty($keyword)) {
|
||||
$query
|
||||
->where('level', '=', $keyword)
|
||||
->orWhere('message', 'LIKE', '%' . $keyword . '%');
|
||||
->where(function (Builder $query) use ($keyword): void {
|
||||
$query->where('level', '=', $keyword)
|
||||
->orWhere('message', 'LIKE', '%' . $keyword . '%');
|
||||
});
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
|
|
|
@ -4,10 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace Engelsystem\Test\Unit\Controllers\Admin;
|
||||
|
||||
use Engelsystem\Config\Config;
|
||||
use Engelsystem\Controllers\Admin\LogsController;
|
||||
use Engelsystem\Helpers\Authenticator;
|
||||
use Engelsystem\Http\Request;
|
||||
use Engelsystem\Http\Response;
|
||||
use Engelsystem\Models\LogEntry;
|
||||
use Engelsystem\Models\User\User;
|
||||
use Engelsystem\Test\Unit\HasDatabase;
|
||||
use Engelsystem\Test\Unit\TestCase;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
@ -25,22 +28,36 @@ class LogsControllerTest extends TestCase
|
|||
{
|
||||
$log = new LogEntry();
|
||||
$alert = $log->create(['level' => LogLevel::ALERT, 'message' => 'Alert test']);
|
||||
$alert = $log->find($alert)->first();
|
||||
$alert = $log->with('user')->find($alert)->first();
|
||||
$error = $log->create(['level' => LogLevel::ERROR, 'message' => 'Error test']);
|
||||
$error = $log->find($error)->first();
|
||||
$error = $log->with('user')->find($error)->first();
|
||||
|
||||
$auth = $this->createMock(Authenticator::class);
|
||||
$this->setExpects($auth, 'user', null, null, 2);
|
||||
$this->setExpects($auth, 'can', ['logs.all'], true, 2);
|
||||
|
||||
$response = $this->createMock(Response::class);
|
||||
$response->expects($this->exactly(2))
|
||||
->method('withView')
|
||||
->withConsecutive(
|
||||
['admin/log.twig', ['entries' => new Collection([$error, $alert]), 'search' => null]],
|
||||
['admin/log.twig', ['entries' => new Collection([$error]), 'search' => 'error']]
|
||||
['admin/log.twig', [
|
||||
'entries' => new Collection([$error, $alert]),
|
||||
'search' => null,
|
||||
'users' => new Collection(),
|
||||
'search_user_id' => null,
|
||||
]],
|
||||
['admin/log.twig', [
|
||||
'entries' => new Collection([$error]),
|
||||
'search' => 'error',
|
||||
'users' => new Collection(),
|
||||
'search_user_id' => null,
|
||||
]]
|
||||
)
|
||||
->willReturn($response);
|
||||
|
||||
$request = Request::create('/');
|
||||
|
||||
$controller = new LogsController($log, $response);
|
||||
$controller = new LogsController($log, $response, $auth);
|
||||
$controller->index($request);
|
||||
|
||||
$request->request->set('search', 'error');
|
||||
|
@ -48,12 +65,48 @@ class LogsControllerTest extends TestCase
|
|||
}
|
||||
|
||||
/**
|
||||
* Setup the DB
|
||||
* @covers \Engelsystem\Controllers\Admin\LogsController::index
|
||||
*/
|
||||
public function testIndexUser(): void
|
||||
{
|
||||
User::factory()->create();
|
||||
$user = User::with(['personalData', 'state'])->first();
|
||||
|
||||
$log = new LogEntry();
|
||||
$alert = $log->create(['level' => LogLevel::ALERT, 'message' => 'Users message', 'user_id' => $user->id]);
|
||||
/** @var LogEntry $alert */
|
||||
$alert = $log->with('user')->find($alert)->first();
|
||||
$log->create(['level' => LogLevel::ERROR, 'message' => 'Error test']);
|
||||
|
||||
$auth = $this->createMock(Authenticator::class);
|
||||
$this->setExpects($auth, 'user', null, $user);
|
||||
$this->setExpects($auth, 'can', ['logs.all'], false);
|
||||
|
||||
$response = $this->createMock(Response::class);
|
||||
$response->expects($this->once())
|
||||
->method('withView')
|
||||
->willReturnCallback(function (string $view, array $data) use ($alert, $response) {
|
||||
$this->assertEquals('admin/log.twig', $view);
|
||||
$this->assertArrayHasKey('entries', $data);
|
||||
$this->assertCount(1, $data['entries']);
|
||||
$this->assertEquals($alert->message, $data['entries'][0]['message']);
|
||||
return $response;
|
||||
});
|
||||
|
||||
$request = Request::create('/');
|
||||
|
||||
$controller = new LogsController($log, $response, $auth);
|
||||
$controller->index($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the DB
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->initDatabase();
|
||||
$this->app->instance('config', new Config([]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ class LoggerTest extends ServiceProviderTest
|
|||
|
||||
/**
|
||||
* @covers \Engelsystem\Logger\Logger::log
|
||||
* @covers \Engelsystem\Logger\Logger::createEntry
|
||||
* @dataProvider provideLogLevels
|
||||
*/
|
||||
public function testAllLevels(string $level): void
|
||||
|
|
|
@ -8,18 +8,23 @@ use Engelsystem\Helpers\Authenticator;
|
|||
use Engelsystem\Logger\UserAwareLogger;
|
||||
use Engelsystem\Models\LogEntry;
|
||||
use Engelsystem\Models\User\User;
|
||||
use Engelsystem\Test\Unit\ServiceProviderTest;
|
||||
use Engelsystem\Test\Unit\HasDatabase;
|
||||
use Engelsystem\Test\Unit\TestCase;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
class UserAwareLoggerTest extends ServiceProviderTest
|
||||
class UserAwareLoggerTest extends TestCase
|
||||
{
|
||||
use HasDatabase;
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Logger\UserAwareLogger::log
|
||||
* @covers \Engelsystem\Logger\UserAwareLogger::createEntry
|
||||
* @covers \Engelsystem\Logger\UserAwareLogger::setAuth
|
||||
*/
|
||||
public function testLog(): void
|
||||
{
|
||||
$this->initDatabase(); // To be able to run the test by itself
|
||||
|
||||
$user = User::factory(['id' => 1, 'name' => 'admin'])->make();
|
||||
|
||||
/** @var LogEntry|MockObject $logEntry */
|
||||
|
@ -30,7 +35,7 @@ class UserAwareLoggerTest extends ServiceProviderTest
|
|||
->method('create')
|
||||
->withConsecutive(
|
||||
[['level' => LogLevel::INFO, 'message' => 'Some more informational foo']],
|
||||
[['level' => LogLevel::INFO, 'message' => 'admin (1): Some even more informational bar']]
|
||||
[['level' => LogLevel::INFO, 'message' => 'Some even more informational bar', 'user_id' => 1]]
|
||||
);
|
||||
|
||||
/** @var Authenticator|MockObject $auth */
|
||||
|
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Engelsystem\Test\Unit\Models;
|
||||
|
||||
use Engelsystem\Models\LogEntry;
|
||||
use Engelsystem\Models\User\User;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
class LogEntryTest extends ModelTest
|
||||
|
@ -14,6 +15,8 @@ class LogEntryTest extends ModelTest
|
|||
*/
|
||||
public function testFilter(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
(new LogEntry(['level' => LogLevel::DEBUG, 'message' => 'Some users fault', 'user_id' => $user->id]))->save();
|
||||
foreach (
|
||||
[
|
||||
'I\'m an info' => LogLevel::INFO,
|
||||
|
@ -31,9 +34,10 @@ class LogEntryTest extends ModelTest
|
|||
(new LogEntry(['level' => $level, 'message' => $message]))->save();
|
||||
}
|
||||
|
||||
$this->assertCount(10, LogEntry::filter());
|
||||
$this->assertCount(11, LogEntry::filter());
|
||||
$this->assertCount(3, LogEntry::filter(LogLevel::INFO));
|
||||
$this->assertCount(1, LogEntry::filter('Oops'));
|
||||
$this->assertCount(1, LogEntry::filter(null, $user->id));
|
||||
|
||||
/** @var LogEntry $first */
|
||||
$first = LogEntry::filter()->first();
|
||||
|
|
Loading…
Reference in New Issue