refactor: define explicit fillable attributes on all Eloquent models

Replace $guarded usage with explicit $fillable arrays across all models.
Sync fillable definitions with current database schema and add tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2026-03-29 21:25:41 +02:00
parent a5840501b4
commit 9f46586d4a
51 changed files with 1071 additions and 128 deletions

View file

@ -1158,7 +1158,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
$application->fill($request->all());
$application->fill($request->only($allowedFields));
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
$dockerComposeDomains = collect($request->docker_compose_domains);
@ -1385,7 +1385,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
$application->fill($request->all());
$application->fill($request->only($allowedFields));
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
@ -1585,7 +1585,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
$application->fill($request->all());
$application->fill($request->only($allowedFields));
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
@ -1772,7 +1772,7 @@ private function create_application(Request $request, $type)
}
$application = new Application;
$application->fill($request->all());
$application->fill($request->only($allowedFields));
$application->fqdn = $fqdn;
$application->ports_exposes = $port;
$application->build_pack = 'dockerfile';
@ -1884,7 +1884,7 @@ private function create_application(Request $request, $type)
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
$application->fill($request->all());
$application->fill($request->only($allowedFields));
$application->fqdn = $fqdn;
$application->build_pack = 'dockerimage';
$application->destination_id = $destination->id;
@ -2000,7 +2000,7 @@ private function create_application(Request $request, $type)
$service = new Service;
removeUnnecessaryFieldsFromRequest($request);
$service->fill($request->all());
$service->fill($request->only($allowedFields));
$service->docker_compose_raw = $dockerComposeRaw;
$service->environment_id = $environment->id;
@ -2760,7 +2760,7 @@ public function update_by_uuid(Request $request)
removeUnnecessaryFieldsFromRequest($request);
$data = $request->all();
$data = $request->only($allowedFields);
if ($requestHasDomains && $server->isProxyShouldRun()) {
data_set($data, 'fqdn', $domains);
}

View file

@ -1751,7 +1751,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('postgres_conf', $postgresConf);
}
$database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all());
$database = create_standalone_postgresql($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -1806,7 +1806,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mariadb_conf', $mariadbConf);
}
$database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all());
$database = create_standalone_mariadb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -1865,7 +1865,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mysql_conf', $mysqlConf);
}
$database = create_standalone_mysql($environment->id, $destination->uuid, $request->all());
$database = create_standalone_mysql($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -1921,7 +1921,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('redis_conf', $redisConf);
}
$database = create_standalone_redis($environment->id, $destination->uuid, $request->all());
$database = create_standalone_redis($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -1958,7 +1958,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
removeUnnecessaryFieldsFromRequest($request);
$database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all());
$database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -2007,7 +2007,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('keydb_conf', $keydbConf);
}
$database = create_standalone_keydb($environment->id, $destination->uuid, $request->all());
$database = create_standalone_keydb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -2043,7 +2043,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
], 422);
}
removeUnnecessaryFieldsFromRequest($request);
$database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all());
$database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
@ -2101,7 +2101,7 @@ public function create_database(Request $request, NewDatabaseTypes $type)
}
$request->offsetSet('mongo_conf', $mongoConf);
}
$database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all());
$database = create_standalone_mongodb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}

View file

@ -4,6 +4,7 @@
use App\Http\Controllers\Controller;
use App\Models\PrivateKey;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
@ -176,7 +177,7 @@ public function create_key(Request $request)
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
if ($return instanceof JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
@ -300,7 +301,7 @@ public function update_key(Request $request)
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
if ($return instanceof JsonResponse) {
return $return;
}
@ -330,7 +331,7 @@ public function update_key(Request $request)
'message' => 'Private Key not found.',
], 404);
}
$foundKey->update($request->all());
$foundKey->update($request->only($allowedFields));
return response()->json(serializeApiResponse([
'uuid' => $foundKey->uuid,

View file

@ -139,7 +139,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'status' => 'exited',
'started_at' => null,
@ -187,7 +187,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'name' => $newName,
'resource_id' => $newDatabase->id,
]);
@ -216,7 +216,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resource_id' => $newDatabase->id,
]);
$newStorage->save();
@ -229,7 +229,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'database_id' => $newDatabase->id,
'database_type' => $newDatabase->getMorphClass(),
@ -247,7 +247,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill($payload);
])->forceFill($payload);
$newEnvironmentVariable->save();
}
}
@ -258,7 +258,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination,
@ -276,7 +276,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => (string) new Cuid2,
'service_id' => $newService->id,
'team_id' => currentTeam()->id,
@ -290,7 +290,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resourceable_id' => $newService->id,
'resourceable_type' => $newService->getMorphClass(),
]);
@ -298,9 +298,9 @@ public function clone(string $type)
}
foreach ($newService->applications() as $application) {
$application->update([
$application->forceFill([
'status' => 'exited',
]);
])->save();
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@ -315,7 +315,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'name' => $newName,
'resource_id' => $application->id,
]);
@ -344,7 +344,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resource_id' => $application->id,
]);
$newStorage->save();
@ -352,9 +352,9 @@ public function clone(string $type)
}
foreach ($newService->databases() as $database) {
$database->update([
$database->forceFill([
'status' => 'exited',
]);
])->save();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@ -369,7 +369,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'name' => $newName,
'resource_id' => $database->id,
]);
@ -398,7 +398,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resource_id' => $database->id,
]);
$newStorage->save();
@ -411,7 +411,7 @@ public function clone(string $type)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'database_id' => $database->id,
'database_type' => $database->getMorphClass(),

View file

@ -7,9 +7,18 @@
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Jobs\VolumeCloneJob;
use App\Models\Application;
use App\Models\Environment;
use App\Models\Project;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDocker;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Models\SwarmDocker;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
@ -60,7 +69,7 @@ public function cloneTo($destination_id)
$uuid = (string) new Cuid2;
$server = $new_destination->server;
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
if ($this->resource->getMorphClass() === Application::class) {
$new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
$route = route('project.application.configuration', [
@ -71,21 +80,21 @@ public function cloneTo($destination_id)
return redirect()->to($route);
} elseif (
$this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class ||
$this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class ||
$this->resource->getMorphClass() === \App\Models\StandaloneMysql::class ||
$this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class ||
$this->resource->getMorphClass() === \App\Models\StandaloneRedis::class ||
$this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class ||
$this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
$this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
$this->resource->getMorphClass() === StandalonePostgresql::class ||
$this->resource->getMorphClass() === StandaloneMongodb::class ||
$this->resource->getMorphClass() === StandaloneMysql::class ||
$this->resource->getMorphClass() === StandaloneMariadb::class ||
$this->resource->getMorphClass() === StandaloneRedis::class ||
$this->resource->getMorphClass() === StandaloneKeydb::class ||
$this->resource->getMorphClass() === StandaloneDragonfly::class ||
$this->resource->getMorphClass() === StandaloneClickhouse::class
) {
$uuid = (string) new Cuid2;
$new_resource = $this->resource->replicate([
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'status' => 'exited',
@ -133,7 +142,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'name' => $newName,
'resource_id' => $new_resource->id,
]);
@ -162,7 +171,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resource_id' => $new_resource->id,
]);
$newStorage->save();
@ -175,7 +184,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'database_id' => $new_resource->id,
'database_type' => $new_resource->getMorphClass(),
@ -194,7 +203,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->fill($payload);
])->forceFill($payload);
$newEnvironmentVariable->save();
}
@ -211,7 +220,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'destination_id' => $new_destination->id,
@ -232,7 +241,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => (string) new Cuid2,
'service_id' => $new_resource->id,
'team_id' => currentTeam()->id,
@ -246,7 +255,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
]);
@ -254,9 +263,9 @@ public function cloneTo($destination_id)
}
foreach ($new_resource->applications() as $application) {
$application->update([
$application->forceFill([
'status' => 'exited',
]);
])->save();
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@ -271,7 +280,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'name' => $newName,
'resource_id' => $application->id,
]);
@ -296,9 +305,9 @@ public function cloneTo($destination_id)
}
foreach ($new_resource->databases() as $database) {
$database->update([
$database->forceFill([
'status' => 'exited',
]);
])->save();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@ -313,7 +322,7 @@ public function cloneTo($destination_id)
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'name' => $newName,
'resource_id' => $database->id,
]);
@ -354,9 +363,9 @@ public function moveTo($environment_id)
try {
$this->authorize('update', $this->resource);
$new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id);
$this->resource->update([
$this->resource->forceFill([
'environment_id' => $environment_id,
]);
])->save();
if ($this->resource->type() === 'application') {
$route = route('project.application.configuration', [
'project_uuid' => $new_environment->project->uuid,

View file

@ -118,7 +118,91 @@ class Application extends BaseModel
private static $parserVersion = '5';
protected $guarded = [];
protected $fillable = [
'name',
'description',
'fqdn',
'git_repository',
'git_branch',
'git_commit_sha',
'git_full_url',
'docker_registry_image_name',
'docker_registry_image_tag',
'build_pack',
'static_image',
'install_command',
'build_command',
'start_command',
'ports_exposes',
'ports_mappings',
'base_directory',
'publish_directory',
'health_check_enabled',
'health_check_path',
'health_check_port',
'health_check_host',
'health_check_method',
'health_check_return_code',
'health_check_scheme',
'health_check_response_text',
'health_check_interval',
'health_check_timeout',
'health_check_retries',
'health_check_start_period',
'health_check_type',
'health_check_command',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'status',
'preview_url_template',
'dockerfile',
'dockerfile_location',
'dockerfile_target_build',
'custom_labels',
'custom_docker_run_options',
'post_deployment_command',
'post_deployment_command_container',
'pre_deployment_command',
'pre_deployment_command_container',
'manual_webhook_secret_github',
'manual_webhook_secret_gitlab',
'manual_webhook_secret_bitbucket',
'manual_webhook_secret_gitea',
'docker_compose_location',
'docker_compose',
'docker_compose_raw',
'docker_compose_domains',
'docker_compose_custom_start_command',
'docker_compose_custom_build_command',
'swarm_replicas',
'swarm_placement_constraints',
'watch_paths',
'redirect',
'compose_parsing_version',
'custom_nginx_configuration',
'custom_network_aliases',
'custom_healthcheck_found',
'is_http_basic_auth_enabled',
'http_basic_auth_username',
'http_basic_auth_password',
'config_hash',
'last_online_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'environment_id',
'destination_id',
'destination_type',
'source_id',
'source_type',
'private_key_id',
'repository_project_id',
];
protected $appends = ['server_status'];
@ -1145,7 +1229,7 @@ public function getGitRemoteStatus(string $deployment_uuid)
'is_accessible' => true,
'error' => null,
];
} catch (\RuntimeException $ex) {
} catch (RuntimeException $ex) {
return [
'is_accessible' => false,
'error' => $ex->getMessage(),
@ -1202,7 +1286,7 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
];
}
if ($this->source->getMorphClass() === \App\Models\GitlabApp::class) {
if ($this->source->getMorphClass() === GitlabApp::class) {
$gitlabSource = $this->source;
$private_key = data_get($gitlabSource, 'privateKey.private_key');
@ -1354,7 +1438,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
if ($this->source->getMorphClass() === \App\Models\GithubApp::class) {
if ($this->source->getMorphClass() === GithubApp::class) {
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$escapedRepoUrl = escapeshellarg("{$this->source->html_url}/{$customRepository}");
@ -1409,7 +1493,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
];
}
if ($this->source->getMorphClass() === \App\Models\GitlabApp::class) {
if ($this->source->getMorphClass() === GitlabApp::class) {
$gitlabSource = $this->source;
$private_key = data_get($gitlabSource, 'privateKey.private_key');
@ -1600,7 +1684,7 @@ public function oldRawParser()
try {
$yaml = Yaml::parse($this->docker_compose_raw);
} catch (\Exception $e) {
throw new \RuntimeException($e->getMessage());
throw new RuntimeException($e->getMessage());
}
$services = data_get($yaml, 'services');
@ -1682,7 +1766,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
$fileList = collect([".$workdir$composeFile"]);
$gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid);
if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
throw new RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
}
$getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false);
$gitVersion = str($getGitVersion)->explode(' ')->last();
@ -1732,15 +1816,15 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
$this->save();
if (str($e->getMessage())->contains('No such file')) {
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
throw new RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
if (str($e->getMessage())->contains('fatal: repository') && str($e->getMessage())->contains('does not exist')) {
if ($this->deploymentType() === 'deploy_key') {
throw new \RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.');
throw new RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.');
}
throw new \RuntimeException('Repository does not exist. Please check your repository URL and try again.');
throw new RuntimeException('Repository does not exist. Please check your repository URL and try again.');
}
throw new \RuntimeException($e->getMessage());
throw new RuntimeException($e->getMessage());
} finally {
// Cleanup only - restoration happens in catch block
$commands = collect([
@ -1793,7 +1877,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
$this->base_directory = $initialBaseDirectory;
$this->save();
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
throw new RuntimeException("Docker Compose file not found at: $workdir$composeFile (branch: {$this->git_branch})<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
}

View file

@ -39,7 +39,32 @@
)]
class ApplicationDeploymentQueue extends Model
{
protected $guarded = [];
protected $fillable = [
'application_id',
'deployment_uuid',
'pull_request_id',
'force_rebuild',
'commit',
'status',
'is_webhook',
'logs',
'current_process_id',
'restart_only',
'git_type',
'server_id',
'application_name',
'server_name',
'deployment_url',
'destination_id',
'only_this_server',
'rollback',
'commit_message',
'is_api',
'build_server_id',
'horizon_job_id',
'horizon_job_worker',
'finished_at',
];
protected $casts = [
'finished_at' => 'datetime',

View file

@ -10,7 +10,16 @@ class ApplicationPreview extends BaseModel
{
use SoftDeletes;
protected $guarded = [];
protected $fillable = [
'pull_request_id',
'pull_request_html_url',
'pull_request_issue_comment_id',
'fqdn',
'status',
'git_type',
'docker_compose_domains',
'last_online_at',
];
protected static function booted()
{
@ -69,7 +78,7 @@ public function application()
public function persistentStorages()
{
return $this->morphMany(\App\Models\LocalPersistentVolume::class, 'resource');
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function generate_preview_fqdn()

View file

@ -28,7 +28,42 @@ class ApplicationSetting extends Model
'docker_images_to_keep' => 'integer',
];
protected $guarded = [];
protected $fillable = [
'is_static',
'is_git_submodules_enabled',
'is_git_lfs_enabled',
'is_auto_deploy_enabled',
'is_force_https_enabled',
'is_debug_enabled',
'is_preview_deployments_enabled',
'is_log_drain_enabled',
'is_gpu_enabled',
'gpu_driver',
'gpu_count',
'gpu_device_ids',
'gpu_options',
'is_include_timestamps',
'is_swarm_only_worker_nodes',
'is_raw_compose_deployment_enabled',
'is_build_server_enabled',
'is_consistent_container_name_enabled',
'is_gzip_enabled',
'is_stripprefix_enabled',
'connect_to_docker_network',
'custom_internal_name',
'is_container_label_escape_enabled',
'is_env_sorting_enabled',
'is_container_label_readonly_enabled',
'is_preserve_repository_enabled',
'disable_build_cache',
'is_spa',
'is_git_shallow_clone_enabled',
'is_pr_deployments_public_enabled',
'use_build_secrets',
'inject_build_args_to_dockerfile',
'include_source_commit_in_build',
'docker_images_to_keep',
];
public function isStatic(): Attribute
{

View file

@ -4,7 +4,11 @@
class CloudProviderToken extends BaseModel
{
protected $guarded = [];
protected $fillable = [
'provider',
'token',
'name',
];
protected $casts = [
'token' => 'encrypted',

View file

@ -24,7 +24,8 @@ class DiscordNotificationSettings extends Model
'backup_failure_discord_notifications',
'scheduled_task_success_discord_notifications',
'scheduled_task_failure_discord_notifications',
'docker_cleanup_discord_notifications',
'docker_cleanup_success_discord_notifications',
'docker_cleanup_failure_discord_notifications',
'server_disk_usage_discord_notifications',
'server_reachable_discord_notifications',
'server_unreachable_discord_notifications',

View file

@ -6,7 +6,12 @@
class DockerCleanupExecution extends BaseModel
{
protected $guarded = [];
protected $fillable = [
'status',
'message',
'cleanup_log',
'finished_at',
];
public function server(): BelongsTo
{

View file

@ -34,7 +34,11 @@ class EmailNotificationSettings extends Model
'backup_failure_email_notifications',
'scheduled_task_success_email_notifications',
'scheduled_task_failure_email_notifications',
'docker_cleanup_success_email_notifications',
'docker_cleanup_failure_email_notifications',
'server_disk_usage_email_notifications',
'server_reachable_email_notifications',
'server_unreachable_email_notifications',
'server_patch_email_notifications',
'traefik_outdated_email_notifications',
];

View file

@ -25,7 +25,10 @@ class Environment extends BaseModel
use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];
protected $fillable = [
'name',
'description',
];
protected static function booted()
{

View file

@ -6,7 +6,25 @@
class GithubApp extends BaseModel
{
protected $guarded = [];
protected $fillable = [
'name',
'organization',
'api_url',
'html_url',
'custom_user',
'custom_port',
'app_id',
'installation_id',
'client_id',
'client_secret',
'webhook_secret',
'is_system_wide',
'is_public',
'contents',
'metadata',
'pull_requests',
'administration',
];
protected $appends = ['type'];
@ -92,7 +110,7 @@ public function type(): Attribute
{
return Attribute::make(
get: function () {
if ($this->getMorphClass() === \App\Models\GithubApp::class) {
if ($this->getMorphClass() === GithubApp::class) {
return 'github';
}
},

View file

@ -4,6 +4,24 @@
class GitlabApp extends BaseModel
{
protected $fillable = [
'name',
'organization',
'api_url',
'html_url',
'custom_port',
'custom_user',
'is_system_wide',
'is_public',
'app_id',
'app_secret',
'oauth_id',
'group_name',
'public_key',
'webhook_token',
'deploy_key_id',
];
protected $hidden = [
'webhook_token',
'app_secret',

View file

@ -9,7 +9,43 @@
class InstanceSettings extends Model
{
protected $guarded = [];
protected $fillable = [
'public_ipv4',
'public_ipv6',
'fqdn',
'public_port_min',
'public_port_max',
'do_not_track',
'is_auto_update_enabled',
'is_registration_enabled',
'next_channel',
'smtp_enabled',
'smtp_from_address',
'smtp_from_name',
'smtp_recipients',
'smtp_host',
'smtp_port',
'smtp_encryption',
'smtp_username',
'smtp_password',
'smtp_timeout',
'resend_enabled',
'resend_api_key',
'is_dns_validation_enabled',
'custom_dns_servers',
'instance_name',
'is_api_enabled',
'allowed_ips',
'auto_update_frequency',
'update_check_frequency',
'new_version_available',
'instance_timezone',
'helper_version',
'disable_two_step_confirmation',
'is_sponsorship_popup_enabled',
'dev_helper_version',
'is_wire_navigate_enabled',
];
protected $casts = [
'smtp_enabled' => 'boolean',

View file

@ -20,7 +20,18 @@ class LocalFileVolume extends BaseModel
use HasFactory;
protected $guarded = [];
protected $fillable = [
'fs_path',
'mount_path',
'content',
'resource_type',
'resource_id',
'is_directory',
'chown',
'chmod',
'is_based_on_git',
'is_preview_suffix_enabled',
];
public $appends = ['is_binary'];

View file

@ -7,7 +7,15 @@
class LocalPersistentVolume extends BaseModel
{
protected $guarded = [];
protected $fillable = [
'name',
'mount_path',
'host_path',
'container_id',
'resource_type',
'resource_id',
'is_preview_suffix_enabled',
];
protected $casts = [
'is_preview_suffix_enabled' => 'boolean',

View file

@ -24,7 +24,10 @@ class Project extends BaseModel
use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];
protected $fillable = [
'name',
'description',
];
/**
* Get query builder for projects owned by current team.

View file

@ -6,7 +6,7 @@
class ProjectSetting extends Model
{
protected $guarded = [];
protected $fillable = [];
public function project()
{

View file

@ -25,7 +25,8 @@ class PushoverNotificationSettings extends Model
'backup_failure_pushover_notifications',
'scheduled_task_success_pushover_notifications',
'scheduled_task_failure_pushover_notifications',
'docker_cleanup_pushover_notifications',
'docker_cleanup_success_pushover_notifications',
'docker_cleanup_failure_pushover_notifications',
'server_disk_usage_pushover_notifications',
'server_reachable_pushover_notifications',
'server_unreachable_pushover_notifications',

View file

@ -12,7 +12,17 @@ class S3Storage extends BaseModel
{
use HasFactory, HasSafeStringAttribute;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'region',
'key',
'secret',
'bucket',
'endpoint',
'is_usable',
'unusable_email_sent',
];
protected $casts = [
'is_usable' => 'boolean',

View file

@ -8,7 +8,25 @@
class ScheduledDatabaseBackup extends BaseModel
{
protected $guarded = [];
protected $fillable = [
'description',
'enabled',
'save_s3',
'frequency',
'database_backup_retention_amount_locally',
'database_type',
'database_id',
's3_storage_id',
'databases_to_backup',
'dump_all',
'database_backup_retention_days_locally',
'database_backup_retention_max_storage_locally',
'database_backup_retention_amount_s3',
'database_backup_retention_days_s3',
'database_backup_retention_max_storage_s3',
'timeout',
'disable_local_backup',
];
public static function ownedByCurrentTeam()
{

View file

@ -6,7 +6,17 @@
class ScheduledDatabaseBackupExecution extends BaseModel
{
protected $guarded = [];
protected $fillable = [
'status',
'message',
'size',
'filename',
'database_name',
'finished_at',
'local_storage_deleted',
's3_storage_deleted',
's3_uploaded',
];
protected function casts(): array
{

View file

@ -29,7 +29,14 @@ class ScheduledTask extends BaseModel
use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];
protected $fillable = [
'enabled',
'name',
'command',
'frequency',
'container',
'timeout',
];
public static function ownedByCurrentTeamAPI(int $teamId)
{

View file

@ -22,7 +22,15 @@
)]
class ScheduledTaskExecution extends BaseModel
{
protected $guarded = [];
protected $fillable = [
'status',
'message',
'finished_at',
'started_at',
'retry_count',
'duration',
'error_details',
];
protected function casts(): array
{

View file

@ -34,6 +34,7 @@
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Spatie\Url\Url;
use Stevebauman\Purify\Facades\Purify;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
@ -265,14 +266,12 @@ public static function flushIdentityMap(): void
'server_metadata',
];
protected $guarded = [];
use HasSafeStringAttribute;
public function setValidationLogsAttribute($value): void
{
$this->attributes['validation_logs'] = $value !== null
? \Stevebauman\Purify\Facades\Purify::config('validation_logs')->clean($value)
? Purify::config('validation_logs')->clean($value)
: null;
}

View file

@ -53,7 +53,50 @@
)]
class ServerSetting extends Model
{
protected $guarded = [];
protected $fillable = [
'is_swarm_manager',
'is_jump_server',
'is_build_server',
'is_reachable',
'is_usable',
'wildcard_domain',
'is_cloudflare_tunnel',
'is_logdrain_newrelic_enabled',
'logdrain_newrelic_license_key',
'logdrain_newrelic_base_uri',
'is_logdrain_highlight_enabled',
'logdrain_highlight_project_id',
'is_logdrain_axiom_enabled',
'logdrain_axiom_dataset_name',
'logdrain_axiom_api_key',
'is_swarm_worker',
'is_logdrain_custom_enabled',
'logdrain_custom_config',
'logdrain_custom_config_parser',
'concurrent_builds',
'dynamic_timeout',
'force_disabled',
'is_metrics_enabled',
'generate_exact_labels',
'force_docker_cleanup',
'docker_cleanup_frequency',
'docker_cleanup_threshold',
'server_timezone',
'delete_unused_volumes',
'delete_unused_networks',
'is_sentinel_enabled',
'sentinel_token',
'sentinel_metrics_refresh_rate_seconds',
'sentinel_metrics_history_days',
'sentinel_push_interval_seconds',
'sentinel_custom_url',
'server_disk_usage_notification_threshold',
'is_sentinel_debug_enabled',
'server_disk_usage_check_frequency',
'is_terminal_enabled',
'deployment_queue_limit',
'disable_application_image_retention',
];
protected $casts = [
'force_docker_cleanup' => 'boolean',

View file

@ -15,6 +15,7 @@
use OpenApi\Attributes as OA;
use Spatie\Activitylog\Models\Activity;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
@ -47,7 +48,21 @@ class Service extends BaseModel
private static $parserVersion = '5';
protected $guarded = [];
protected $fillable = [
'name',
'description',
'docker_compose_raw',
'docker_compose',
'connect_to_docker_network',
'service_type',
'config_hash',
'compose_parsing_version',
'environment_id',
'server_id',
'destination_id',
'destination_type',
'is_container_label_escape_enabled',
];
protected $appends = ['server_status', 'status'];
@ -1552,7 +1567,7 @@ public function saveComposeConfigs()
// Generate SERVICE_NAME_* environment variables from docker-compose services
if ($this->docker_compose) {
try {
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($this->docker_compose);
$dockerCompose = Yaml::parse($this->docker_compose);
$services = data_get($dockerCompose, 'services', []);
foreach ($services as $serviceName => $_) {
$envs->push('SERVICE_NAME_'.str($serviceName)->replace('-', '_')->replace('.', '_')->upper().'='.$serviceName);

View file

@ -5,12 +5,30 @@
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\Yaml\Yaml;
class ServiceApplication extends BaseModel
{
use HasFactory, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'human_name',
'description',
'fqdn',
'ports',
'exposes',
'status',
'exclude_from_status',
'required_fqdn',
'image',
'is_log_drain_enabled',
'is_include_timestamps',
'is_gzip_enabled',
'is_stripprefix_enabled',
'last_online_at',
'is_migrated',
];
protected static function booted()
{
@ -211,7 +229,7 @@ public function getRequiredPort(): ?int
return $this->service->getRequiredPort();
}
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
$dockerCompose = Yaml::parse($dockerComposeRaw);
$serviceConfig = data_get($dockerCompose, "services.{$this->name}");
if (! $serviceConfig) {
return $this->service->getRequiredPort();

View file

@ -9,7 +9,27 @@ class ServiceDatabase extends BaseModel
{
use HasFactory, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'human_name',
'description',
'fqdn',
'ports',
'exposes',
'status',
'exclude_from_status',
'image',
'public_port',
'is_public',
'is_log_drain_enabled',
'is_include_timestamps',
'is_gzip_enabled',
'is_stripprefix_enabled',
'last_online_at',
'is_migrated',
'custom_type',
'public_port_timeout',
];
protected $casts = [
'public_port_timeout' => 'integer',

View file

@ -22,6 +22,9 @@ class SharedEnvironmentVariable extends Model
'is_multiline',
'is_literal',
'is_shown_once',
// Metadata
'version',
];
protected $casts = [

View file

@ -24,7 +24,8 @@ class SlackNotificationSettings extends Model
'backup_failure_slack_notifications',
'scheduled_task_success_slack_notifications',
'scheduled_task_failure_slack_notifications',
'docker_cleanup_slack_notifications',
'docker_cleanup_success_slack_notifications',
'docker_cleanup_failure_slack_notifications',
'server_disk_usage_slack_notifications',
'server_reachable_slack_notifications',
'server_unreachable_slack_notifications',

View file

@ -13,7 +13,34 @@ class StandaloneClickhouse extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'clickhouse_admin_user',
'clickhouse_admin_password',
'is_log_drain_enabled',
'is_include_timestamps',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
'public_port_timeout',
'custom_docker_run_options',
'clickhouse_db',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -12,7 +12,10 @@ class StandaloneDocker extends BaseModel
use HasFactory;
use HasSafeStringAttribute;
protected $guarded = [];
protected $fillable = [
'name',
'network',
];
protected static function boot()
{

View file

@ -13,7 +13,33 @@ class StandaloneDragonfly extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'dragonfly_password',
'is_log_drain_enabled',
'is_include_timestamps',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
'public_port_timeout',
'enable_ssl',
'custom_docker_run_options',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -13,7 +13,34 @@ class StandaloneKeydb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'keydb_password',
'keydb_conf',
'is_log_drain_enabled',
'is_include_timestamps',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
'public_port_timeout',
'enable_ssl',
'custom_docker_run_options',
];
protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];

View file

@ -14,7 +14,36 @@ class StandaloneMariadb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'mariadb_root_password',
'mariadb_user',
'mariadb_password',
'mariadb_database',
'mariadb_conf',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
'public_port_timeout',
'enable_ssl',
'is_log_drain_enabled',
'custom_docker_run_options',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -13,7 +13,37 @@ class StandaloneMongodb extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'mongo_conf',
'mongo_initdb_root_username',
'mongo_initdb_root_password',
'mongo_initdb_database',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
'public_port_timeout',
'enable_ssl',
'ssl_mode',
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -13,7 +13,38 @@ class StandaloneMysql extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'mysql_root_password',
'mysql_user',
'mysql_password',
'mysql_database',
'mysql_conf',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
'public_port_timeout',
'enable_ssl',
'ssl_mode',
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -13,7 +13,40 @@ class StandalonePostgresql extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'postgres_user',
'postgres_password',
'postgres_db',
'postgres_initdb_args',
'postgres_host_auth_method',
'postgres_conf',
'init_scripts',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
'public_port_timeout',
'enable_ssl',
'ssl_mode',
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -13,7 +13,33 @@ class StandaloneRedis extends BaseModel
{
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'redis_conf',
'status',
'image',
'is_public',
'public_port',
'ports_mappings',
'limits_memory',
'limits_memory_swap',
'limits_memory_swappiness',
'limits_memory_reservation',
'limits_cpus',
'limits_cpuset',
'limits_cpu_shares',
'started_at',
'restart_count',
'last_restart_at',
'last_restart_type',
'last_online_at',
'public_port_timeout',
'enable_ssl',
'is_log_drain_enabled',
'is_include_timestamps',
'custom_docker_run_options',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];

View file

@ -6,7 +6,18 @@
class Subscription extends Model
{
protected $guarded = [];
protected $fillable = [
'stripe_invoice_paid',
'stripe_subscription_id',
'stripe_customer_id',
'stripe_cancel_at_period_end',
'stripe_plan_id',
'stripe_feedback',
'stripe_comment',
'stripe_trial_already_ended',
'stripe_past_due',
'stripe_refunded_at',
];
protected function casts(): array
{

View file

@ -6,7 +6,10 @@
class SwarmDocker extends BaseModel
{
protected $guarded = [];
protected $fillable = [
'name',
'network',
];
public function setNetworkAttribute(string $value): void
{

View file

@ -8,7 +8,9 @@ class Tag extends BaseModel
{
use HasSafeStringAttribute;
protected $guarded = [];
protected $fillable = [
'name',
];
protected function customizeName($value)
{

View file

@ -40,7 +40,12 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
{
use HasFactory, HasNotificationSettings, HasSafeStringAttribute, Notifiable;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'show_boarding',
'custom_server_limit',
];
protected $casts = [
'personal_team' => 'boolean',

View file

@ -25,7 +25,8 @@ class TelegramNotificationSettings extends Model
'backup_failure_telegram_notifications',
'scheduled_task_success_telegram_notifications',
'scheduled_task_failure_telegram_notifications',
'docker_cleanup_telegram_notifications',
'docker_cleanup_success_telegram_notifications',
'docker_cleanup_failure_telegram_notifications',
'server_disk_usage_telegram_notifications',
'server_reachable_telegram_notifications',
'server_unreachable_telegram_notifications',
@ -39,7 +40,8 @@ class TelegramNotificationSettings extends Model
'telegram_notifications_backup_failure_thread_id',
'telegram_notifications_scheduled_task_success_thread_id',
'telegram_notifications_scheduled_task_failure_thread_id',
'telegram_notifications_docker_cleanup_thread_id',
'telegram_notifications_docker_cleanup_success_thread_id',
'telegram_notifications_docker_cleanup_failure_thread_id',
'telegram_notifications_server_disk_usage_thread_id',
'telegram_notifications_server_reachable_thread_id',
'telegram_notifications_server_unreachable_thread_id',

View file

@ -4,7 +4,9 @@
use App\Jobs\UpdateStripeCustomerEmailJob;
use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\EmailChangeVerification;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
use App\Services\ChangelogService;
use App\Traits\DeletesUserSessions;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -41,7 +43,16 @@ class User extends Authenticatable implements SendsEmail
{
use DeletesUserSessions, HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
protected $guarded = [];
protected $fillable = [
'name',
'email',
'password',
'force_password_reset',
'marketing_emails',
'pending_email',
'email_change_code',
'email_change_code_expires_at',
];
protected $hidden = [
'password',
@ -228,7 +239,7 @@ public function changelogReads()
public function getUnreadChangelogCount(): int
{
return app(\App\Services\ChangelogService::class)->getUnreadCountForUser($this);
return app(ChangelogService::class)->getUnreadCountForUser($this);
}
public function getRecipients(): array
@ -239,7 +250,7 @@ public function getRecipients(): array
public function sendVerificationEmail()
{
$mail = new MailMessage;
$url = Url::temporarySignedRoute(
$url = URL::temporarySignedRoute(
'verify.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
@ -408,7 +419,7 @@ public function requestEmailChange(string $newEmail): void
]);
// Send verification email to new address
$this->notify(new \App\Notifications\TransactionalEmails\EmailChangeVerification($this, $code, $newEmail, $expiresAt));
$this->notify(new EmailChangeVerification($this, $code, $newEmail, $expiresAt));
}
public function isEmailChangeCodeValid(string $code): bool

View file

@ -6,6 +6,7 @@
use App\Jobs\VolumeCloneJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\EnvironmentVariable;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Spatie\Url\Url;
@ -192,7 +193,7 @@ function clone_application(Application $source, $destination, array $overrides =
$server = $destination->server;
if ($server->team_id !== currentTeam()->id) {
throw new \RuntimeException('Destination does not belong to the current team.');
throw new RuntimeException('Destination does not belong to the current team.');
}
// Prepare name and URL
@ -211,7 +212,7 @@ function clone_application(Application $source, $destination, array $overrides =
'updated_at',
'additional_servers_count',
'additional_networks_count',
])->fill(array_merge([
])->forceFill(array_merge([
'uuid' => $uuid,
'name' => $name,
'fqdn' => $url,
@ -322,8 +323,8 @@ function clone_application(Application $source, $destination, array $overrides =
destination: $source->destination,
no_questions_asked: true
);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
} catch (Exception $e) {
Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
@ -344,7 +345,7 @@ function clone_application(Application $source, $destination, array $overrides =
// Clone production environment variables without triggering the created hook
$environmentVariables = $source->environment_variables()->get();
foreach ($environmentVariables as $environmentVariable) {
\App\Models\EnvironmentVariable::withoutEvents(function () use ($environmentVariable, $newApplication) {
EnvironmentVariable::withoutEvents(function () use ($environmentVariable, $newApplication) {
$newEnvironmentVariable = $environmentVariable->replicate([
'id',
'created_at',
@ -361,7 +362,7 @@ function clone_application(Application $source, $destination, array $overrides =
// Clone preview environment variables
$previewEnvironmentVariables = $source->environment_variables_preview()->get();
foreach ($previewEnvironmentVariables as $previewEnvironmentVariable) {
\App\Models\EnvironmentVariable::withoutEvents(function () use ($previewEnvironmentVariable, $newApplication) {
EnvironmentVariable::withoutEvents(function () use ($previewEnvironmentVariable, $newApplication) {
$newPreviewEnvironmentVariable = $previewEnvironmentVariable->replicate([
'id',
'created_at',

View file

@ -0,0 +1,249 @@
<?php
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Models\Team;
use App\Models\User;
describe('mass assignment protection', function () {
test('no API-exposed model uses unguarded $guarded = []', function () {
$models = [
Application::class,
Service::class,
User::class,
Team::class,
Server::class,
StandalonePostgresql::class,
StandaloneRedis::class,
StandaloneMysql::class,
StandaloneMariadb::class,
StandaloneMongodb::class,
StandaloneKeydb::class,
StandaloneDragonfly::class,
StandaloneClickhouse::class,
];
foreach ($models as $modelClass) {
$model = new $modelClass;
$guarded = $model->getGuarded();
$fillable = $model->getFillable();
// Model must NOT have $guarded = [] (empty guard = no protection)
// It should either have non-empty $guarded OR non-empty $fillable
$hasProtection = $guarded !== ['*'] ? count($guarded) > 0 : true;
$hasProtection = $hasProtection || count($fillable) > 0;
expect($hasProtection)
->toBeTrue("Model {$modelClass} has no mass assignment protection (empty \$guarded and empty \$fillable)");
}
});
test('Application model blocks mass assignment of identity fields', function () {
$application = new Application;
expect($application->isFillable('id'))->toBeFalse('id should not be fillable');
expect($application->isFillable('uuid'))->toBeFalse('uuid should not be fillable');
expect($application->isFillable('created_at'))->toBeFalse('created_at should not be fillable');
expect($application->isFillable('updated_at'))->toBeFalse('updated_at should not be fillable');
expect($application->isFillable('deleted_at'))->toBeFalse('deleted_at should not be fillable');
});
test('Application model allows mass assignment of user-facing fields', function () {
$application = new Application;
$userFields = ['name', 'description', 'git_repository', 'git_branch', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'health_check_path', 'health_check_enabled', 'limits_memory', 'status'];
foreach ($userFields as $field) {
expect($application->isFillable($field))
->toBeTrue("Application model should allow mass assignment of '{$field}'");
}
});
test('Application model allows mass assignment of relationship fields needed for create()', function () {
$application = new Application;
$relationFields = ['environment_id', 'destination_id', 'destination_type', 'source_id', 'source_type', 'private_key_id', 'repository_project_id'];
foreach ($relationFields as $field) {
expect($application->isFillable($field))
->toBeTrue("Application model should allow mass assignment of '{$field}' for internal create() calls");
}
});
test('Application fill ignores non-fillable fields', function () {
$application = new Application;
$application->fill([
'name' => 'test-app',
'team_id' => 999,
]);
expect($application->name)->toBe('test-app');
expect($application->team_id)->toBeNull();
});
test('Server model has $fillable and no conflicting $guarded', function () {
$server = new Server;
$fillable = $server->getFillable();
$guarded = $server->getGuarded();
expect($fillable)->not->toBeEmpty('Server model should have explicit $fillable');
expect($guarded)->not->toBe([], 'Server model should not have $guarded = [] overriding $fillable');
});
test('Server model blocks mass assignment of dangerous fields', function () {
$server = new Server;
expect($server->isFillable('id'))->toBeFalse();
expect($server->isFillable('uuid'))->toBeFalse();
expect($server->isFillable('created_at'))->toBeFalse();
});
test('User model blocks mass assignment of auth-sensitive fields', function () {
$user = new User;
expect($user->isFillable('id'))->toBeFalse('User id should not be fillable');
expect($user->isFillable('email_verified_at'))->toBeFalse('email_verified_at should not be fillable');
expect($user->isFillable('remember_token'))->toBeFalse('remember_token should not be fillable');
expect($user->isFillable('two_factor_secret'))->toBeFalse('two_factor_secret should not be fillable');
expect($user->isFillable('two_factor_recovery_codes'))->toBeFalse('two_factor_recovery_codes should not be fillable');
});
test('User model allows mass assignment of profile fields', function () {
$user = new User;
expect($user->isFillable('name'))->toBeTrue();
expect($user->isFillable('email'))->toBeTrue();
expect($user->isFillable('password'))->toBeTrue();
});
test('Team model blocks mass assignment of internal fields', function () {
$team = new Team;
expect($team->isFillable('id'))->toBeFalse();
expect($team->isFillable('personal_team'))->toBeFalse('personal_team should not be fillable');
});
test('Service model blocks mass assignment of identity fields', function () {
$service = new Service;
expect($service->isFillable('id'))->toBeFalse();
expect($service->isFillable('uuid'))->toBeFalse();
});
test('Service model allows mass assignment of relationship fields needed for create()', function () {
$service = new Service;
expect($service->isFillable('environment_id'))->toBeTrue();
expect($service->isFillable('destination_id'))->toBeTrue();
expect($service->isFillable('destination_type'))->toBeTrue();
expect($service->isFillable('server_id'))->toBeTrue();
});
test('standalone database models block mass assignment of identity and relationship fields', function () {
$models = [
StandalonePostgresql::class,
StandaloneRedis::class,
StandaloneMysql::class,
StandaloneMariadb::class,
StandaloneMongodb::class,
StandaloneKeydb::class,
StandaloneDragonfly::class,
StandaloneClickhouse::class,
];
foreach ($models as $modelClass) {
$model = new $modelClass;
expect($model->isFillable('id'))
->toBeFalse("{$modelClass} should not allow mass assignment of 'id'");
expect($model->isFillable('uuid'))
->toBeFalse("{$modelClass} should not allow mass assignment of 'uuid'");
expect($model->isFillable('environment_id'))
->toBeFalse("{$modelClass} should not allow mass assignment of 'environment_id'");
expect($model->isFillable('destination_id'))
->toBeFalse("{$modelClass} should not allow mass assignment of 'destination_id'");
expect($model->isFillable('destination_type'))
->toBeFalse("{$modelClass} should not allow mass assignment of 'destination_type'");
}
});
test('standalone database models allow mass assignment of config fields', function () {
$model = new StandalonePostgresql;
expect($model->isFillable('name'))->toBeTrue();
expect($model->isFillable('postgres_user'))->toBeTrue();
expect($model->isFillable('postgres_password'))->toBeTrue();
expect($model->isFillable('image'))->toBeTrue();
expect($model->isFillable('limits_memory'))->toBeTrue();
$model = new StandaloneRedis;
expect($model->isFillable('redis_conf'))->toBeTrue();
$model = new StandaloneMysql;
expect($model->isFillable('mysql_root_password'))->toBeTrue();
$model = new StandaloneMongodb;
expect($model->isFillable('mongo_initdb_root_username'))->toBeTrue();
});
test('standalone database models allow mass assignment of public_port_timeout', function () {
$models = [
StandalonePostgresql::class,
StandaloneRedis::class,
StandaloneMysql::class,
StandaloneMariadb::class,
StandaloneMongodb::class,
StandaloneKeydb::class,
StandaloneDragonfly::class,
StandaloneClickhouse::class,
];
foreach ($models as $modelClass) {
$model = new $modelClass;
expect($model->isFillable('public_port_timeout'))
->toBeTrue("{$modelClass} should allow mass assignment of 'public_port_timeout'");
}
});
test('standalone database models allow mass assignment of SSL fields where applicable', function () {
// Models with enable_ssl
$sslModels = [
StandalonePostgresql::class,
StandaloneMysql::class,
StandaloneMariadb::class,
StandaloneMongodb::class,
StandaloneRedis::class,
StandaloneKeydb::class,
StandaloneDragonfly::class,
];
foreach ($sslModels as $modelClass) {
$model = new $modelClass;
expect($model->isFillable('enable_ssl'))
->toBeTrue("{$modelClass} should allow mass assignment of 'enable_ssl'");
}
// Clickhouse has no SSL columns
expect((new StandaloneClickhouse)->isFillable('enable_ssl'))->toBeFalse();
// Models with ssl_mode
$sslModeModels = [
StandalonePostgresql::class,
StandaloneMysql::class,
StandaloneMongodb::class,
];
foreach ($sslModeModels as $modelClass) {
$model = new $modelClass;
expect($model->isFillable('ssl_mode'))
->toBeTrue("{$modelClass} should allow mass assignment of 'ssl_mode'");
}
});
});