Add support for returning transforming activity logs on the front-end

This commit is contained in:
DaneEveritt 2022-05-29 20:34:48 -04:00
parent e15985ea39
commit a5521ecb79
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
9 changed files with 162 additions and 6 deletions

View 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();
}
}

View file

@ -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'];

View 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);
}
}

View 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 };

View file

@ -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>;
} }
/** /**

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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>;
}; };
} }

View file

@ -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']);