API: Added audit log list endpoint

Not yested covered with testing.
Changes database columns for more presentable names and for future use
to connect additional model types.
For #4316
This commit is contained in:
Dan Brown 2024-05-04 16:28:18 +01:00
parent dd251d9e62
commit 3946158e88
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
10 changed files with 165 additions and 26 deletions

View File

@ -27,14 +27,14 @@ class ActivityQueries
public function latest(int $count = 20, int $page = 0): array public function latest(int $count = 20, int $page = 0): array
{ {
$activityList = $this->permissions $activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type') ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->with(['user']) ->with(['user'])
->skip($count * $page) ->skip($count * $page)
->take($count) ->take($count)
->get(); ->get();
$this->listLoader->loadIntoRelations($activityList->all(), 'entity', false); $this->listLoader->loadIntoRelations($activityList->all(), 'loggable', false);
return $this->filterSimilar($activityList); return $this->filterSimilar($activityList);
} }
@ -59,8 +59,8 @@ class ActivityQueries
$query->where(function (Builder $query) use ($queryIds) { $query->where(function (Builder $query) use ($queryIds) {
foreach ($queryIds as $morphClass => $idArr) { foreach ($queryIds as $morphClass => $idArr) {
$query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) { $query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {
$innerQuery->where('entity_type', '=', $morphClass) $innerQuery->where('loggable_type', '=', $morphClass)
->whereIn('entity_id', $idArr); ->whereIn('loggable_id', $idArr);
}); });
} }
}); });
@ -82,7 +82,7 @@ class ActivityQueries
public function userActivity(User $user, int $count = 20, int $page = 0): array public function userActivity(User $user, int $count = 20, int $page = 0): array
{ {
$activityList = $this->permissions $activityList = $this->permissions
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type') ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->where('user_id', '=', $user->id) ->where('user_id', '=', $user->id)
->skip($count * $page) ->skip($count * $page)

View File

@ -0,0 +1,28 @@
<?php
namespace BookStack\Activity\Controllers;
use BookStack\Activity\Models\Activity;
use BookStack\Http\ApiController;
class AuditLogApiController extends ApiController
{
/**
* Get a listing of audit log events in the system.
* The loggable relation fields currently only relates to core
* content types (page, book, bookshelf, chapter) but this may be
* used more in the future across other types.
* Requires permission to manage both users and system settings.
*/
public function list()
{
$this->checkPermission('settings-manage');
$this->checkPermission('users-manage');
$query = Activity::query()->with(['user']);
return $this->apiListingResponse($query, [
'id', 'type', 'detail', 'user_id', 'loggable_id', 'loggable_type', 'ip', 'created_at',
]);
}
}

View File

@ -32,7 +32,7 @@ class AuditLogController extends Controller
$query = Activity::query() $query = Activity::query()
->with([ ->with([
'entity' => fn ($query) => $query->withTrashed(), 'loggable' => fn ($query) => $query->withTrashed(),
'user', 'user',
]) ])
->orderBy($listOptions->getSort(), $listOptions->getOrder()); ->orderBy($listOptions->getSort(), $listOptions->getOrder());

View File

@ -17,24 +17,22 @@ use Illuminate\Support\Str;
* @property User $user * @property User $user
* @property Entity $entity * @property Entity $entity
* @property string $detail * @property string $detail
* @property string $entity_type * @property string $loggable_type
* @property int $entity_id * @property int $loggable_id
* @property int $user_id * @property int $user_id
* @property Carbon $created_at * @property Carbon $created_at
* @property Carbon $updated_at
*/ */
class Activity extends Model class Activity extends Model
{ {
/** /**
* Get the entity for this activity. * Get the loggable model related to this activity.
* Currently only used for entities (previously entity_[id/type] columns).
* Could be used for others but will need an audit of uses where assumed
* to be entities.
*/ */
public function entity(): MorphTo public function loggable(): MorphTo
{ {
if ($this->entity_type === '') { return $this->morphTo('loggable');
$this->entity_type = null;
}
return $this->morphTo('entity');
} }
/** /**
@ -47,8 +45,8 @@ class Activity extends Model
public function jointPermissions(): HasMany public function jointPermissions(): HasMany
{ {
return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id') return $this->hasMany(JointPermission::class, 'entity_id', 'loggable_id')
->whereColumn('activities.entity_type', '=', 'joint_permissions.entity_type'); ->whereColumn('activities.loggable_type', '=', 'joint_permissions.entity_type');
} }
/** /**
@ -74,6 +72,6 @@ class Activity extends Model
*/ */
public function isSimilarTo(self $activityB): bool public function isSimilarTo(self $activityB): bool
{ {
return [$this->type, $this->entity_type, $this->entity_id] === [$activityB->type, $activityB->entity_type, $activityB->entity_id]; return [$this->type, $this->loggable_type, $this->loggable_id] === [$activityB->type, $activityB->loggable_type, $activityB->loggable_id];
} }
} }

View File

@ -32,8 +32,8 @@ class ActivityLogger
$activity->detail = $detailToStore; $activity->detail = $detailToStore;
if ($detail instanceof Entity) { if ($detail instanceof Entity) {
$activity->entity_id = $detail->id; $activity->loggable_id = $detail->id;
$activity->entity_type = $detail->getMorphClass(); $activity->loggable_type = $detail->getMorphClass();
} }
$activity->save(); $activity->save();
@ -64,9 +64,9 @@ class ActivityLogger
public function removeEntity(Entity $entity): void public function removeEntity(Entity $entity): void
{ {
$entity->activity()->update([ $entity->activity()->update([
'detail' => $entity->name, 'detail' => $entity->name,
'entity_id' => null, 'loggable_id' => null,
'entity_type' => null, 'loggable_type' => null,
]); ]);
} }

View File

@ -19,7 +19,7 @@ class ClearActivityCommand extends Command
* *
* @var string * @var string
*/ */
protected $description = 'Clear user activity from the system'; protected $description = 'Clear user (audit-log) activity from the system';
/** /**
* Execute the console command. * Execute the console command.

View File

@ -137,7 +137,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
*/ */
public function activity(): MorphMany public function activity(): MorphMany
{ {
return $this->morphMany(Activity::class, 'entity') return $this->morphMany(Activity::class, 'loggable')
->orderBy('created_at', 'desc'); ->orderBy('created_at', 'desc');
} }

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('activities', function (Blueprint $table) {
$table->renameColumn('entity_id', 'loggable_id');
$table->renameColumn('entity_type', 'loggable_type');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('activities', function (Blueprint $table) {
$table->renameColumn('loggable_id', 'entity_id');
$table->renameColumn('loggable_type', 'entity_type');
});
}
};

View File

@ -0,0 +1,80 @@
{
"data": [
{
"id": 1,
"type": "bookshelf_create",
"detail": "",
"user_id": 1,
"loggable_id": 1,
"loggable_type": "bookshelf",
"ip": "124.4.x.x",
"created_at": "2021-09-29T12:32:02.000000Z",
"user": {
"id": 1,
"name": "Admins",
"slug": "admins"
}
},
{
"id": 2,
"type": "auth_login",
"detail": "standard; (1) Admin",
"user_id": 1,
"loggable_id": null,
"loggable_type": null,
"ip": "127.0.x.x",
"created_at": "2021-09-29T12:32:04.000000Z",
"user": {
"id": 1,
"name": "Admins",
"slug": "admins"
}
},
{
"id": 3,
"type": "bookshelf_update",
"detail": "",
"user_id": 1,
"loggable_id": 1,
"loggable_type": "bookshelf",
"ip": "127.0.x.x",
"created_at": "2021-09-29T12:32:07.000000Z",
"user": {
"id": 1,
"name": "Admins",
"slug": "admins"
}
},
{
"id": 4,
"type": "page_create",
"detail": "",
"user_id": 1,
"loggable_id": 1,
"loggable_type": "page",
"ip": "127.0.x.x",
"created_at": "2021-09-29T12:32:13.000000Z",
"user": {
"id": 1,
"name": "Admins",
"slug": "admins"
}
},
{
"id": 5,
"type": "page_update",
"detail": "",
"user_id": 1,
"loggable_id": 1,
"loggable_type": "page",
"ip": "127.0.x.x",
"created_at": "2021-09-29T12:37:27.000000Z",
"user": {
"id": 1,
"name": "Admins",
"slug": "admins"
}
}
],
"total": 6088
}

View File

@ -6,6 +6,7 @@
* Controllers all end with "ApiController" * Controllers all end with "ApiController"
*/ */
use BookStack\Activity\Controllers\AuditLogApiController;
use BookStack\Api\ApiDocsController; use BookStack\Api\ApiDocsController;
use BookStack\Entities\Controllers as EntityControllers; use BookStack\Entities\Controllers as EntityControllers;
use BookStack\Permissions\ContentPermissionApiController; use BookStack\Permissions\ContentPermissionApiController;
@ -89,3 +90,5 @@ Route::delete('recycle-bin/{deletionId}', [EntityControllers\RecycleBinApiContro
Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'read']); Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'read']);
Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'update']); Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'update']);
Route::get('audit-log', [AuditLogApiController::class, 'list']);