Merge branch 'develop' into matthewpi/security-keys-backport

This commit is contained in:
Matthew Penner 2023-01-17 15:33:53 -07:00
commit f631ac1946
No known key found for this signature in database
1153 changed files with 25099 additions and 37002 deletions

View file

@ -3,7 +3,6 @@
namespace Pterodactyl\Models;
use Carbon\Carbon;
use LogicException;
use Illuminate\Support\Facades\Event;
use Pterodactyl\Events\ActivityLogged;
use Illuminate\Database\Eloquent\Builder;
@ -124,7 +123,7 @@ class ActivityLog extends Model
public function prunable()
{
if (is_null(config('activity.prune_days'))) {
throw new LogicException('Cannot prune activity logs: no "prune_days" configuration value is set.');
throw new \LogicException('Cannot prune activity logs: no "prune_days" configuration value is set.');
}
return static::where('timestamp', '<=', Carbon::now()->subDays(config('activity.prune_days')));

59
app/Models/AdminRole.php Normal file
View file

@ -0,0 +1,59 @@
<?php
namespace Pterodactyl\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string $name
* @property string|null $description
* @property int $sort_id
* @property array $permissions
*/
class AdminRole extends Model
{
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
*/
public const RESOURCE_NAME = 'admin_role';
/**
* The table associated with the model.
*/
protected $table = 'admin_roles';
/**
* Fields that are mass assignable.
*/
protected $fillable = [
'name',
'description',
'sort_id',
];
/**
* Cast values to correct type.
*/
protected $casts = [
'sort_id' => 'int',
'permissions' => 'array',
];
public static array $validationRules = [
'name' => 'required|string|max:64',
'description' => 'nullable|string|max:255',
'sort_id' => 'sometimes|numeric',
];
public $timestamps = false;
/**
* Gets the permissions associated with an admin role.
*/
public function permissions(): HasMany
{
return $this->hasMany(Permission::class);
}
}

View file

@ -1,80 +0,0 @@
<?php
namespace Pterodactyl\Models;
use Ramsey\Uuid\Uuid;
use Illuminate\Http\Request;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @deprecated this class will be dropped in a future version, use the activity log
*/
class AuditLog extends Model
{
public const UPDATED_AT = null;
public static array $validationRules = [
'uuid' => 'required|uuid',
'action' => 'required|string|max:191',
'subaction' => 'nullable|string|max:191',
'device' => 'array',
'device.ip_address' => 'ip',
'device.user_agent' => 'string',
'metadata' => 'array',
];
protected $table = 'audit_logs';
protected bool $immutableDates = true;
protected $casts = [
'is_system' => 'bool',
'device' => 'array',
'metadata' => 'array',
];
protected $guarded = [
'id',
'created_at',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
/**
* Creates a new AuditLog model and returns it, attaching device information and the
* currently authenticated user if available. This model is not saved at this point, so
* you can always make modifications to it as needed before saving.
*
* @deprecated
*/
public static function instance(string $action, array $metadata, bool $isSystem = false): self
{
/** @var \Illuminate\Http\Request $request */
$request = Container::getInstance()->make('request');
if ($isSystem || !$request instanceof Request) {
$request = null;
}
return (new self())->fill([
'uuid' => Uuid::uuid4()->toString(),
'is_system' => $isSystem,
'user_id' => ($request && $request->user()) ? $request->user()->id : null,
'server_id' => null,
'action' => $action,
'device' => $request ? [
'ip_address' => $request->getClientIp() ?? '127.0.0.1',
'user_agent' => $request->userAgent() ?? '',
] : [],
'metadata' => $metadata,
]);
}
}

View file

@ -22,7 +22,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property \Carbon\CarbonImmutable $updated_at
* @property \Carbon\CarbonImmutable|null $deleted_at
* @property \Pterodactyl\Models\Server $server
* @property \Pterodactyl\Models\AuditLog[] $audits
*/
class Backup extends Model
{

View file

@ -3,7 +3,7 @@
namespace Pterodactyl\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int $id
@ -13,7 +13,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string $username
* @property string $password
* @property int|null $max_databases
* @property int|null $node_id
* @property \Carbon\CarbonImmutable $created_at
* @property \Carbon\CarbonImmutable $updated_at
*/
@ -41,7 +40,7 @@ class DatabaseHost extends Model
* Fields that are mass assignable.
*/
protected $fillable = [
'name', 'host', 'port', 'username', 'password', 'max_databases', 'node_id',
'name', 'host', 'port', 'username', 'password', 'max_databases',
];
/**
@ -50,7 +49,6 @@ class DatabaseHost extends Model
protected $casts = [
'id' => 'integer',
'max_databases' => 'integer',
'node_id' => 'integer',
];
/**
@ -62,17 +60,8 @@ class DatabaseHost extends Model
'port' => 'required|numeric|between:1,65535',
'username' => 'required|string|max:32',
'password' => 'nullable|string',
'node_id' => 'sometimes|nullable|integer|exists:nodes,id',
];
/**
* Gets the node associated with a database host.
*/
public function node(): BelongsTo
{
return $this->belongsTo(Node::class);
}
/**
* Gets the databases associated with this host.
*/
@ -80,4 +69,12 @@ class DatabaseHost extends Model
{
return $this->hasMany(Database::class);
}
/**
* Returns the nodes that a database host is assigned to.
*/
public function nodes(): BelongsToMany
{
return $this->belongsToMany(Node::class);
}
}

View file

@ -20,15 +20,14 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property array|null $file_denylist
* @property string|null $config_files
* @property string|null $config_startup
* @property string|null $config_logs
* @property string|null $config_stop
* @property int|null $config_from
* @property string|null $startup
* @property bool $script_is_privileged
* @property string|null $script_install
* @property string $script_entry
* @property string $script_container
* @property int|null $copy_script_from
* @property ?string $script_entry
* @property ?string $script_container
* @property ?int $copy_script_from
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property string|null $copy_script_install
@ -36,7 +35,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string $copy_script_container
* @property string|null $inherit_config_files
* @property string|null $inherit_config_startup
* @property string|null $inherit_config_logs
* @property string|null $inherit_config_stop
* @property string $inherit_file_denylist
* @property array|null $inherit_features
@ -88,7 +86,6 @@ class Egg extends Model
'file_denylist',
'config_files',
'config_startup',
'config_logs',
'config_stop',
'config_from',
'startup',
@ -128,7 +125,6 @@ class Egg extends Model
'config_from' => 'sometimes|bail|nullable|numeric|exists:eggs,id',
'config_stop' => 'required_without:config_from|nullable|string|max:191',
'config_startup' => 'required_without:config_from|nullable|json',
'config_logs' => 'required_without:config_from|nullable|json',
'config_files' => 'required_without:config_from|nullable|json',
'update_url' => 'sometimes|nullable|string',
'force_outgoing_ip' => 'sometimes|boolean',
@ -139,7 +135,6 @@ class Egg extends Model
'file_denylist' => null,
'config_stop' => null,
'config_startup' => null,
'config_logs' => null,
'config_files' => null,
'update_url' => null,
];
@ -207,18 +202,6 @@ class Egg extends Model
return $this->configFrom->config_startup;
}
/**
* Return the log reading configuration for an egg.
*/
public function getInheritConfigLogsAttribute(): ?string
{
if (!is_null($this->config_logs) || is_null($this->config_from)) {
return $this->config_logs;
}
return $this->configFrom->config_logs;
}
/**
* Return the stop command configuration for an egg.
*/

View file

@ -2,8 +2,8 @@
namespace Pterodactyl\Models;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
@ -18,8 +18,9 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @property \Carbon\CarbonImmutable $created_at
* @property \Carbon\CarbonImmutable $updated_at
* @property bool $required
* @property \Pterodactyl\Models\Egg $egg
* @property \Pterodactyl\Models\ServerVariable $serverVariable
* @property Egg $egg
* @property ServerVariable $serverVariables
* @property string $field_type
*
* The "server_value" variable is only present on the object if you've loaded this model
* using the server relationship.
@ -80,15 +81,18 @@ class EggVariable extends Model
return in_array('required', explode('|', $this->rules));
}
public function egg(): HasOne
/**
* Returns the egg that this variable belongs to.
*/
public function egg(): BelongsTo
{
return $this->hasOne(Egg::class);
return $this->belongsTo(Egg::class);
}
/**
* Return server variables associated with this variable.
*/
public function serverVariable(): HasMany
public function serverVariables(): HasMany
{
return $this->hasMany(ServerVariable::class, 'variable_id');
}

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Models\Filters;
use BadMethodCallException;
use Spatie\QueryBuilder\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
@ -17,7 +16,7 @@ class AdminServerFilter implements Filter
public function __invoke(Builder $query, $value, string $property)
{
if ($query->getQuery()->from !== 'servers') {
throw new BadMethodCallException('Cannot use the AdminServerFilter against a non-server model.');
throw new \BadMethodCallException('Cannot use the AdminServerFilter against a non-server model.');
}
$query
->select('servers.*')

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Models\Filters;
use BadMethodCallException;
use Illuminate\Support\Str;
use Spatie\QueryBuilder\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
@ -25,7 +24,7 @@ class MultiFieldServerFilter implements Filter
public function __invoke(Builder $query, $value, string $property)
{
if ($query->getQuery()->from !== 'servers') {
throw new BadMethodCallException('Cannot use the MultiFieldServerFilter against a non-server model.');
throw new \BadMethodCallException('Cannot use the MultiFieldServerFilter against a non-server model.');
}
if (preg_match(self::IPV4_REGEX, $value) || preg_match('/^:\d{1,5}$/', $value)) {

View file

@ -155,6 +155,7 @@ abstract class Model extends IlluminateModel
return;
}
/** @var \Illuminate\Validation\Validator $validator */
$validator = $this->getValidator();
$validator->setData(
// Trying to do self::toArray() here will leave out keys based on the whitelist/blacklist

View file

@ -6,6 +6,7 @@ use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Illuminate\Container\Container;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -18,26 +19,34 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
* @property string $name
* @property string|null $description
* @property int $location_id
* @property string $fqdn
* @property int|null $database_host_id
* @property string $scheme
* @property string $fqdn
* @property int $listen_port_http
* @property int $listen_port_sftp
* @property int $public_port_http
* @property int $public_port_sftp
* @property bool $behind_proxy
* @property bool $maintenance_mode
* @property int $memory
* @property int $memory_overallocate
* @property int $sum_memory
* @property int $disk
* @property int $disk_overallocate
* @property int $sum_disk
* @property int $upload_size
* @property string $daemon_token_id
* @property string $daemon_token
* @property int $daemonListen
* @property int $daemonSFTP
* @property string $daemonBase
* @property string $daemon_base
* @property int $servers_count
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property \Pterodactyl\Models\Location $location
* @property \Pterodactyl\Models\Mount[]|\Illuminate\Database\Eloquent\Collection $mounts
* @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
* @property \Pterodactyl\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
* @property Allocation[]|Collection $allocations
* @property \Pterodactyl\Models\DatabaseHost|null $databaseHost
* @property Location $location
* @property Mount[]|Collection $mounts
* @property int[]|\Illuminate\Support\Collection $ports
* @property Server[]|Collection $servers
*/
class Node extends Model
{
@ -49,6 +58,11 @@ class Node extends Model
*/
public const RESOURCE_NAME = 'node';
/**
* The default location of server files on the Wings instance.
*/
public const DEFAULT_DAEMON_BASE = '/var/lib/pterodactyl/volumes';
public const DAEMON_TOKEN_ID_LENGTH = 16;
public const DAEMON_TOKEN_LENGTH = 64;
@ -67,10 +81,13 @@ class Node extends Model
*/
protected $casts = [
'location_id' => 'integer',
'database_host_id' => 'integer',
'listen_port_http' => 'integer',
'listen_port_sftp' => 'integer',
'public_port_http' => 'integer',
'public_port_sftp' => 'integer',
'memory' => 'integer',
'disk' => 'integer',
'daemonListen' => 'integer',
'daemonSFTP' => 'integer',
'behind_proxy' => 'boolean',
'public' => 'boolean',
'maintenance_mode' => 'boolean',
@ -80,11 +97,11 @@ class Node extends Model
* Fields that are mass assignable.
*/
protected $fillable = [
'public', 'name', 'location_id',
'public', 'name', 'location_id', 'database_host_id',
'listen_port_http', 'listen_port_sftp', 'public_port_http', 'public_port_sftp',
'fqdn', 'scheme', 'behind_proxy',
'memory', 'memory_overallocate', 'disk',
'disk_overallocate', 'upload_size', 'daemonBase',
'daemonSFTP', 'daemonListen',
'disk_overallocate', 'upload_size', 'daemon_base',
'description', 'maintenance_mode',
];
@ -92,17 +109,20 @@ class Node extends Model
'name' => 'required|regex:/^([\w .-]{1,100})$/',
'description' => 'string|nullable',
'location_id' => 'required|exists:locations,id',
'database_host_id' => 'sometimes|nullable|exists:database_hosts,id',
'public' => 'boolean',
'fqdn' => 'required|string',
'listen_port_http' => 'required|numeric|between:1,65535',
'listen_port_sftp' => 'required|numeric|between:1,65535',
'public_port_http' => 'required|numeric|between:1,65535',
'public_port_sftp' => 'required|numeric|between:1,65535',
'scheme' => 'required',
'behind_proxy' => 'boolean',
'memory' => 'required|numeric|min:1',
'memory_overallocate' => 'required|numeric|min:-1',
'disk' => 'required|numeric|min:1',
'disk_overallocate' => 'required|numeric|min:-1',
'daemonBase' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/',
'daemonSFTP' => 'required|numeric|between:1,65535',
'daemonListen' => 'required|numeric|between:1,65535',
'daemon_base' => 'sometimes|required|regex:/^([\/][\d\w.\-\/]+)$/',
'maintenance_mode' => 'boolean',
'upload_size' => 'int|between:1,1024',
];
@ -111,13 +131,15 @@ class Node extends Model
* Default values for specific columns that are generally not changed on base installs.
*/
protected $attributes = [
'listen_port_http' => 8080,
'listen_port_sftp' => 2022,
'public_port_http' => 8080,
'public_port_sftp' => 2022,
'public' => true,
'behind_proxy' => false,
'memory_overallocate' => 0,
'disk_overallocate' => 0,
'daemonBase' => '/var/lib/pterodactyl/volumes',
'daemonSFTP' => 2022,
'daemonListen' => 8080,
'daemon_base' => self::DEFAULT_DAEMON_BASE,
'maintenance_mode' => false,
];
@ -126,7 +148,7 @@ class Node extends Model
*/
public function getConnectionAddress(): string
{
return sprintf('%s://%s:%s', $this->scheme, $this->fqdn, $this->daemonListen);
return sprintf('%s://%s:%s', $this->scheme, $this->fqdn, $this->public_port_http);
}
/**
@ -141,7 +163,7 @@ class Node extends Model
'token' => Container::getInstance()->make(Encrypter::class)->decrypt($this->daemon_token),
'api' => [
'host' => '0.0.0.0',
'port' => $this->daemonListen,
'port' => $this->listen_port_http,
'ssl' => [
'enabled' => (!$this->behind_proxy && $this->scheme === 'https'),
'cert' => '/etc/letsencrypt/live/' . Str::lower($this->fqdn) . '/fullchain.pem',
@ -150,9 +172,9 @@ class Node extends Model
'upload_limit' => $this->upload_size,
],
'system' => [
'data' => $this->daemonBase,
'data' => $this->daemon_base,
'sftp' => [
'bind_port' => $this->daemonSFTP,
'bind_port' => $this->listen_port_sftp,
],
],
'allowed_mounts' => $this->mounts->pluck('source')->toArray(),
@ -186,25 +208,9 @@ class Node extends Model
);
}
public function mounts(): HasManyThrough
public function isUnderMaintenance(): bool
{
return $this->hasManyThrough(Mount::class, MountNode::class, 'node_id', 'id', 'id', 'mount_id');
}
/**
* Gets the location associated with a node.
*/
public function location(): BelongsTo
{
return $this->belongsTo(Location::class);
}
/**
* Gets the servers associated with a node.
*/
public function servers(): HasMany
{
return $this->hasMany(Server::class);
return $this->maintenance_mode;
}
/**
@ -215,13 +221,55 @@ class Node extends Model
return $this->hasMany(Allocation::class);
}
/**
* Returns the database host associated with a node.
*/
public function databaseHost(): BelongsTo
{
return $this->belongsTo(DatabaseHost::class);
}
/**
* Gets the location associated with a node.
*/
public function location(): BelongsTo
{
return $this->belongsTo(Location::class);
}
/**
* Returns a HasManyThrough relationship for all the mounts associated with a node.
*/
public function mounts(): HasManyThrough
{
return $this->hasManyThrough(Mount::class, MountNode::class, 'node_id', 'id', 'id', 'mount_id');
}
/**
* Gets the servers associated with a node.
*/
public function servers(): HasMany
{
return $this->hasMany(Server::class);
}
public function loadServerSums(): self
{
$this->loadSum('servers as sum_memory', 'memory');
$this->loadSum('servers as sum_disk', 'disk');
return $this;
}
/**
* Returns a boolean if the node is viable for an additional server to be placed on it.
*/
public function isViable(int $memory, int $disk): bool
public function isViable(int $memory = 0, int $disk = 0): bool
{
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
$this->loadServerSums();
$memoryLimit = $this->memory * (1.0 + ($this->memory_overallocate / 100.0));
$diskLimit = $this->disk * (1.0 + ($this->disk_overallocate / 100.0));
return ($this->sum_memory + $memory) <= $memoryLimit && ($this->sum_disk + $disk) <= $diskLimit;
}

View file

@ -31,11 +31,11 @@ use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;
* @property int $io
* @property int $cpu
* @property string|null $threads
* @property bool $oom_disabled
* @property bool $oom_killer
* @property int $allocation_id
* @property int $nest_id
* @property int $egg_id
* @property string $startup
* @property string|null $startup
* @property string $image
* @property int|null $allocation_limit
* @property int|null $database_limit
@ -55,6 +55,7 @@ use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;
* @property \Pterodactyl\Models\Egg|null $egg
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Mount[] $mounts
* @property int|null $mounts_count
* @property \Pterodactyl\Models\Location $location
* @property \Pterodactyl\Models\Nest $nest
* @property \Pterodactyl\Models\Node $node
* @property \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
@ -89,7 +90,7 @@ use Pterodactyl\Exceptions\Http\Server\ServerStateConflictException;
* @method static \Illuminate\Database\Eloquent\Builder|Server whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereNestId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereNodeId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomDisabled($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOomKiller($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereOwnerId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereSkipScripts($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereStartup($value)
@ -115,6 +116,7 @@ class Server extends Model
public const STATUS_INSTALLING = 'installing';
public const STATUS_INSTALL_FAILED = 'install_failed';
public const STATUS_REINSTALL_FAILED = 'reinstall_failed';
public const STATUS_SUSPENDED = 'suspended';
public const STATUS_RESTORING_BACKUP = 'restoring_backup';
@ -129,7 +131,7 @@ class Server extends Model
*/
protected $attributes = [
'status' => self::STATUS_INSTALLING,
'oom_disabled' => true,
'oom_killer' => false,
'installed_at' => null,
];
@ -160,7 +162,7 @@ class Server extends Model
'io' => 'required|numeric|between:10,1000',
'cpu' => 'required|numeric|min:0',
'threads' => 'nullable|regex:/^[0-9-,]+$/',
'oom_disabled' => 'sometimes|boolean',
'oom_killer' => 'sometimes|boolean',
'disk' => 'required|numeric|min:0',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'nest_id' => 'required|exists:nests,id',
@ -185,7 +187,7 @@ class Server extends Model
'disk' => 'integer',
'io' => 'integer',
'cpu' => 'integer',
'oom_disabled' => 'boolean',
'oom_killer' => 'boolean',
'allocation_id' => 'integer',
'nest_id' => 'integer',
'egg_id' => 'integer',
@ -354,6 +356,7 @@ class Server extends Model
{
if (
$this->isSuspended() ||
$this->node->isUnderMaintenance() ||
!$this->isInstalled() ||
$this->status === self::STATUS_RESTORING_BACKUP ||
!is_null($this->transfer)

View file

@ -2,6 +2,13 @@
namespace Pterodactyl\Models;
/**
* Pterodactyl\Models\Setting.
*
* @property int $id
* @property string $key
* @property string $value
*/
class Setting extends Model
{
/**

View file

@ -12,6 +12,8 @@ use Illuminate\Database\Eloquent\Builder;
use Webauthn\PublicKeyCredentialUserEntity;
use Pterodactyl\Models\Traits\HasAccessTokens;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Pterodactyl\Traits\Helpers\AvailableLanguages;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\Access\Authorizable;
@ -29,11 +31,10 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
* @property string $uuid
* @property string $username
* @property string $email
* @property string|null $name_first
* @property string|null $name_last
* @property string $password
* @property string|null $remember_token
* @property string $language
* @property int|null $admin_role_id
* @property bool $root_admin
* @property bool $use_totp
* @property string|null $totp_secret
@ -41,9 +42,12 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
* @property bool $gravatar
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string $avatar_url
* @property string|null $admin_role_name
* @property string $md5
* @property \Pterodactyl\Models\AdminRole|null $adminRole
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\ApiKey[] $apiKeys
* @property int|null $api_keys_count
* @property string $name
* @property \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
* @property int|null $notifications_count
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\RecoveryToken[] $recoveryTokens
@ -79,7 +83,9 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
* @method static Builder|User whereUsername($value)
* @method static Builder|User whereUuid($value)
*
* @mixin \Eloquent
* @mixin \Barryvdh\LaravelIdeHelper\Eloquent
* @mixin \Illuminate\Database\Query\Builder
* @mixin \Illuminate\Database\Eloquent\Builder
*/
class User extends Model implements
AuthenticatableContract,
@ -119,8 +125,6 @@ class User extends Model implements
'external_id',
'username',
'email',
'name_first',
'name_last',
'password',
'language',
'use_totp',
@ -165,8 +169,6 @@ class User extends Model implements
'email' => 'required|email|between:1,191|unique:users,email',
'external_id' => 'sometimes|nullable|string|max:191|unique:users,external_id',
'username' => 'required|between:1,191|unique:users,username',
'name_first' => 'required|string|between:1,191',
'name_last' => 'required|string|between:1,191',
'password' => 'sometimes|nullable|string',
'root_admin' => 'boolean',
'language' => 'string',
@ -193,7 +195,9 @@ class User extends Model implements
*/
public function toReactObject(): array
{
return Collection::make($this->toArray())->except(['id', 'external_id'])->toArray();
return Collection::make($this->append(['avatar_url', 'admin_role_name'])->toArray())
->except(['id', 'external_id', 'admin_role', 'admin_role_id'])
->toArray();
}
/**
@ -219,20 +223,39 @@ class User extends Model implements
$this->attributes['username'] = mb_strtolower($value);
}
/**
* Return a concatenated result for the accounts full name.
*/
public function getNameAttribute(): string
public function avatarUrl(): Attribute
{
return trim($this->name_first . ' ' . $this->name_last);
return Attribute::make(
get: fn () => 'https://www.gravatar.com/avatar/' . $this->md5 . '.jpg',
);
}
public function adminRoleName(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->adminRole) ? ($this->root_admin ? 'None' : null) : $this->adminRole->name,
);
}
public function md5(): Attribute
{
return Attribute::make(
get: fn () => md5(strtolower($this->email)),
);
}
/**
* Returns all servers that a user owns.
* Returns all the activity logs where this user is the subject not to
* be confused by activity logs where this user is the _actor_.
*/
public function servers(): HasMany
public function activity(): MorphToMany
{
return $this->hasMany(Server::class, 'owner_id');
return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');
}
public function adminRole(): HasOne
{
return $this->hasOne(AdminRole::class, 'id', 'admin_role_id');
}
public function apiKeys(): HasMany
@ -246,6 +269,11 @@ class User extends Model implements
return $this->hasMany(RecoveryToken::class);
}
public function servers(): HasMany
{
return $this->hasMany(Server::class, 'owner_id');
}
public function sshKeys(): HasMany
{
return $this->hasMany(UserSSHKey::class);
@ -256,15 +284,6 @@ class User extends Model implements
return $this->hasMany(SecurityKey::class);
}
/**
* Returns all the activity logs where this user is the subject not to
* be confused by activity logs where this user is the _actor_.
*/
public function activity(): MorphToMany
{
return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');
}
/**
* Returns all the servers that a user can access by way of being the owner of the
* server, or because they are assigned as a subuser for that server.