Add PHP 8.4 Support (#858)

* Add php 8.4

* Update ide helper

* Add php 8.4

* Update laravel sanctum

* Update laravel framework

* Hash rounds were increased

* This is always false

* Extend model now

* This does nothing

* Move model validation methods to trait

* Remove base model

* Backup routes were previously referenced by uuids

* Remove commented code

* Upgrade laravel/framework

* Fix migration

* Update ide helper

* Update sanctum

* Add version to composer

* Add this back in, fixed

* Make this protected to be safer
This commit is contained in:
Lance Pioch 2025-01-30 16:39:00 -05:00 committed by GitHub
parent 20125dbc6f
commit 635cc6a029
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 245 additions and 275 deletions

View File

@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3]
php: [8.2, 8.3, 8.4]
database: ["mysql:8"]
services:
database:
@ -86,7 +86,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3]
php: [8.2, 8.3, 8.4]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
services:
database:
@ -159,7 +159,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3]
php: [8.2, 8.3, 8.4]
env:
APP_ENV: testing
APP_DEBUG: "false"

View File

@ -0,0 +1,16 @@
<?php
namespace App\Contracts;
use Illuminate\Validation\Validator;
interface Validatable
{
public function getValidator(): Validator;
public static function getRules(): array;
public static function getRulesForField(string $field): array;
public function validate(): void;
}

View File

@ -4,7 +4,6 @@ namespace App\Http\Requests\Api\Application;
use Webmozart\Assert\Assert;
use App\Models\ApiKey;
use Laravel\Sanctum\TransientToken;
use Illuminate\Validation\Validator;
use Illuminate\Database\Eloquent\Model;
use App\Services\Acl\Api\AdminAcl;
@ -38,9 +37,6 @@ abstract class ApplicationApiRequest extends FormRequest
}
$token = $this->user()->currentAccessToken();
if ($token instanceof TransientToken) {
return true;
}
/** @var ApiKey $token */
if ($token->key_type === ApiKey::TYPE_ACCOUNT) {

View File

@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\HasValidation;
use Carbon\Carbon;
use Illuminate\Support\Facades\Event;
use App\Events\ActivityLogged;
@ -12,7 +13,7 @@ use Illuminate\Database\Eloquent\MassPrunable;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Model as IlluminateModel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
/**
@ -28,12 +29,12 @@ use Illuminate\Support\Str;
* @property int|null $api_key_id
* @property \Illuminate\Support\Collection|null $properties
* @property \Carbon\Carbon $timestamp
* @property IlluminateModel|\Eloquent $actor
* @property Model|\Eloquent $actor
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\ActivityLogSubject[] $subjects
* @property int|null $subjects_count
* @property \App\Models\ApiKey|null $apiKey
*
* @method static Builder|ActivityLog forActor(\Illuminate\Database\Eloquent\Model $actor)
* @method static Builder|ActivityLog forActor(Model $actor)
* @method static Builder|ActivityLog forEvent(string $action)
* @method static Builder|ActivityLog newModelQuery()
* @method static Builder|ActivityLog newQuery()
@ -51,6 +52,7 @@ use Illuminate\Support\Str;
*/
class ActivityLog extends Model implements HasIcon, HasLabel
{
use HasValidation;
use MassPrunable;
public const RESOURCE_NAME = 'activity_log';
@ -109,7 +111,7 @@ class ActivityLog extends Model implements HasIcon, HasLabel
/**
* Scopes a query to only return results where the actor is a given model.
*/
public function scopeForActor(Builder $builder, IlluminateModel $actor): Builder
public function scopeForActor(Builder $builder, Model $actor): Builder
{
return $builder->whereMorphedTo('actor', $actor);
}

View File

@ -3,7 +3,10 @@
namespace App\Models;
use App\Exceptions\Service\Allocation\ServerUsingAllocationException;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
@ -40,17 +43,15 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
*/
class Allocation extends Model
{
use HasFactory;
use HasValidation;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal. Also used as name for api key permissions.
*/
public const RESOURCE_NAME = 'allocation';
/**
* The table associated with the model.
*/
protected $table = 'allocations';
/**
* Fields that are not mass assignable.
*/
@ -81,11 +82,6 @@ class Allocation extends Model
];
}
public function getRouteKeyName(): string
{
return $this->getKeyName();
}
/**
* Accessor to automatically provide the IP alias if defined.
*/

View File

@ -3,7 +3,10 @@
namespace App\Models;
use App\Services\Acl\Api\AdminAcl;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;
use Laravel\Sanctum\PersonalAccessToken;
use Webmozart\Assert\Assert;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -47,8 +50,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApiKey whereUserId($value)
*/
class ApiKey extends Model
class ApiKey extends PersonalAccessToken
{
use HasFactory;
use HasValidation;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
@ -75,11 +81,6 @@ class ApiKey extends Model
*/
public const KEY_LENGTH = 32;
/**
* The table associated with the model.
*/
protected $table = 'api_keys';
/**
* Fields that are mass assignable.
*/
@ -148,13 +149,9 @@ class ApiKey extends Model
return $this->belongsTo(User::class);
}
/**
* Required for support with Laravel Sanctum.
*
* @see \Laravel\Sanctum\Guard::supportsTokens()
*/
public function tokenable(): BelongsTo
public function tokenable()
{
// @phpstan-ignore-next-line
return $this->user();
}
@ -178,11 +175,11 @@ class ApiKey extends Model
Role::RESOURCE_NAME,
];
private static array $customResourceNames = [];
protected static array $customResourceNames = [];
public static function registerCustomResourceName(string $resourceName): void
{
$customResourceNames[] = $resourceName;
static::$customResourceNames[] = $resourceName;
}
/**
@ -195,11 +192,14 @@ class ApiKey extends Model
/**
* Finds the model matching the provided token.
*
* @param string $token
*/
public static function findToken(string $token): ?self
public static function findToken($token): ?self
{
$identifier = substr($token, 0, self::IDENTIFIER_LENGTH);
/** @var static|null $model */
$model = static::where('identifier', $identifier)->first();
if (!is_null($model) && $model->token === substr($token, strlen($identifier))) {
return $model;

View File

@ -2,6 +2,10 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Ramsey\Uuid\Uuid;
use Illuminate\Http\Request;
use Illuminate\Container\Container;
@ -10,8 +14,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @deprecated this class will be dropped in a future version, use the activity log
*/
class AuditLog extends Model
class AuditLog extends Model implements Validatable
{
use HasFactory;
use HasValidation;
public const UPDATED_AT = null;
public static array $validationRules = [
@ -24,8 +31,6 @@ class AuditLog extends Model
'metadata' => 'array',
];
protected $table = 'audit_logs';
protected $guarded = [
'id',
'created_at',

View File

@ -2,6 +2,10 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Eloquent\BackupQueryBuilder;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -25,8 +29,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property \App\Models\Server $server
* @property \App\Models\AuditLog[] $audits
*/
class Backup extends Model
class Backup extends Model implements Validatable
{
use HasFactory;
use HasValidation;
use SoftDeletes;
public const RESOURCE_NAME = 'backup';
@ -35,8 +41,6 @@ class Backup extends Model
public const ADAPTER_AWS_S3 = 's3';
protected $table = 'backups';
protected $attributes = [
'is_successful' => false,
'is_locked' => false,

View File

@ -2,7 +2,11 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\DB;
@ -21,8 +25,11 @@ use Illuminate\Support\Facades\DB;
* @property \App\Models\Server $server
* @property \App\Models\DatabaseHost $host
*/
class Database extends Model
class Database extends Model implements Validatable
{
use HasFactory;
use HasValidation;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal. Also used as name for api key permissions.
@ -31,11 +38,6 @@ class Database extends Model
public const DEFAULT_CONNECTION_NAME = 'dynamic';
/**
* The table associated with the model.
*/
protected $table = 'databases';
/**
* The attributes excluded from the model's JSON form.
*/
@ -68,11 +70,6 @@ class Database extends Model
];
}
public function getRouteKeyName(): string
{
return $this->getKeyName();
}
/**
* Gets the host database server associated with a database.
*/

View File

@ -2,6 +2,10 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -21,19 +25,17 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Database[] $databases
* @property int|null $databases_count
*/
class DatabaseHost extends Model
class DatabaseHost extends Model implements Validatable
{
use HasFactory;
use HasValidation;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal. Also used as name for api key permissions.
*/
public const RESOURCE_NAME = 'database_host';
/**
* The table associated with the model.
*/
protected $table = 'database_hosts';
/**
* The attributes excluded from the model's JSON form.
*/
@ -70,11 +72,6 @@ class DatabaseHost extends Model
];
}
public function getRouteKeyName(): string
{
return 'id';
}
public function nodes(): BelongsToMany
{
return $this->belongsToMany(Node::class);

View File

@ -2,8 +2,12 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Exceptions\Service\Egg\HasChildrenException;
use App\Exceptions\Service\HasActiveServersException;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
@ -49,8 +53,11 @@ use Illuminate\Support\Str;
* @property \App\Models\Egg|null $scriptFrom
* @property \App\Models\Egg|null $configFrom
*/
class Egg extends Model
class Egg extends Model implements Validatable
{
use HasFactory;
use HasValidation;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal. Also used as name for api key permissions.
@ -75,11 +82,6 @@ class Egg extends Model
public const FEATURE_FASTDL = 'fastdl';
/**
* The table associated with the model.
*/
protected $table = 'eggs';
/**
* Fields that are not mass assignable.
*/
@ -167,11 +169,6 @@ class Egg extends Model
});
}
public function getRouteKeyName(): string
{
return 'id';
}
/**
* Returns the install script for the egg; if egg is copying from another
* it will return the copied script.

View File

@ -2,6 +2,10 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -26,8 +30,11 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* using the server relationship.
* @property string|null $server_value
*/
class EggVariable extends Model
class EggVariable extends Model implements Validatable
{
use HasFactory;
use HasValidation;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
@ -39,11 +46,6 @@ class EggVariable extends Model
*/
public const RESERVED_ENV_NAMES = 'P_SERVER_UUID,P_SERVER_ALLOCATION_LIMIT,SERVER_MEMORY,SERVER_IP,SERVER_PORT,ENV,HOME,USER,STARTUP,MODIFIED_STARTUP,SERVER_UUID,UUID,INTERNAL_IP';
/**
* The table associated with the model.
*/
protected $table = 'egg_variables';
/**
* Fields that are not mass assignable.
*/

View File

@ -7,6 +7,7 @@ use App\Repositories\Daemon\DaemonFileRepository;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Sushi\Sushi;
/**

View File

@ -2,6 +2,9 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rules\NotIn;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -18,19 +21,16 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
* @property \App\Models\Node[]|\Illuminate\Database\Eloquent\Collection $nodes
* @property \App\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
*/
class Mount extends Model
class Mount extends Model implements Validatable
{
use HasValidation { getRules as getValidationRules; }
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
*/
public const RESOURCE_NAME = 'mount';
/**
* The table associated with the model.
*/
protected $table = 'mounts';
/**
* Fields that are not mass assignable.
*/
@ -54,7 +54,7 @@ class Mount extends Model
*/
public static function getRules(): array
{
$rules = parent::getRules();
$rules = self::getValidationRules();
$rules['source'][] = new NotIn(Mount::$invalidSourcePaths);
$rules['target'][] = new NotIn(Mount::$invalidTargetPaths);
@ -115,9 +115,4 @@ class Mount extends Model
{
return $this->belongsToMany(Server::class);
}
public function getRouteKeyName(): string
{
return 'id';
}
}

View File

@ -2,9 +2,13 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Exceptions\Service\HasActiveServersException;
use App\Repositories\Daemon\DaemonConfigurationRepository;
use App\Traits\HasValidation;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
@ -47,8 +51,10 @@ use Symfony\Component\Yaml\Yaml;
* @property \App\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
* @property int|null $allocations_count
*/
class Node extends Model
class Node extends Model implements Validatable
{
use HasFactory;
use HasValidation;
use Notifiable;
/**
@ -61,11 +67,6 @@ class Node extends Model
public const DAEMON_TOKEN_LENGTH = 64;
/**
* The table associated with the model.
*/
protected $table = 'nodes';
/**
* The attributes excluded from the model's JSON form.
*/
@ -146,11 +147,6 @@ class Node extends Model
public int $servers_sum_cpu = 0;
public function getRouteKeyName(): string
{
return 'id';
}
protected static function booted(): void
{
static::creating(function (self $node) {

View File

@ -2,10 +2,15 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class Permission extends Model
class Permission extends Model implements Validatable
{
use HasValidation;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
@ -95,16 +100,8 @@ class Permission extends Model
public const ACTION_ACTIVITY_READ = 'activity.read';
/**
* Should timestamps be used on this model.
*/
public $timestamps = false;
/**
* The table associated with the model.
*/
protected $table = 'permissions';
/**
* Fields that are not mass assignable.
*/

View File

@ -2,6 +2,9 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
@ -11,8 +14,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property \Carbon\CarbonImmutable $created_at
* @property \App\Models\User $user
*/
class RecoveryToken extends Model
class RecoveryToken extends Model implements Validatable
{
use HasValidation;
/**
* There are no updates to this model, only inserts and deletes.
*/

View File

@ -2,7 +2,11 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Helpers\Utilities;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -25,19 +29,17 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property \App\Models\Server $server
* @property \App\Models\Task[]|\Illuminate\Support\Collection $tasks
*/
class Schedule extends Model
class Schedule extends Model implements Validatable
{
use HasFactory;
use HasValidation;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
*/
public const RESOURCE_NAME = 'server_schedule';
/**
* The table associated with the model.
*/
protected $table = 'schedules';
/**
* Always return the tasks associated with this schedule.
*/
@ -101,11 +103,6 @@ class Schedule extends Model
];
}
public function getRouteKeyName(): string
{
return $this->getKeyName();
}
/**
* Returns the schedule's execution crontab entry as a string.
*

View File

@ -2,12 +2,16 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Enums\ContainerStatus;
use App\Enums\ServerResourceType;
use App\Enums\ServerState;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Traits\HasValidation;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Notifications\Notifiable;
@ -121,8 +125,10 @@ use App\Services\Subusers\SubuserDeletionService;
* @method static \Illuminate\Database\Eloquent\Builder|Server wherePorts($value)
* @method static \Illuminate\Database\Eloquent\Builder|Server whereUuidShort($value)
*/
class Server extends Model
class Server extends Model implements Validatable
{
use HasFactory;
use HasValidation;
use Notifiable;
/**
@ -131,11 +137,6 @@ class Server extends Model
*/
public const RESOURCE_NAME = 'server';
/**
* The table associated with the model.
*/
protected $table = 'servers';
/**
* Default values when creating the model. We want to switch to disabling OOM killer
* on server instances unless the user specifies otherwise in the request.
@ -363,11 +364,6 @@ class Server extends Model
return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');
}
public function getRouteKeyName(): string
{
return 'id';
}
public function resolveRouteBinding($value, $field = null): ?self
{
return match ($field) {

View File

@ -2,6 +2,9 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -22,19 +25,16 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property \App\Models\Node $oldNode
* @property \App\Models\Node $newNode
*/
class ServerTransfer extends Model
class ServerTransfer extends Model implements Validatable
{
use HasValidation;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
*/
public const RESOURCE_NAME = 'server_transfer';
/**
* The table associated with the model.
*/
protected $table = 'server_transfers';
/**
* Fields that are not mass assignable.
*/

View File

@ -2,6 +2,9 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
@ -14,16 +17,16 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property \App\Models\EggVariable $variable
* @property \App\Models\Server $server
*/
class ServerVariable extends Model
class ServerVariable extends Model implements Validatable
{
use HasValidation;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
*/
public const RESOURCE_NAME = 'server_variable';
protected $table = 'server_variables';
protected $guarded = ['id', 'created_at', 'updated_at'];
public static array $validationRules = [

View File

@ -2,6 +2,10 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -16,8 +20,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property \App\Models\User $user
* @property \App\Models\Server $server
*/
class Subuser extends Model
class Subuser extends Model implements Validatable
{
use HasFactory;
use HasValidation;
use Notifiable;
/**
@ -26,11 +32,6 @@ class Subuser extends Model
*/
public const RESOURCE_NAME = 'server_subuser';
/**
* The table associated with the model.
*/
protected $table = 'subusers';
/**
* Fields that are not mass assignable.
*/

View File

@ -2,6 +2,10 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -19,8 +23,11 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property \App\Models\Schedule $schedule
* @property \App\Models\Server $server
*/
class Task extends Model
class Task extends Model implements Validatable
{
use HasFactory;
use HasValidation;
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
@ -38,11 +45,6 @@ class Task extends Model
public const ACTION_DELETE_FILES = 'delete_files';
/**
* The table associated with the model.
*/
protected $table = 'tasks';
/**
* Relationships to be updated when this model is updated.
*/
@ -92,11 +94,6 @@ class Task extends Model
];
}
public function getRouteKeyName(): string
{
return $this->getKeyName();
}
/**
* Return the schedule that a task belongs to.
*/

View File

@ -2,15 +2,19 @@
namespace App\Models;
use App\Contracts\Validatable;
use App\Exceptions\DisplayException;
use App\Rules\Username;
use App\Facades\Activity;
use App\Traits\HasValidation;
use DateTimeZone;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
use Filament\Models\Contracts\HasName;
use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
@ -28,7 +32,6 @@ use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use App\Notifications\SendPasswordReset as ResetPasswordNotification;
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Model as IlluminateModel;
use ResourceBundle;
use Spatie\Permission\Traits\HasRoles;
@ -85,13 +88,15 @@ use Spatie\Permission\Traits\HasRoles;
* @method static Builder|User whereUsername($value)
* @method static Builder|User whereUuid($value)
*/
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAvatar, HasName, HasTenants
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, FilamentUser, HasAvatar, HasName, HasTenants, Validatable
{
use Authenticatable;
use Authorizable {can as protected canned; }
use Authorizable { can as protected canned; }
use CanResetPassword;
use HasAccessTokens;
use HasFactory;
use HasRoles;
use HasValidation { getRules as getValidationRules; }
use Notifiable;
public const USER_LEVEL_USER = 0;
@ -104,16 +109,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
*/
public const RESOURCE_NAME = 'user';
/**
* Level of servers to display when using access() on a user.
*/
protected string $accessLevel = 'all';
/**
* The table associated with the model.
*/
protected $table = 'users';
/**
* A list of mass-assignable variables.
*/
@ -191,18 +186,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
});
}
public function getRouteKeyName(): string
{
return 'id';
}
/**
* Implement language verification by overriding Eloquence's gather
* rules function.
*/
public static function getRules(): array
{
$rules = parent::getRules();
$rules = self::getValidationRules();
$rules['language'][] = new In(array_values(array_filter(ResourceBundle::getLocales(''), fn ($lang) => preg_match('/^[a-z]{2}$/', $lang))));
$rules['timezone'][] = new In(DateTimeZone::listIdentifiers());
@ -407,7 +397,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return 'https://gravatar.com/avatar/' . md5(strtolower($this->email));
}
public function canTarget(IlluminateModel $user): bool
public function canTarget(Model $user): bool
{
if ($this->isRootAdmin()) {
return true;
@ -421,7 +411,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return $this->accessibleServers()->get();
}
public function canAccessTenant(IlluminateModel $tenant): bool
public function canAccessTenant(Model $tenant): bool
{
if ($tenant instanceof Server) {
if ($this->canned('view server', $tenant) || $tenant->owner_id === $this->id) {

View File

@ -2,6 +2,9 @@
namespace App\Models;
use App\Traits\HasValidation;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -36,6 +39,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
*/
class UserSSHKey extends Model
{
use HasFactory;
use HasValidation;
use SoftDeletes;
public const RESOURCE_NAME = 'ssh_key';

View File

@ -0,0 +1,20 @@
<?php
namespace App\Observers;
use App\Contracts\Validatable as HasValidationContract;
use App\Exceptions\Model\DataValidationException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\ValidationException;
class ValidationObserver
{
public function saving(Model&HasValidationContract $model): void
{
try {
$model->validate();
} catch (ValidationException $exception) {
throw new DataValidationException($exception->validator, $model);
}
}
}

View File

@ -1,69 +1,21 @@
<?php
namespace App\Models;
namespace App\Traits;
use App\Observers\ValidationObserver;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Container\Container;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use App\Exceptions\Model\DataValidationException;
use Illuminate\Database\Eloquent\Model as IlluminateModel;
use Illuminate\Validation\Factory as ValidationFactory;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Validator;
abstract class Model extends IlluminateModel
#[ObservedBy([ValidationObserver::class])]
trait HasValidation
{
use HasFactory;
/**
* Determines if the model should undergo data validation before it is saved
* to the database.
*/
protected bool $skipValidation = false;
protected static ValidationFactory $validatorFactory;
public static array $validationRules = [];
/**
* Listen for the model saving event and fire off the validation
* function before it is saved.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected static function boot(): void
{
parent::boot();
static::$validatorFactory = Container::getInstance()->make(ValidationFactory::class);
static::saving(function (Model $model) {
try {
$model->validate();
} catch (ValidationException $exception) {
throw new DataValidationException($exception->validator, $model);
}
return true;
});
}
/**
* Returns the model key to use for route model binding. By default, we'll
* assume every model uses a UUID field for this. If the model does not have
* a UUID and is using a different key it should be specified on the model
* itself.
*
* You may also optionally override this on a per-route basis by declaring
* the key name in the URL definition, like "{user:id}".
*/
public function getRouteKeyName(): string
{
return 'uuid';
}
/**
* Returns the validator instance used by this model.
*/
@ -71,7 +23,9 @@ abstract class Model extends IlluminateModel
{
$rules = $this->exists ? static::getRulesForUpdate($this) : static::getRules();
return static::$validatorFactory->make([], $rules);
$validatorFactory = Container::getInstance()->make(ValidationFactory::class);
return $validatorFactory->make([], $rules);
}
/**
@ -132,7 +86,7 @@ abstract class Model extends IlluminateModel
*/
public function validate(): void
{
if ($this->skipValidation) {
if (isset($this->skipValidation)) {
return;
}

View File

@ -2,7 +2,7 @@
"name": "pelican-dev/panel",
"description": "The free, open-source game management panel. Supporting Minecraft, Spigot, BungeeCord, and SRCDS servers.",
"require": {
"php": "^8.2 || ^8.3",
"php": "^8.2 || ^8.3 || ^8.4",
"ext-intl": "*",
"ext-json": "*",
"ext-mbstring": "*",

45
composer.lock generated
View File

@ -2951,16 +2951,16 @@
},
{
"name": "laravel/framework",
"version": "v11.37.0",
"version": "v11.40.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "6cb103d2024b087eae207654b3f4b26646119ba5"
"reference": "599a28196d284fee158cc10086fd56ac625ad7a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/6cb103d2024b087eae207654b3f4b26646119ba5",
"reference": "6cb103d2024b087eae207654b3f4b26646119ba5",
"url": "https://api.github.com/repos/laravel/framework/zipball/599a28196d284fee158cc10086fd56ac625ad7a3",
"reference": "599a28196d284fee158cc10086fd56ac625ad7a3",
"shasum": ""
},
"require": {
@ -2986,7 +2986,7 @@
"league/flysystem-local": "^3.25.1",
"league/uri": "^7.5.1",
"monolog/monolog": "^3.0",
"nesbot/carbon": "^2.72.2|^3.4",
"nesbot/carbon": "^2.72.6|^3.8.4",
"nunomaduro/termwind": "^2.0",
"php": "^8.2",
"psr/container": "^1.1.1|^2.0.1",
@ -3061,6 +3061,7 @@
"fakerphp/faker": "^1.24",
"guzzlehttp/promises": "^2.0.3",
"guzzlehttp/psr7": "^2.4",
"laravel/pint": "^1.18",
"league/flysystem-aws-s3-v3": "^3.25.1",
"league/flysystem-ftp": "^3.25.1",
"league/flysystem-path-prefixing": "^3.25.1",
@ -3161,7 +3162,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2025-01-02T20:10:21+00:00"
"time": "2025-01-24T16:17:42+00:00"
},
{
"name": "laravel/helpers",
@ -3281,16 +3282,16 @@
},
{
"name": "laravel/sanctum",
"version": "v4.0.3",
"version": "v4.0.7",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab"
"reference": "698064236a46df016e64a7eb059b1414e0b281df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/54aea9d13743ae8a6cdd3c28dbef128a17adecab",
"reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/698064236a46df016e64a7eb059b1414e0b281df",
"reference": "698064236a46df016e64a7eb059b1414e0b281df",
"shasum": ""
},
"require": {
@ -3341,7 +3342,7 @@
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2024-09-27T14:55:41+00:00"
"time": "2024-12-11T16:40:21+00:00"
},
{
"name": "laravel/serializable-closure",
@ -10881,16 +10882,16 @@
"packages-dev": [
{
"name": "barryvdh/laravel-ide-helper",
"version": "v3.2.2",
"version": "v3.3.0",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
"reference": "07e3bd8796f3d1414801a03d3783f9d3ec9efc08"
"reference": "b7675670f75914bf34afdea52a6c2fe3781f7c44"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/07e3bd8796f3d1414801a03d3783f9d3ec9efc08",
"reference": "07e3bd8796f3d1414801a03d3783f9d3ec9efc08",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/b7675670f75914bf34afdea52a6c2fe3781f7c44",
"reference": "b7675670f75914bf34afdea52a6c2fe3781f7c44",
"shasum": ""
},
"require": {
@ -10921,13 +10922,13 @@
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.2-dev"
},
"laravel": {
"providers": [
"Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider"
]
},
"branch-alias": {
"dev-master": "3.2-dev"
}
},
"autoload": {
@ -10959,7 +10960,7 @@
],
"support": {
"issues": "https://github.com/barryvdh/laravel-ide-helper/issues",
"source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.2.2"
"source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.3.0"
},
"funding": [
{
@ -10971,7 +10972,7 @@
"type": "github"
}
],
"time": "2024-10-29T14:00:16+00:00"
"time": "2024-12-18T08:24:19+00:00"
},
{
"name": "barryvdh/reflection-docblock",
@ -13969,7 +13970,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
@ -13980,6 +13981,6 @@
"ext-pdo": "*",
"ext-zip": "*"
},
"platform-dev": {},
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

View File

@ -25,7 +25,7 @@ return new class extends Migration
$table->json('permissions');
});
foreach (ApiKey::query() as $apiKey) {
foreach (ApiKey::all() as $apiKey) {
$permissions = [
Server::RESOURCE_NAME => intval($apiKey->r_servers ?? 0),
Node::RESOURCE_NAME => intval($apiKey->r_nodes ?? 0),

View File

@ -115,11 +115,11 @@ Route::prefix('/servers/{server:uuid}')->middleware([ServerSubject::class, Authe
Route::prefix('/backups')->group(function () {
Route::get('/', [Client\Servers\BackupController::class, 'index']);
Route::post('/', [Client\Servers\BackupController::class, 'store']);
Route::get('/{backup}', [Client\Servers\BackupController::class, 'view']);
Route::get('/{backup}/download', [Client\Servers\BackupController::class, 'download']);
Route::post('/{backup}/lock', [Client\Servers\BackupController::class, 'toggleLock']);
Route::post('/{backup}/restore', [Client\Servers\BackupController::class, 'restore']);
Route::delete('/{backup}', [Client\Servers\BackupController::class, 'delete']);
Route::get('/{backup:uuid}', [Client\Servers\BackupController::class, 'view']);
Route::get('/{backup:uuid}/download', [Client\Servers\BackupController::class, 'download']);
Route::post('/{backup:uuid}/lock', [Client\Servers\BackupController::class, 'toggleLock']);
Route::post('/{backup:uuid}/restore', [Client\Servers\BackupController::class, 'restore']);
Route::delete('/{backup:uuid}', [Client\Servers\BackupController::class, 'delete']);
});
Route::prefix('/startup')->group(function () {

View File

@ -24,7 +24,7 @@ Route::prefix('/servers/{server:uuid}')->group(function () {
});
Route::prefix('/backups')->group(function () {
Route::get('/{backup}', Remote\Backups\BackupRemoteUploadController::class);
Route::post('/{backup}', [Remote\Backups\BackupStatusController::class, 'index']);
Route::post('/{backup}/restore', [Remote\Backups\BackupStatusController::class, 'restore']);
Route::get('/{backup:uuid}', Remote\Backups\BackupRemoteUploadController::class);
Route::post('/{backup:uuid}', [Remote\Backups\BackupStatusController::class, 'index']);
Route::post('/{backup:uuid}/restore', [Remote\Backups\BackupStatusController::class, 'restore']);
});

View File

@ -100,7 +100,7 @@ class TwoFactorControllerTest extends ClientApiIntegrationTestCase
$tokens = RecoveryToken::query()->where('user_id', $user->id)->get();
$this->assertCount(10, $tokens);
$this->assertStringStartsWith('$2y$10$', $tokens[0]->token);
$this->assertStringStartsWith('$2y$', $tokens[0]->token);
// Ensure the recovery tokens that were created include a "created_at" timestamp value on them.
$this->assertNotNull($tokens[0]->created_at);