Add support for returning transforming activity logs on the front-end
This commit is contained in:
parent
e15985ea39
commit
a5521ecb79
9 changed files with 162 additions and 6 deletions
30
app/Http/Controllers/Api/Client/ActivityLogController.php
Normal file
30
app/Http/Controllers/Api/Client/ActivityLogController.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Http\Controllers\Api\Client;
|
||||||
|
|
||||||
|
use Spatie\QueryBuilder\QueryBuilder;
|
||||||
|
use Spatie\QueryBuilder\AllowedFilter;
|
||||||
|
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||||
|
use Pterodactyl\Transformers\Api\Client\ActivityLogTransformer;
|
||||||
|
|
||||||
|
class ActivityLogController extends ClientApiController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns a paginated set of the user's activity logs.
|
||||||
|
*/
|
||||||
|
public function __invoke(ClientApiRequest $request): array
|
||||||
|
{
|
||||||
|
$activity = QueryBuilder::for($request->user()->activity())
|
||||||
|
->with('actor')
|
||||||
|
->allowedFilters([
|
||||||
|
AllowedFilter::exact('ip'),
|
||||||
|
AllowedFilter::partial('event'),
|
||||||
|
])
|
||||||
|
->paginate(min($request->query('per_page', 50), 100))
|
||||||
|
->appends($request->query());
|
||||||
|
|
||||||
|
return $this->fractal->collection($activity)
|
||||||
|
->transformWith($this->getTransformer(ActivityLogTransformer::class))
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ use Illuminate\Database\Eloquent\Model as IlluminateModel;
|
||||||
* @property string|null $actor_type
|
* @property string|null $actor_type
|
||||||
* @property int|null $actor_id
|
* @property int|null $actor_id
|
||||||
* @property \Illuminate\Support\Collection|null $properties
|
* @property \Illuminate\Support\Collection|null $properties
|
||||||
* @property string $timestamp
|
* @property \Carbon\Carbon $timestamp
|
||||||
* @property IlluminateModel|\Eloquent $actor
|
* @property IlluminateModel|\Eloquent $actor
|
||||||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\ActivityLogSubject[] $subjects
|
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\ActivityLogSubject[] $subjects
|
||||||
* @property int|null $subjects_count
|
* @property int|null $subjects_count
|
||||||
|
@ -47,6 +47,8 @@ class ActivityLog extends Model
|
||||||
{
|
{
|
||||||
use MassPrunable;
|
use MassPrunable;
|
||||||
|
|
||||||
|
public const RESOURCE_NAME = 'activity_log';
|
||||||
|
|
||||||
public $timestamps = false;
|
public $timestamps = false;
|
||||||
|
|
||||||
protected $guarded = [
|
protected $guarded = [
|
||||||
|
@ -56,6 +58,7 @@ class ActivityLog extends Model
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'properties' => 'collection',
|
'properties' => 'collection',
|
||||||
|
'timestamp' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $with = ['subjects'];
|
protected $with = ['subjects'];
|
||||||
|
|
37
app/Transformers/Api/Client/ActivityLogTransformer.php
Normal file
37
app/Transformers/Api/Client/ActivityLogTransformer.php
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Transformers\Api\Client;
|
||||||
|
|
||||||
|
use Pterodactyl\Models\User;
|
||||||
|
use Pterodactyl\Models\ActivityLog;
|
||||||
|
|
||||||
|
class ActivityLogTransformer extends BaseClientTransformer
|
||||||
|
{
|
||||||
|
protected array $availableIncludes = ['actor'];
|
||||||
|
|
||||||
|
public function getResourceName(): string
|
||||||
|
{
|
||||||
|
return ActivityLog::RESOURCE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transform(ActivityLog $model): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'batch' => $model->batch,
|
||||||
|
'event' => $model->event,
|
||||||
|
'ip' => $model->ip,
|
||||||
|
'description' => $model->description,
|
||||||
|
'properties' => $model->properties,
|
||||||
|
'timestamp' => $model->timestamp->toIso8601String(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function includeActor(ActivityLog $model)
|
||||||
|
{
|
||||||
|
if (!$model->actor instanceof User) {
|
||||||
|
return $this->null();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->item($model->actor, $this->makeTransformer(UserTransformer::class), User::RESOURCE_NAME);
|
||||||
|
}
|
||||||
|
}
|
26
resources/scripts/api/definitions/helpers.ts
Normal file
26
resources/scripts/api/definitions/helpers.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { FractalResponseData, FractalResponseList } from '@/api/http';
|
||||||
|
|
||||||
|
type Transformer<T> = (callback: FractalResponseData) => T;
|
||||||
|
|
||||||
|
const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list';
|
||||||
|
|
||||||
|
function transform<T, M>(data: null | undefined, transformer: Transformer<T>, missing?: M): M;
|
||||||
|
function transform<T, M>(data: FractalResponseData | null | undefined, transformer: Transformer<T>, missing?: M): T | M;
|
||||||
|
function transform<T, M>(data: FractalResponseList | null | undefined, transformer: Transformer<T>, missing?: M): T[] | M;
|
||||||
|
function transform<T> (data: FractalResponseData | FractalResponseList | null | undefined, transformer: Transformer<T>, missing = undefined) {
|
||||||
|
if (data === undefined || data === null) {
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isList(data)) {
|
||||||
|
return data.data.map(transformer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || !data.attributes || data.object === 'null_resource') {
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformer(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { transform };
|
3
resources/scripts/api/definitions/index.d.ts
vendored
3
resources/scripts/api/definitions/index.d.ts
vendored
|
@ -1,4 +1,5 @@
|
||||||
import { MarkRequired } from 'ts-essentials';
|
import { MarkRequired } from 'ts-essentials';
|
||||||
|
import { FractalResponseData, FractalResponseList } from '../http';
|
||||||
|
|
||||||
export type UUID = string;
|
export type UUID = string;
|
||||||
|
|
||||||
|
@ -6,7 +7,7 @@ export type UUID = string;
|
||||||
export interface Model {}
|
export interface Model {}
|
||||||
|
|
||||||
interface ModelWithRelationships extends Model {
|
interface ModelWithRelationships extends Model {
|
||||||
relationships: Record<string, unknown>;
|
relationships: Record<string, FractalResponseData | FractalResponseList | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,16 @@
|
||||||
import { Model } from '@/api/definitions';
|
import { Model, UUID } from '@/api/definitions';
|
||||||
|
import { SubuserPermission } from '@/state/server/subusers';
|
||||||
|
|
||||||
|
interface User extends Model {
|
||||||
|
uuid: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
image: string;
|
||||||
|
twoFactorEnabled: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
permissions: SubuserPermission[];
|
||||||
|
can (permission: SubuserPermission): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface SSHKey extends Model {
|
interface SSHKey extends Model {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -6,3 +18,15 @@ interface SSHKey extends Model {
|
||||||
fingerprint: string;
|
fingerprint: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ActivityLog extends Model<'actor'> {
|
||||||
|
batch: UUID | null;
|
||||||
|
event: string;
|
||||||
|
ip: string;
|
||||||
|
description: string | null;
|
||||||
|
properties: Record<string, string | unknown>;
|
||||||
|
timestamp: Date;
|
||||||
|
relationships: {
|
||||||
|
actor: User | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { SSHKey } from '@definitions/user/models';
|
import * as Models from '@definitions/user/models';
|
||||||
|
import { FractalResponseData } from '@/api/http';
|
||||||
|
import { transform } from '@definitions/helpers';
|
||||||
|
|
||||||
export default class Transformers {
|
export default class Transformers {
|
||||||
static toSSHKey (data: Record<any, any>): SSHKey {
|
static toSSHKey (data: Record<any, any>): Models.SSHKey {
|
||||||
return {
|
return {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
publicKey: data.public_key,
|
publicKey: data.public_key,
|
||||||
|
@ -9,6 +11,37 @@ export default class Transformers {
|
||||||
createdAt: new Date(data.created_at),
|
createdAt: new Date(data.created_at),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static toUser ({ attributes }: FractalResponseData): Models.User {
|
||||||
|
return {
|
||||||
|
uuid: attributes.uuid,
|
||||||
|
username: attributes.username,
|
||||||
|
email: attributes.email,
|
||||||
|
image: attributes.image,
|
||||||
|
twoFactorEnabled: attributes['2fa_enabled'],
|
||||||
|
permissions: attributes.permissions || [],
|
||||||
|
createdAt: new Date(attributes.created_at),
|
||||||
|
can (permission): boolean {
|
||||||
|
return this.permissions.includes(permission);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static toActivityLog ({ attributes }: FractalResponseData): Models.ActivityLog {
|
||||||
|
const { actor } = attributes.relationships || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
batch: attributes.batch,
|
||||||
|
event: attributes.event,
|
||||||
|
ip: attributes.ip,
|
||||||
|
description: attributes.description,
|
||||||
|
properties: attributes.properties,
|
||||||
|
timestamp: new Date(attributes.timestamp),
|
||||||
|
relationships: {
|
||||||
|
actor: transform(actor as FractalResponseData, this.toUser, null),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MetaTransformers {
|
export class MetaTransformers {
|
||||||
|
|
|
@ -68,7 +68,7 @@ export interface FractalResponseData {
|
||||||
object: string;
|
object: string;
|
||||||
attributes: {
|
attributes: {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
relationships?: Record<string, FractalResponseData | FractalResponseList>;
|
relationships?: Record<string, FractalResponseData | FractalResponseList | null | undefined>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,8 @@ Route::prefix('/account')->middleware(AccountActivitySubject::class)->group(func
|
||||||
Route::put('/email', [Client\AccountController::class, 'updateEmail'])->name('api:client.account.update-email');
|
Route::put('/email', [Client\AccountController::class, 'updateEmail'])->name('api:client.account.update-email');
|
||||||
Route::put('/password', [Client\AccountController::class, 'updatePassword'])->name('api:client.account.update-password');
|
Route::put('/password', [Client\AccountController::class, 'updatePassword'])->name('api:client.account.update-password');
|
||||||
|
|
||||||
|
Route::get('/activity', Client\ActivityLogController::class)->name('api:client.account.activity');
|
||||||
|
|
||||||
Route::get('/api-keys', [Client\ApiKeyController::class, 'index']);
|
Route::get('/api-keys', [Client\ApiKeyController::class, 'index']);
|
||||||
Route::post('/api-keys', [Client\ApiKeyController::class, 'store']);
|
Route::post('/api-keys', [Client\ApiKeyController::class, 'store']);
|
||||||
Route::delete('/api-keys/{identifier}', [Client\ApiKeyController::class, 'delete']);
|
Route::delete('/api-keys/{identifier}', [Client\ApiKeyController::class, 'delete']);
|
||||||
|
|
Loading…
Reference in a new issue