Merge branch 'next' into feat/api/tag-management

This commit is contained in:
Michael Jathe authored and GitHub committed 2026-03-30 10:04:00 +02:00
commit 2b573ef5ec
62 files changed
+1577 -244

No files matched your search

+2 -1
View File
@@ -37,12 +37,13 @@ class CreateNewUser implements CreatesNewUsers
if (User::count() == 0) {
// If this is the first user, make them the root user
// Team is already created in the database/seeders/ProductionSeeder.php
$user = User::create([
$user = (new User)->forceFill([
'id' => 0,
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
$user->save();
$team = $user->teams()->first();
// Disable registration after first user is created
+2 -2
View File
@@ -40,10 +40,10 @@ class StartService
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
if (data_get($service, 'connect_to_docker_network')) {
$compose = data_get($service, 'docker_compose', []);
$network = $service->destination->network;
$safeNetwork = escapeshellarg($service->destination->network);
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
foreach ($serviceNames as $serviceName => $serviceConfig) {
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} {$safeNetwork} {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
}
}
+6 -5
View File
@@ -212,18 +212,19 @@ class Init extends Command
$removeNetworks = $allNetworks->diff($networks);
$commands = collect();
foreach ($removeNetworks as $network) {
$out = instant_remote_process(["docker network inspect -f json $network | jq '.[].Containers | if . == {} then null else . end'"], $server, false);
$safe = escapeshellarg($network);
$out = instant_remote_process(["docker network inspect -f json {$safe} | jq '.[].Containers | if . == {} then null else . end'"], $server, false);
if (empty($out)) {
$commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
$commands->push("docker network rm $network >/dev/null 2>&1 || true");
$commands->push("docker network disconnect {$safe} coolify-proxy >/dev/null 2>&1 || true");
$commands->push("docker network rm {$safe} >/dev/null 2>&1 || true");
} else {
$data = collect(json_decode($out, true));
if ($data->count() === 1) {
// If only coolify-proxy itself is connected to that network (it should not be possible, but who knows)
$isCoolifyProxyItself = data_get($data->first(), 'Name') === 'coolify-proxy';
if ($isCoolifyProxyItself) {
$commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
$commands->push("docker network rm $network >/dev/null 2>&1 || true");
$commands->push("docker network disconnect {$safe} coolify-proxy >/dev/null 2>&1 || true");
$commands->push("docker network rm {$safe} >/dev/null 2>&1 || true");
}
}
}
@@ -243,6 +243,7 @@ class ApplicationsController extends Controller
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
],
)
),
@@ -409,6 +410,7 @@ class ApplicationsController extends Controller
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
],
)
),
@@ -575,6 +577,7 @@ class ApplicationsController extends Controller
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
],
)
),
@@ -1023,7 +1026,7 @@ class ApplicationsController extends Controller
if ($return instanceof JsonResponse) {
return $return;
}
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', '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', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', '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', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags'];
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', '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', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', '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', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled', 'tags', 'is_preserve_repository_enabled'];
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
@@ -1074,6 +1077,7 @@ class ApplicationsController extends Controller
$connectToDockerNetwork = $request->connect_to_docker_network;
$customNginxConfiguration = $request->custom_nginx_configuration;
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled', true);
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled',false);
if (! is_null($customNginxConfiguration)) {
if (! isBase64Encoded($customNginxConfiguration)) {
@@ -1177,7 +1181,7 @@ class ApplicationsController extends Controller
$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);
@@ -1286,6 +1290,10 @@ class ApplicationsController extends Controller
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
if (isset($isPreserveRepositoryEnabled)) {
$application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
$application->settings->save();
}
$application->refresh();
// Auto-generate domain if requested and no custom domain provided
if ($autogenerateDomain && blank($fqdn)) {
@@ -1407,7 +1415,7 @@ class ApplicationsController extends Controller
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
$application->fill($request->all());
$application->fill($request->only($allowedFields));
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
@@ -1521,6 +1529,10 @@ class ApplicationsController extends Controller
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
if (isset($isPreserveRepositoryEnabled)) {
$application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
$application->settings->save();
}
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
@@ -1610,7 +1622,7 @@ class ApplicationsController extends Controller
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
$application->fill($request->all());
$application->fill($request->only($allowedFields));
$dockerComposeDomainsJson = collect();
if ($request->has('docker_compose_domains')) {
@@ -1720,6 +1732,10 @@ class ApplicationsController extends Controller
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
if (isset($isPreserveRepositoryEnabled)) {
$application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
$application->settings->save();
}
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save();
@@ -1800,7 +1816,7 @@ class ApplicationsController extends Controller
}
$application = new Application;
$application->fill($request->all());
$application->fill($request->only($allowedFields));
$application->fqdn = $fqdn;
$application->ports_exposes = $port;
$application->build_pack = 'dockerfile';
@@ -1915,7 +1931,7 @@ class ApplicationsController extends Controller
$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;
@@ -2034,7 +2050,7 @@ class ApplicationsController extends Controller
$service = new Service;
removeUnnecessaryFieldsFromRequest($request);
$service->fill($request->all());
$service->fill($request->only($allowedFields));
$service->docker_compose_raw = $dockerComposeRaw;
$service->environment_id = $environment->id;
@@ -2428,6 +2444,7 @@ class ApplicationsController extends Controller
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'is_preserve_repository_enabled' => ['type' => 'boolean', 'description' => 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'],
],
)
),
@@ -2513,7 +2530,7 @@ class ApplicationsController extends Controller
$this->authorize('update', $application);
$server = $application->destination->server;
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', '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', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', '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', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled'];
$validationRules = [
'name' => 'string|max:255',
@@ -2760,7 +2777,7 @@ class ApplicationsController extends Controller
$connectToDockerNetwork = $request->connect_to_docker_network;
$useBuildServer = $request->use_build_server;
$isContainerLabelEscapeEnabled = $request->boolean('is_container_label_escape_enabled');
$isPreserveRepositoryEnabled = $request->boolean('is_preserve_repository_enabled');
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
@@ -2795,10 +2812,13 @@ class ApplicationsController extends Controller
$application->settings->is_container_label_escape_enabled = $isContainerLabelEscapeEnabled;
$application->settings->save();
}
if ($request->has('is_preserve_repository_enabled')) {
$application->settings->is_preserve_repository_enabled = $isPreserveRepositoryEnabled;
$application->settings->save();
}
removeUnnecessaryFieldsFromRequest($request);
$data = $request->all();
$data = $request->only($allowedFields);
if ($requestHasDomains && $server->isProxyShouldRun()) {
data_set($data, 'fqdn', $domains);
}
@@ -1773,13 +1773,13 @@ class DatabasesController extends Controller
}
$request->offsetSet('postgres_conf', $postgresConf);
}
$database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all());
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database = create_standalone_postgresql($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database->refresh();
$payload = [
'uuid' => $database->uuid,
@@ -1831,13 +1831,13 @@ class DatabasesController extends Controller
}
$request->offsetSet('mariadb_conf', $mariadbConf);
}
$database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all());
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database = create_standalone_mariadb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database->refresh();
$payload = [
@@ -1893,13 +1893,13 @@ class DatabasesController extends Controller
}
$request->offsetSet('mysql_conf', $mysqlConf);
}
$database = create_standalone_mysql($environment->id, $destination->uuid, $request->all());
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database = create_standalone_mysql($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database->refresh();
$payload = [
@@ -1952,13 +1952,13 @@ class DatabasesController extends Controller
}
$request->offsetSet('redis_conf', $redisConf);
}
$database = create_standalone_redis($environment->id, $destination->uuid, $request->all());
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database = create_standalone_redis($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database->refresh();
$payload = [
@@ -1992,13 +1992,13 @@ class DatabasesController extends Controller
}
removeUnnecessaryFieldsFromRequest($request);
$database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all());
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
return response()->json(serializeApiResponse([
'uuid' => $database->uuid,
@@ -2044,13 +2044,13 @@ class DatabasesController extends Controller
}
$request->offsetSet('keydb_conf', $keydbConf);
}
$database = create_standalone_keydb($environment->id, $destination->uuid, $request->all());
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database = create_standalone_keydb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database->refresh();
$payload = [
@@ -2083,13 +2083,13 @@ class DatabasesController extends Controller
], 422);
}
removeUnnecessaryFieldsFromRequest($request);
$database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all());
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database->refresh();
$payload = [
@@ -2144,13 +2144,13 @@ class DatabasesController extends Controller
}
$request->offsetSet('mongo_conf', $mongoConf);
}
$database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all());
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database = create_standalone_mongodb($environment->id, $destination->uuid, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
if ($request->has('tags')) {
$this->attachTagsToResource($database, $request->tags, $teamId);
}
$database->refresh();
$payload = [
@@ -250,7 +250,7 @@ class DeployController extends Controller
]);
// Get the server
$server = Server::find($build_server_id);
$server = Server::whereTeamId($teamId)->find($build_server_id);
if ($server) {
// Add cancellation log entry
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
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 @@ class SecurityController extends Controller
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 @@ class SecurityController extends Controller
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
if ($return instanceof JsonResponse) {
return $return;
}
@@ -330,7 +331,7 @@ class SecurityController extends Controller
'message' => 'Private Key not found.',
], 404);
}
$foundKey->update($request->all());
$foundKey->update($request->only($allowedFields));
return response()->json(serializeApiResponse([
'uuid' => $foundKey->uuid,
@@ -14,14 +14,6 @@ class TeamController extends Controller
'custom_server_limit',
'pivot',
]);
if (request()->attributes->get('can_read_sensitive', false) === false) {
$team->makeHidden([
'smtp_username',
'smtp_password',
'resend_api_key',
'telegram_token',
]);
}
return serializeApiResponse($team);
}
+13 -9
View File
@@ -288,7 +288,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// Make sure the private key is stored in the filesystem
$this->server->privateKey->storeInFileSystem();
// Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
$safeNetwork = escapeshellarg($this->destination->network);
$allContainers = instant_remote_process(["docker network inspect {$safeNetwork} -f '{{json .Containers}}' "], $this->server);
if (! is_null($allContainers)) {
$allContainers = format_docker_command_output_to_json($allContainers);
@@ -2015,9 +2016,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$runCommand = "docker run -d --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
$safeNetwork = escapeshellarg($this->destination->network);
$runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} {$env_flags} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
$safeNetwork = escapeshellarg($this->destination->network);
$runCommand = "docker run -d --network {$safeNetwork} --name {$this->deployment_uuid} {$env_flags} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
}
if ($firstTry) {
@@ -3046,28 +3049,29 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]);
} else {
// Dockerfile buildpack
$safeNetwork = escapeshellarg($this->destination->network);
if ($this->dockerSecretsSupported) {
// Modify the Dockerfile to use build secrets
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
if ($this->force_rebuild) {
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->workdir}");
}
} elseif ($this->dockerBuildkitSupported) {
// BuildKit without secrets
if ($this->force_rebuild) {
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
}
} else {
// Traditional build with args
if ($this->force_rebuild) {
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
} else {
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$safeNetwork} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
}
}
$base64_build_command = base64_encode($build_command);
+4 -3
View File
@@ -678,6 +678,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else {
$network = $this->database->destination->network;
}
$safeNetwork = escapeshellarg($network);
$fullImageName = $this->getFullImageName();
@@ -689,13 +690,13 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if (isDev()) {
if ($this->database->name === 'coolify-db') {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
$commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
} else {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file;
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
$commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
}
} else {
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup_log_uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
$commands[] = "docker run -d --network {$safeNetwork} --name backup-of-{$this->backup_log_uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
}
// Escape S3 credentials to prevent command injection
+6 -3
View File
@@ -121,7 +121,7 @@ class Index extends Component
}
if ($this->selectedExistingServer) {
$this->createdServer = Server::find($this->selectedExistingServer);
$this->createdServer = Server::ownedByCurrentTeam()->find($this->selectedExistingServer);
if ($this->createdServer) {
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
$this->updateServerDetails();
@@ -145,7 +145,7 @@ class Index extends Component
}
if ($this->selectedProject) {
$this->createdProject = Project::find($this->selectedProject);
$this->createdProject = Project::ownedByCurrentTeam()->find($this->selectedProject);
if (! $this->createdProject) {
$this->projects = Project::ownedByCurrentTeam(['name'])->get();
}
@@ -431,7 +431,10 @@ class Index extends Component
public function selectExistingProject()
{
$this->createdProject = Project::find($this->selectedProject);
$this->createdProject = Project::ownedByCurrentTeam()->find($this->selectedProject);
if (! $this->createdProject) {
return $this->dispatch('error', 'Project not found.');
}
$this->currentState = 'create-resource';
}
+2 -1
View File
@@ -5,6 +5,7 @@ namespace App\Livewire\Destination\New;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Support\ValidationPatterns;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
@@ -24,7 +25,7 @@ class Docker extends Component
#[Validate(['required', 'string'])]
public string $name;
#[Validate(['required', 'string'])]
#[Validate(['required', 'string', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'])]
public string $network;
#[Validate(['required', 'string'])]
+4 -3
View File
@@ -20,7 +20,7 @@ class Show extends Component
#[Validate(['string', 'required'])]
public string $name;
#[Validate(['string', 'required'])]
#[Validate(['string', 'required', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'])]
public string $network;
#[Validate(['string', 'required'])]
@@ -84,8 +84,9 @@ class Show extends Component
if ($this->destination->attachedTo()) {
return $this->dispatch('error', 'You must delete all resources before deleting this destination.');
}
instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false);
instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server);
$safeNetwork = escapeshellarg($this->destination->network);
instant_remote_process(["docker network disconnect {$safeNetwork} coolify-proxy"], $this->destination->server, throwError: false);
instant_remote_process(["docker network rm -f {$safeNetwork}"], $this->destination->server);
}
$this->destination->delete();
+2 -2
View File
@@ -1203,7 +1203,7 @@ class GlobalSearch extends Component
public function loadDestinations()
{
$this->loadingDestinations = true;
$server = Server::find($this->selectedServerId);
$server = Server::ownedByCurrentTeam()->find($this->selectedServerId);
if (! $server) {
$this->loadingDestinations = false;
@@ -1280,7 +1280,7 @@ class GlobalSearch extends Component
public function loadEnvironments()
{
$this->loadingEnvironments = true;
$project = Project::where('uuid', $this->selectedProjectUuid)->first();
$project = Project::ownedByCurrentTeam()->where('uuid', $this->selectedProjectUuid)->first();
if (! $project) {
$this->loadingEnvironments = false;
@@ -735,6 +735,7 @@ class General extends Component
$this->authorize('update', $this->application);
try {
$this->application->redirect = $this->redirect;
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
if ($has_www === 0 && $this->application->redirect === 'www') {
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.<br><br>Please add www to your domain list and as an A DNS record (if applicable).');
+21 -18
View File
@@ -54,7 +54,7 @@ class CloneMe extends Component
public function mount($project_uuid)
{
$this->project_uuid = $project_uuid;
$this->project = Project::where('uuid', $project_uuid)->firstOrFail();
$this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments->where('uuid', $this->environment_uuid)->first();
$this->project_id = $this->project->id;
$this->servers = currentTeam()
@@ -139,7 +139,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'status' => 'exited',
'started_at' => null,
@@ -187,7 +187,8 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
'uuid',
])->forceFill([
'name' => $newName,
'resource_id' => $newDatabase->id,
]);
@@ -216,7 +217,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resource_id' => $newDatabase->id,
]);
$newStorage->save();
@@ -229,7 +230,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'database_id' => $newDatabase->id,
'database_type' => $newDatabase->getMorphClass(),
@@ -247,7 +248,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill($payload);
])->forceFill($payload);
$newEnvironmentVariable->save();
}
}
@@ -258,7 +259,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination,
@@ -276,7 +277,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => (string) new Cuid2,
'service_id' => $newService->id,
'team_id' => currentTeam()->id,
@@ -290,7 +291,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resourceable_id' => $newService->id,
'resourceable_type' => $newService->getMorphClass(),
]);
@@ -298,9 +299,9 @@ class CloneMe extends Component
}
foreach ($newService->applications() as $application) {
$application->update([
$application->forceFill([
'status' => 'exited',
]);
])->save();
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -315,7 +316,8 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
'uuid',
])->forceFill([
'name' => $newName,
'resource_id' => $application->id,
]);
@@ -344,7 +346,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resource_id' => $application->id,
]);
$newStorage->save();
@@ -352,9 +354,9 @@ class CloneMe extends Component
}
foreach ($newService->databases() as $database) {
$database->update([
$database->forceFill([
'status' => 'exited',
]);
])->save();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -369,7 +371,8 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
'uuid',
])->forceFill([
'name' => $newName,
'resource_id' => $database->id,
]);
@@ -398,7 +401,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resource_id' => $database->id,
]);
$newStorage->save();
@@ -411,7 +414,7 @@ class CloneMe extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'database_id' => $database->id,
'database_type' => $database->getMorphClass(),
+2 -2
View File
@@ -21,7 +21,7 @@ class DeleteProject extends Component
public function mount()
{
$this->parameters = get_route_parameters();
$this->projectName = Project::findOrFail($this->project_id)->name;
$this->projectName = Project::ownedByCurrentTeam()->findOrFail($this->project_id)->name;
}
public function delete()
@@ -29,7 +29,7 @@ class DeleteProject extends Component
$this->validate([
'project_id' => 'required|int',
]);
$project = Project::findOrFail($this->project_id);
$project = Project::ownedByCurrentTeam()->findOrFail($this->project_id);
$this->authorize('delete', $project);
if ($project->isEmpty()) {
+2 -2
View File
@@ -41,8 +41,8 @@ class DockerCompose extends Component
// Validate for command injection BEFORE saving to database
validateDockerComposeForInjection($this->dockerComposeRaw);
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
+2 -2
View File
@@ -121,8 +121,8 @@ class DockerImage extends Component
}
$destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
// Append @sha256 to image name if using digest and not already present
$imageName = $parser->getFullImageNameWithoutTag();
@@ -185,8 +185,8 @@ class GithubPrivateRepository extends Component
}
$destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$application = Application::create([
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
@@ -144,8 +144,8 @@ class GithubPrivateRepositoryDeployKey extends Component
// Note: git_repository has already been validated and transformed in get_git_source()
// It may now be in SSH format (git@host:repo.git) which is valid for deploy keys
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
if ($this->git_source === 'other') {
$application_init = [
'name' => generate_random_name(),
@@ -278,8 +278,8 @@ class PublicGitRepository extends Component
}
$destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $project_uuid)->first();
$environment = $project->load(['environments'])->environments->where('uuid', $environment_uuid)->first();
$project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
$environment = $project->environments()->where('uuid', $environment_uuid)->firstOrFail();
if ($this->build_pack === 'dockercompose' && isDev() && $this->new_compose_services) {
$server = $destination->server;
+2 -2
View File
@@ -65,7 +65,7 @@ class Select extends Component
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
}
$projectUuid = data_get($this->parameters, 'project_uuid');
$project = Project::whereUuid($projectUuid)->firstOrFail();
$project = Project::ownedByCurrentTeam()->whereUuid($projectUuid)->firstOrFail();
$this->environments = $project->environments;
$this->selectedEnvironment = $this->environments->where('uuid', data_get($this->parameters, 'environment_uuid'))->firstOrFail()->name;
@@ -79,7 +79,7 @@ class Select extends Component
$this->type = $queryType;
$this->server_id = $queryServerId;
$this->destination_uuid = $queryDestination;
$this->server = Server::find($queryServerId);
$this->server = Server::ownedByCurrentTeam()->find($queryServerId);
$this->current_step = 'select-postgresql-type';
}
} catch (\Exception $e) {
@@ -45,8 +45,8 @@ CMD ["nginx", "-g", "daemon off;"]
}
$destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
$port = get_port_from_dockerfile($this->dockerfile);
if (! $port) {
+27 -5
View File
@@ -16,7 +16,9 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use App\Support\ValidationPatterns;
use Illuminate\Support\Facades\Process;
use Livewire\Attributes\Locked;
use Livewire\Component;
class GetLogs extends Component
@@ -29,12 +31,16 @@ class GetLogs extends Component
public string $errors = '';
#[Locked]
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|null $resource = null;
#[Locked]
public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
#[Locked]
public Server $server;
#[Locked]
public ?string $container = null;
public ?string $displayName = null;
@@ -54,7 +60,7 @@ class GetLogs extends Component
public function mount()
{
if (! is_null($this->resource)) {
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
if ($this->resource->getMorphClass() === Application::class) {
$this->showTimeStamps = $this->resource->settings->is_include_timestamps;
} else {
if ($this->servicesubtype) {
@@ -63,7 +69,7 @@ class GetLogs extends Component
$this->showTimeStamps = $this->resource->is_include_timestamps;
}
}
if ($this->resource?->getMorphClass() === \App\Models\Application::class) {
if ($this->resource?->getMorphClass() === Application::class) {
if (str($this->container)->contains('-pr-')) {
$this->pull_request = 'Pull Request: '.str($this->container)->afterLast('-pr-')->beforeLast('_')->value();
}
@@ -74,11 +80,11 @@ class GetLogs extends Component
public function instantSave()
{
if (! is_null($this->resource)) {
if ($this->resource->getMorphClass() === \App\Models\Application::class) {
if ($this->resource->getMorphClass() === Application::class) {
$this->resource->settings->is_include_timestamps = $this->showTimeStamps;
$this->resource->settings->save();
}
if ($this->resource->getMorphClass() === \App\Models\Service::class) {
if ($this->resource->getMorphClass() === Service::class) {
$serviceName = str($this->container)->beforeLast('-')->value();
$subType = $this->resource->applications()->where('name', $serviceName)->first();
if ($subType) {
@@ -118,10 +124,20 @@ class GetLogs extends Component
public function getLogs($refresh = false)
{
if (! Server::ownedByCurrentTeam()->where('id', $this->server->id)->exists()) {
$this->outputs = 'Unauthorized.';
return;
}
if (! $this->server->isFunctional()) {
return;
}
if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) {
if ($this->container && ! ValidationPatterns::isValidContainerName($this->container)) {
$this->outputs = 'Invalid container name.';
return;
}
if (! $refresh && ! $this->expandByDefault && ($this->resource?->getMorphClass() === Service::class || str($this->container)->contains('-pr-'))) {
return;
}
if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
@@ -194,9 +210,15 @@ class GetLogs extends Component
public function downloadAllLogs(): string
{
if (! Server::ownedByCurrentTeam()->where('id', $this->server->id)->exists()) {
return '';
}
if (! $this->server->isFunctional() || ! $this->container) {
return '';
}
if (! ValidationPatterns::isValidContainerName($this->container)) {
return '';
}
if ($this->showTimeStamps) {
if ($this->server->isSwarm()) {
@@ -7,9 +7,18 @@ use App\Actions\Database\StopDatabase;
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 @@ class ResourceOperations extends Component
$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 @@ class ResourceOperations extends Component
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,8 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->fill([
'uuid',
])->forceFill([
'name' => $newName,
'resource_id' => $new_resource->id,
]);
@@ -162,7 +172,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resource_id' => $new_resource->id,
]);
$newStorage->save();
@@ -175,7 +185,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'database_id' => $new_resource->id,
'database_type' => $new_resource->getMorphClass(),
@@ -194,7 +204,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->fill($payload);
])->forceFill($payload);
$newEnvironmentVariable->save();
}
@@ -211,7 +221,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => $uuid,
'name' => $this->resource->name.'-clone-'.$uuid,
'destination_id' => $new_destination->id,
@@ -232,7 +242,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'uuid' => (string) new Cuid2,
'service_id' => $new_resource->id,
'team_id' => currentTeam()->id,
@@ -246,7 +256,7 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->fill([
])->forceFill([
'resourceable_id' => $new_resource->id,
'resourceable_type' => $new_resource->getMorphClass(),
]);
@@ -254,9 +264,9 @@ class ResourceOperations extends Component
}
foreach ($new_resource->applications() as $application) {
$application->update([
$application->forceFill([
'status' => 'exited',
]);
])->save();
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -271,7 +281,8 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->fill([
'uuid',
])->forceFill([
'name' => $newName,
'resource_id' => $application->id,
]);
@@ -296,9 +307,9 @@ class ResourceOperations extends Component
}
foreach ($new_resource->databases() as $database) {
$database->update([
$database->forceFill([
'status' => 'exited',
]);
])->save();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
@@ -313,7 +324,8 @@ class ResourceOperations extends Component
'id',
'created_at',
'updated_at',
])->fill([
'uuid',
])->forceFill([
'name' => $newName,
'resource_id' => $database->id,
]);
@@ -354,9 +366,9 @@ class ResourceOperations extends Component
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,
+97 -12
View File
@@ -118,7 +118,92 @@ 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_pr_location',
'docker_compose',
'docker_compose_pr',
'docker_compose_raw',
'docker_compose_pr_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',
'nixpkgsarchive',
'is_http_basic_auth_enabled',
'http_basic_auth_username',
'http_basic_auth_password',
'connect_to_docker_network',
'force_domain_override',
'is_container_label_escape_enabled',
'use_build_server',
'config_hash',
'last_online_at',
'restart_count',
'last_restart_at',
'last_restart_type',
];
protected $appends = ['server_status'];
@@ -1145,7 +1230,7 @@ class Application extends BaseModel
'is_accessible' => true,
'error' => null,
];
} catch (\RuntimeException $ex) {
} catch (RuntimeException $ex) {
return [
'is_accessible' => false,
'error' => $ex->getMessage(),
@@ -1202,7 +1287,7 @@ class Application extends BaseModel
];
}
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 +1439,7 @@ class Application extends BaseModel
$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 +1494,7 @@ class Application extends BaseModel
];
}
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 +1685,7 @@ class Application extends BaseModel
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 +1767,7 @@ class Application extends BaseModel
$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. Please verify repository access and try again.');
}
$getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false);
$gitVersion = str($getGitVersion)->explode(' ')->last();
@@ -1732,15 +1817,15 @@ class Application extends BaseModel
$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('Failed to read the Docker Compose file from the repository.');
} finally {
// Cleanup only - restoration happens in catch block
$commands = collect([
@@ -1793,7 +1878,7 @@ class Application extends BaseModel
$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.");
}
}
-2
View File
@@ -265,8 +265,6 @@ class Server extends BaseModel
'server_metadata',
];
protected $guarded = [];
use HasSafeStringAttribute;
public function setValidationLogsAttribute($value): void
+1
View File
@@ -56,6 +56,7 @@ class ServerSetting extends Model
protected $guarded = [];
protected $casts = [
'force_disabled' => 'boolean',
'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer',
'sentinel_token' => 'encrypted',
+12 -2
View File
@@ -15,6 +15,7 @@ use Illuminate\Support\Facades\Storage;
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,16 @@ 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',
];
protected $appends = ['server_status', 'status'];
@@ -1552,7 +1562,7 @@ class Service extends BaseModel
// 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);
+26 -2
View File
@@ -13,12 +13,36 @@ 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',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
protected $casts = [
'clickhouse_password' => 'encrypted',
'clickhouse_admin_password' => 'encrypted',
'public_port_timeout' => 'integer',
'restart_count' => 'integer',
'last_restart_at' => 'datetime',
+12 -1
View File
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Jobs\ConnectProxyToNetworksJob;
use App\Support\ValidationPatterns;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -18,13 +19,23 @@ class StandaloneDocker extends BaseModel
parent::boot();
static::created(function ($newStandaloneDocker) {
$server = $newStandaloneDocker->server;
$safeNetwork = escapeshellarg($newStandaloneDocker->network);
instant_remote_process([
"docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null",
"docker network inspect {$safeNetwork} >/dev/null 2>&1 || docker network create --driver overlay --attachable {$safeNetwork} >/dev/null",
], $server, false);
ConnectProxyToNetworksJob::dispatchSync($server);
});
}
public function setNetworkAttribute(string $value): void
{
if (! ValidationPatterns::isValidDockerNetwork($value)) {
throw new \InvalidArgumentException('Invalid Docker network name. Must start with alphanumeric and contain only alphanumeric characters, dots, hyphens, and underscores.');
}
$this->attributes['network'] = $value;
}
public function applications()
{
return $this->morphMany(Application::class, 'destination');
+24 -1
View File
@@ -13,7 +13,30 @@ 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',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+25 -1
View File
@@ -13,7 +13,31 @@ 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',
];
protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];
+26 -1
View File
@@ -14,7 +14,32 @@ 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',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+25 -1
View File
@@ -13,7 +13,31 @@ 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',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+26 -1
View File
@@ -13,7 +13,32 @@ 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',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+28 -1
View File
@@ -13,7 +13,34 @@ 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',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+22 -1
View File
@@ -13,7 +13,28 @@ 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',
];
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
+11
View File
@@ -2,10 +2,21 @@
namespace App\Models;
use App\Support\ValidationPatterns;
class SwarmDocker extends BaseModel
{
protected $guarded = [];
public function setNetworkAttribute(string $value): void
{
if (! ValidationPatterns::isValidDockerNetwork($value)) {
throw new \InvalidArgumentException('Invalid Docker network name. Must start with alphanumeric and contain only alphanumeric characters, dots, hyphens, and underscores.');
}
$this->attributes['network'] = $value;
}
public function applications()
{
return $this->morphMany(Application::class, 'destination');
+7 -1
View File
@@ -40,7 +40,13 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
{
use HasFactory, HasNotificationSettings, HasSafeStringAttribute, Notifiable;
protected $guarded = [];
protected $fillable = [
'name',
'description',
'personal_team',
'show_boarding',
'custom_server_limit',
];
protected $casts = [
'personal_team' => 'boolean',
+16 -8
View File
@@ -4,7 +4,9 @@ namespace App\Models;
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,13 @@ class User extends Authenticatable implements SendsEmail
{
use DeletesUserSessions, HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
protected $guarded = [];
protected $fillable = [
'name',
'email',
'password',
'force_password_reset',
'marketing_emails',
];
protected $hidden = [
'password',
@@ -87,7 +95,7 @@ class User extends Authenticatable implements SendsEmail
$team['id'] = 0;
$team['name'] = 'Root Team';
}
$new_team = Team::create($team);
$new_team = Team::forceCreate($team);
$user->teams()->attach($new_team, ['role' => 'owner']);
});
@@ -190,7 +198,7 @@ class User extends Authenticatable implements SendsEmail
$team['id'] = 0;
$team['name'] = 'Root Team';
}
$new_team = Team::create($team);
$new_team = Team::forceCreate($team);
$this->teams()->attach($new_team, ['role' => 'owner']);
return $new_team;
@@ -228,7 +236,7 @@ class User extends Authenticatable implements SendsEmail
public function getUnreadChangelogCount(): int
{
return app(\App\Services\ChangelogService::class)->getUnreadCountForUser($this);
return app(ChangelogService::class)->getUnreadCountForUser($this);
}
public function getRecipients(): array
@@ -239,7 +247,7 @@ class User extends Authenticatable implements SendsEmail
public function sendVerificationEmail()
{
$mail = new MailMessage;
$url = Url::temporarySignedRoute(
$url = URL::temporarySignedRoute(
'verify.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
@@ -401,14 +409,14 @@ class User extends Authenticatable implements SendsEmail
$expiryMinutes = config('constants.email_change.verification_code_expiry_minutes', 10);
$expiresAt = Carbon::now()->addMinutes($expiryMinutes);
$this->update([
$this->forceFill([
'pending_email' => $newEmail,
'email_change_code' => $code,
'email_change_code_expires_at' => $expiresAt,
]);
])->save();
// 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
+45
View File
@@ -58,6 +58,13 @@ class ValidationPatterns
*/
public const CONTAINER_NAME_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/';
/**
* Pattern for Docker network names
* Must start with alphanumeric, followed by alphanumeric, dots, hyphens, or underscores
* Matches Docker's network naming rules and prevents shell injection
*/
public const DOCKER_NETWORK_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/';
/**
* Get validation rules for name fields
*/
@@ -210,6 +217,44 @@ class ValidationPatterns
return preg_match(self::CONTAINER_NAME_PATTERN, $name) === 1;
}
/**
* Get validation rules for Docker network name fields
*/
public static function dockerNetworkRules(bool $required = true, int $maxLength = 255): array
{
$rules = [];
if ($required) {
$rules[] = 'required';
} else {
$rules[] = 'nullable';
}
$rules[] = 'string';
$rules[] = "max:$maxLength";
$rules[] = 'regex:'.self::DOCKER_NETWORK_PATTERN;
return $rules;
}
/**
* Get validation messages for Docker network name fields
*/
public static function dockerNetworkMessages(string $field = 'network'): array
{
return [
"{$field}.regex" => 'The network name must start with an alphanumeric character and contain only alphanumeric characters, dots, hyphens, and underscores.',
];
}
/**
* Check if a string is a valid Docker network name.
*/
public static function isValidDockerNetwork(string $name): bool
{
return preg_match(self::DOCKER_NETWORK_PATTERN, $name) === 1;
}
/**
* Get combined validation messages for both name and description fields
*/