diff --git a/README.md b/README.md
index 0a3ce0132..dac48d127 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ Special thanks to our biggest sponsors!
### Special Sponsors
-
+
* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry.
* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions.
@@ -50,6 +50,7 @@ Special thanks to our biggest sponsors!
* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
+* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies.
* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
@@ -90,7 +91,6 @@ Special thanks to our biggest sponsors!
-
@@ -147,10 +147,10 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
# Core Maintainers
-| Andras Bacsai | Peak |
+| Andras Bacsai | 🏔️ Peak |
|------------|------------|
-|
|
|
-|
|
|
+|
|
|
+|
|
|
# Repo Activity
diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php
index 13667e829..42c6e1449 100644
--- a/app/Actions/Database/StartClickhouse.php
+++ b/app/Actions/Database/StartClickhouse.php
@@ -24,7 +24,7 @@ class StartClickhouse
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index c72714e1c..ea235be4e 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -26,7 +26,7 @@ class StartDragonfly
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index bd98258ab..010bf5884 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -27,7 +27,7 @@ class StartKeydb
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index 696dd7ff4..2437a013e 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -24,7 +24,7 @@ class StartMariadb
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 26a0f82d0..a33e72c27 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -30,7 +30,7 @@ class StartMongodb
}
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index a3694648f..0b19b3f0c 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -24,7 +24,7 @@ class StartMysql
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index f5e85087f..7faa232c3 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -25,7 +25,7 @@ class StartPostgresql
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/",
];
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index 7a2d2b34d..bacf49f82 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -25,7 +25,7 @@ class StartRedis
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
- "echo 'Starting {$database->name}.'",
+ "echo 'Starting database.'",
"mkdir -p $this->configuration_dir",
];
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index 8aae910a9..706356930 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -179,7 +179,7 @@ class GetContainersStatus
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
- // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
+ // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
}
}
} else {
diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php
index 5f9a1e357..75b8501f3 100644
--- a/app/Actions/Server/ServerCheck.php
+++ b/app/Actions/Server/ServerCheck.php
@@ -51,7 +51,6 @@ class ServerCheck
$containerReplicates = null;
$this->isSentinel = true;
-
} else {
['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
// ServerStorageCheckJob::dispatch($this->server);
@@ -148,7 +147,6 @@ class ServerCheck
} else {
$labels = Arr::undot(data_get($container, 'Config.Labels'));
}
-
}
$managed = data_get($labels, 'coolify.managed');
if (! $managed) {
@@ -259,7 +257,7 @@ class ServerCheck
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
- // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
+ // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
}
}
}
diff --git a/app/Console/Commands/CloudCleanupSubscriptions.php b/app/Console/Commands/CloudCleanupSubscriptions.php
index 8bb420ab8..9198b003e 100644
--- a/app/Console/Commands/CloudCleanupSubscriptions.php
+++ b/app/Console/Commands/CloudCleanupSubscriptions.php
@@ -36,7 +36,7 @@ class CloudCleanupSubscriptions extends Command
}
// If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
- $this->info("Resetting invoice paid status for team {$team->id} {$team->name}");
+ $this->info("Resetting invoice paid status for team {$team->id}");
$team->subscription->update([
'stripe_invoice_paid' => false,
@@ -61,9 +61,9 @@ class CloudCleanupSubscriptions extends Command
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
$confirm = $this->confirm('Do you want to cancel the subscription?', true);
if (! $confirm) {
- $this->info("Skipping team {$team->id} {$team->name}");
+ $this->info("Skipping team {$team->id}");
} else {
- $this->info("Cancelling subscription for team {$team->id} {$team->name}");
+ $this->info("Cancelling subscription for team {$team->id}");
$team->subscription->update([
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index c69640970..614208c78 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -1591,16 +1591,32 @@ class ApplicationsController extends Controller
}
$domains = $request->domains;
if ($request->has('domains') && $server->isProxyShouldRun()) {
- $errors = [];
+ $uuid = $request->uuid;
$fqdn = $request->domains;
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
- $application->fqdn = $fqdn;
- if (! $application->settings->is_container_label_readonly_enabled) {
- $customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
- $application->custom_labels = base64_encode($customLabels);
+ $errors = [];
+ $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
+ $domain = trim($domain);
+ if (filter_var($domain, FILTER_VALIDATE_URL) === false || !preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}/', $domain)) {
+ $errors[] = 'Invalid domain: '.$domain;
+ }
+ return $domain;
+ });
+ if (count($errors) > 0) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+ if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'domains' => 'One of the domain is already used.',
+ ],
+ ], 422);
}
- $request->offsetUnset('domains');
}
$dockerComposeDomainsJson = collect();
@@ -2811,3 +2827,30 @@ class ApplicationsController extends Controller
}
}
}
+
+ $fqdn = str($fqdn)->replaceStart(',', '')->trim();
+ $errors = [];
+ $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
+ if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
+ $errors[] = 'Invalid domain: ' . $domain;
+ }
+
+ return str($domain)->trim()->lower();
+ });
+ if (count($errors) > 0) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+ if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'domains' => 'One of the domain is already used.',
+ ],
+ ], 422);
+ }
+ }
+ }
+}
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index ce658d2a2..98a076c49 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -211,8 +211,9 @@ class DatabasesController extends Controller
'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'],
'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'],
'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'],
- 'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
+ 'mongo_initdb_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
+ 'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -241,7 +242,7 @@ class DatabasesController extends Controller
)]
public function update_by_uuid(Request $request)
{
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -413,12 +414,12 @@ class DatabasesController extends Controller
}
break;
case 'standalone-mongodb':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
- 'mongo_initdb_init_database' => 'string',
+ 'mongo_initdb_database' => 'string',
]);
if ($request->has('mongo_conf')) {
if (! isBase64Encoded($request->mongo_conf)) {
@@ -443,9 +444,10 @@ class DatabasesController extends Controller
break;
case 'standalone-mysql':
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
+ 'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@@ -909,6 +911,7 @@ class DatabasesController extends Controller
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
+ 'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@@ -1013,7 +1016,7 @@ class DatabasesController extends Controller
public function create_database(Request $request, NewDatabaseTypes $type)
{
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -1220,9 +1223,10 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MYSQL) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_user', 'mysql_database', 'mysql_conf'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
+ 'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@@ -1456,12 +1460,12 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MONGODB) {
- $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
+ $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
- 'mongo_initdb_init_database' => 'string',
+ 'mongo_initdb_database' => 'string',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
@@ -1557,7 +1561,8 @@ class DatabasesController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1632,9 +1637,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database starting request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1708,9 +1715,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1784,9 +1793,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php
index 3683adaa8..ac1d4ded2 100644
--- a/app/Http/Controllers/Webhook/Github.php
+++ b/app/Http/Controllers/Webhook/Github.php
@@ -463,7 +463,7 @@ class Github extends Controller
$private_key = data_get($data, 'pem');
$webhook_secret = data_get($data, 'webhook_secret');
$private_key = PrivateKey::create([
- 'name' => $slug,
+ 'name' => "github-app-{$slug}",
'private_key' => $private_key,
'team_id' => $github_app->team_id,
'is_git_related' => true,
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 6a66bb56d..04e71c4e3 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -140,6 +140,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $buildTarget = null;
+ private bool $disableBuildCache = false;
+
private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
@@ -178,7 +180,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit;
$this->rollback = $this->application_deployment_queue->rollback;
+ $this->disableBuildCache = $this->application->settings->disable_build_cache;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
+ if ($this->disableBuildCache) {
+ $this->force_rebuild = true;
+ }
$this->restart_only = $this->application_deployment_queue->restart_only;
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
$this->only_this_server = $this->application_deployment_queue->only_this_server;
@@ -1976,6 +1982,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->build_args = $this->build_args->implode(' ');
$this->application_deployment_queue->addLogEntry('----------------------------------------');
+ if ($this->disableBuildCache) {
+ $this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.');
+ }
if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
} else {
@@ -2400,7 +2409,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if (! $this->only_this_server) {
$this->deploy_to_additional_destinations();
}
- $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
+ //$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
}
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 89674b255..ee702202f 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -306,7 +306,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
- $this->team?->notify(new BackupSuccess($this->backup, $this->database, $database));
+ //$this->team?->notify(new BackupSuccess($this->backup, $this->database, $database));
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index b6c799c4e..eadabba7c 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -172,13 +172,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function getProxyType()
{
- // Set Default Proxy Type
$this->selectProxy(ProxyTypes::TRAEFIK->value);
- // $proxyTypeSet = $this->createdServer->proxy->type;
- // if (!$proxyTypeSet) {
- // $this->currentState = 'select-proxy';
- // return;
- // }
$this->getProjects();
}
@@ -189,7 +183,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return;
}
- $this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey);
+ $this->createdPrivateKey = PrivateKey::where('team_id', currentTeam()->id)->where('id', $this->selectedExistingPrivateKey)->first();
$this->privateKey = $this->createdPrivateKey->private_key;
$this->currentState = 'create-server';
}
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index fcedf1305..ab3768643 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -37,7 +37,7 @@ class Email extends Component
#[Validate(['nullable', 'numeric'])]
public ?int $smtpPort = null;
- #[Validate(['nullable', 'string'])]
+ #[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
public ?string $smtpEncryption = null;
#[Validate(['nullable', 'string'])]
@@ -73,8 +73,8 @@ class Email extends Component
#[Validate(['nullable', 'string'])]
public ?string $resendApiKey = null;
- #[Validate(['required', 'email'])]
- public string $testEmailAddress = '';
+ #[Validate(['nullable', 'email'])]
+ public ?string $testEmailAddress = null;
public function mount()
{
diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php
index 05ac25429..cb63f0e1a 100644
--- a/app/Livewire/Project/Application/Advanced.php
+++ b/app/Livewire/Project/Application/Advanced.php
@@ -25,6 +25,9 @@ class Advanced extends Component
#[Validate(['boolean'])]
public bool $isAutoDeployEnabled = true;
+ #[Validate(['boolean'])]
+ public bool $disableBuildCache = false;
+
#[Validate(['boolean'])]
public bool $isLogDrainEnabled = false;
@@ -95,6 +98,7 @@ class Advanced extends Component
$this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled;
$this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled;
$this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled;
+ $this->application->settings->disable_build_cache = $this->disableBuildCache;
$this->application->settings->save();
} else {
$this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled();
@@ -116,6 +120,7 @@ class Advanced extends Component
$this->customInternalName = $this->application->settings->custom_internal_name;
$this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled;
$this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network;
+ $this->disableBuildCache = $this->application->settings->disable_build_cache;
}
}
diff --git a/app/Livewire/Project/Application/Configuration.php b/app/Livewire/Project/Application/Configuration.php
index 5e7f83772..5261a0800 100644
--- a/app/Livewire/Project/Application/Configuration.php
+++ b/app/Livewire/Project/Application/Configuration.php
@@ -21,19 +21,16 @@ class Configuration extends Component
->select('id', 'uuid', 'team_id')
->where('uuid', request()->route('project_uuid'))
->firstOrFail();
-
$environment = $project->environments()
->select('id', 'name', 'project_id')
->where('name', request()->route('environment_name'))
->firstOrFail();
-
$application = $environment->applications()
->with(['destination'])
->where('uuid', request()->route('application_uuid'))
->firstOrFail();
$this->application = $application;
-
if ($application->destination && $application->destination->server) {
$mainServer = $application->destination->server;
$this->servers = Server::ownedByCurrentTeam()
diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
index 621ab1bac..d12d8e26a 100644
--- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php
+++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
@@ -168,18 +168,42 @@ class ExecuteContainerCommand extends Component
return;
}
try {
+ // Validate container name format
+ if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $this->selected_container)) {
+ throw new \InvalidArgumentException('Invalid container name format');
+ }
+
+ // Verify container exists in our allowed list
$container = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
if (is_null($container)) {
throw new \RuntimeException('Container not found.');
}
- $server = data_get($this->container, 'server');
+
+ // Verify server ownership and status
+ $server = data_get($container, 'server');
+ if (! $server || ! $server instanceof Server) {
+ throw new \RuntimeException('Invalid server configuration.');
+ }
if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
+
+ // Additional ownership verification based on resource type
+ $resourceServer = match ($this->type) {
+ 'application' => $this->resource->destination->server,
+ 'database' => $this->resource->destination->server,
+ 'service' => $this->resource->server,
+ default => throw new \RuntimeException('Invalid resource type.')
+ };
+
+ if ($server->id !== $resourceServer->id && ! $this->resource->additional_servers->contains('id', $server->id)) {
+ throw new \RuntimeException('Server ownership verification failed.');
+ }
+
$this->dispatch(
'send-terminal-command',
- isset($container),
+ true,
data_get($container, 'container.Names'),
data_get($container, 'server.uuid')
);
diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php
index 5af8f057e..d8f101277 100644
--- a/app/Livewire/Project/Shared/Terminal.php
+++ b/app/Livewire/Project/Shared/Terminal.php
@@ -29,11 +29,20 @@ class Terminal extends Component
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
if ($isContainer) {
+ // Validate container identifier format (alphanumeric, dashes, and underscores only)
+ if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $identifier)) {
+ throw new \InvalidArgumentException('Invalid container identifier format');
+ }
+
+ // Verify container exists and belongs to the user's team
$status = getContainerStatus($server, $identifier);
if ($status !== 'running') {
return;
}
- $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
+
+ // Escape the identifier for shell usage
+ $escapedIdentifier = escapeshellarg($identifier);
+ $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} else {
$command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi');
}
diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php
index a5544489d..ac5211c1b 100644
--- a/app/Livewire/Server/Show.php
+++ b/app/Livewire/Server/Show.php
@@ -5,7 +5,7 @@ namespace App\Livewire\Server;
use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel;
use App\Models\Server;
-use Livewire\Attributes\Locked;
+use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -79,9 +79,6 @@ class Show extends Component
#[Validate(['required'])]
public string $serverTimezone;
- #[Locked]
- public array $timezones;
-
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@@ -96,13 +93,21 @@ class Show extends Component
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
- $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
+ #[Computed]
+ public function timezones(): array
+ {
+ return collect(timezone_identifiers_list())
+ ->sort()
+ ->values()
+ ->toArray();
+ }
+
public function syncData(bool $toModel = false)
{
if ($toModel) {
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index 31dd13c52..c1be35ced 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -7,7 +7,7 @@ use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
-use Livewire\Attributes\Locked;
+use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -17,9 +17,6 @@ class Index extends Component
protected Server $server;
- #[Locked]
- public $timezones;
-
#[Validate('boolean')]
public bool $is_auto_update_enabled;
@@ -101,12 +98,20 @@ class Index extends Component
$this->is_api_enabled = $this->settings->is_api_enabled;
$this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency;
- $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->instance_timezone = $this->settings->instance_timezone;
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
}
}
+ #[Computed]
+ public function timezones(): array
+ {
+ return collect(timezone_identifiers_list())
+ ->sort()
+ ->values()
+ ->toArray();
+ }
+
public function instantSave($isSave = true)
{
$this->validate();
diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php
index 61f720b3a..abf3a12f9 100644
--- a/app/Livewire/SettingsEmail.php
+++ b/app/Livewire/SettingsEmail.php
@@ -19,7 +19,7 @@ class SettingsEmail extends Component
#[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])]
public ?int $smtpPort = null;
- #[Validate(['nullable', 'string'])]
+ #[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
public ?string $smtpEncryption = null;
#[Validate(['nullable', 'string'])]
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index 07cef54f9..467927484 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -4,6 +4,11 @@ namespace App\Livewire\Source\Github;
use App\Jobs\GithubAppPermissionJob;
use App\Models\GithubApp;
+use App\Models\PrivateKey;
+use Illuminate\Support\Facades\Http;
+use Lcobucci\JWT\Configuration;
+use Lcobucci\JWT\Signer\Key\InMemory;
+use Lcobucci\JWT\Signer\Rsa\Sha256;
use Livewire\Component;
class Change extends Component
@@ -51,12 +56,20 @@ class Change extends Component
'github_app.administration' => 'nullable|string',
];
+ public function boot()
+ {
+ if ($this->github_app) {
+ $this->github_app->makeVisible(['client_secret', 'webhook_secret']);
+ }
+ }
+
public function checkPermissions()
{
GithubAppPermissionJob::dispatchSync($this->github_app);
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->dispatch('success', 'Github App permissions updated.');
}
+
// public function check()
// {
@@ -90,15 +103,16 @@ class Change extends Component
// ray($runners_by_repository);
// }
+
public function mount()
{
try {
$github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
+ $this->github_app->makeVisible(['client_secret', 'webhook_secret']);
$this->applications = $this->github_app->applications;
$settings = instanceSettings();
- $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->name = str($this->github_app->name)->kebab();
$this->fqdn = $settings->fqdn;
@@ -142,6 +156,77 @@ class Change extends Component
}
}
+ public function getGithubAppNameUpdatePath()
+ {
+ if (str($this->github_app->organization)->isNotEmpty()) {
+ return "{$this->github_app->html_url}/organizations/{$this->github_app->organization}/settings/apps/{$this->github_app->name}";
+ }
+
+ return "{$this->github_app->html_url}/settings/apps/{$this->github_app->name}";
+ }
+
+ private function generateGithubJwt($private_key, $app_id): string
+ {
+ $configuration = Configuration::forAsymmetricSigner(
+ new Sha256,
+ InMemory::plainText($private_key),
+ InMemory::plainText($private_key)
+ );
+
+ $now = time();
+
+ return $configuration->builder()
+ ->issuedBy((string) $app_id)
+ ->permittedFor('https://api.github.com')
+ ->identifiedBy((string) $now)
+ ->issuedAt(new \DateTimeImmutable("@{$now}"))
+ ->expiresAt(new \DateTimeImmutable('@'.($now + 600)))
+ ->getToken($configuration->signer(), $configuration->signingKey())
+ ->toString();
+ }
+
+ public function updateGithubAppName()
+ {
+ try {
+ $privateKey = PrivateKey::ownedByCurrentTeam()->find($this->github_app->private_key_id);
+
+ if (! $privateKey) {
+ $this->dispatch('error', 'No private key found for this GitHub App.');
+
+ return;
+ }
+
+ $jwt = $this->generateGithubJwt($privateKey->private_key, $this->github_app->app_id);
+
+ $response = Http::withHeaders([
+ 'Accept' => 'application/vnd.github+json',
+ 'X-GitHub-Api-Version' => '2022-11-28',
+ 'Authorization' => "Bearer {$jwt}",
+ ])->get("{$this->github_app->api_url}/app");
+
+ if ($response->successful()) {
+ $app_data = $response->json();
+ $app_slug = $app_data['slug'] ?? null;
+
+ if ($app_slug) {
+ $this->github_app->name = $app_slug;
+ $this->name = str($app_slug)->kebab();
+ $privateKey->name = "github-app-{$app_slug}";
+ $privateKey->save();
+ $this->github_app->save();
+ $this->dispatch('success', 'GitHub App name and SSH key name synchronized successfully.');
+ } else {
+ $this->dispatch('info', 'Could not find App Name (slug) in GitHub response.');
+ }
+ } else {
+ $error_message = $response->json()['message'] ?? 'Unknown error';
+ $this->dispatch('error', "Failed to fetch GitHub App information: {$error_message}");
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
public function submit()
{
try {
diff --git a/app/Models/Application.php b/app/Models/Application.php
index c284528f1..a68c1d54a 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -4,6 +4,7 @@ namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Process\InvokedProcess;
@@ -104,7 +105,7 @@ use Visus\Cuid2\Cuid2;
class Application extends BaseModel
{
- use SoftDeletes;
+ use HasFactory, SoftDeletes;
private static $parserVersion = '4';
diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php
index 17201ea6e..79801987b 100644
--- a/app/Models/BaseModel.php
+++ b/app/Models/BaseModel.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Visus\Cuid2\Cuid2;
@@ -18,4 +19,18 @@ abstract class BaseModel extends Model
}
});
}
+
+ public function name(): Attribute
+ {
+ return new Attribute(
+ get: fn () => sanitize_string($this->getRawOriginal('name')),
+ );
+ }
+
+ public function image(): Attribute
+ {
+ return new Attribute(
+ get: fn () => sanitize_string($this->getRawOriginal('image')),
+ );
+ }
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 27c2b9b99..77673b959 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -11,6 +11,7 @@ use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
@@ -48,7 +49,7 @@ use Symfony\Component\Yaml\Yaml;
class Server extends BaseModel
{
- use SchemalessAttributesTrait, SoftDeletes;
+ use HasFactory, SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0;
@@ -610,7 +611,8 @@ $schema://$host {
}
$memory = json_decode($memory, true);
$parsedCollection = collect($memory)->map(function ($metric) {
- return [(int) $metric['time'], (float) $metric['usedPercent']];
+ $usedPercent = $metric['usedPercent'] ?? 0.0;
+ return [(int) $metric['time'], (float) $usedPercent];
});
return $parsedCollection->toArray();
@@ -1039,7 +1041,7 @@ $schema://$host {
$this->unreachable_notification_sent = false;
$this->save();
$this->refresh();
- $this->team->notify(new Reachable($this));
+ // $this->team->notify(new Reachable($this));
}
public function sendUnreachableNotification()
@@ -1047,7 +1049,7 @@ $schema://$host {
$this->unreachable_notification_sent = true;
$this->save();
$this->refresh();
- $this->team->notify(new Unreachable($this));
+ // $this->team->notify(new Unreachable($this));
}
public function validateConnection(bool $justCheckingNewKey = false)
diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php
index af9af978d..5394f6106 100644
--- a/app/Notifications/Channels/EmailChannel.php
+++ b/app/Notifications/Channels/EmailChannel.php
@@ -66,11 +66,12 @@ class EmailChannel
'transport' => 'smtp',
'host' => data_get($notifiable, 'smtp_host'),
'port' => data_get($notifiable, 'smtp_port'),
- 'encryption' => data_get($notifiable, 'smtp_encryption'),
+ 'encryption' => data_get($notifiable, 'smtp_encryption') === 'none' ? null : data_get($notifiable, 'smtp_encryption'),
'username' => data_get($notifiable, 'smtp_username'),
'password' => data_get($notifiable, 'smtp_password'),
'timeout' => data_get($notifiable, 'smtp_timeout'),
'local_domain' => null,
+ 'auto_tls' => data_get($notifiable, 'smtp_encryption') === 'none' ? '0' : '',
]);
}
}
diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php
index 303fcab8e..b568e090c 100644
--- a/bootstrap/helpers/constants.php
+++ b/bootstrap/helpers/constants.php
@@ -46,7 +46,7 @@ const SPECIFIC_SERVICES = [
// Based on /etc/os-release
const SUPPORTED_OS = [
- 'ubuntu debian raspbian',
+ 'ubuntu debian raspbian pop',
'centos fedora rhel ol rocky amzn almalinux',
'sles opensuse-leap opensuse-tumbleweed',
'arch',
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 8dd01a162..eda2133a7 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -288,9 +288,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$host_without_www = str($host)->replace('www.', '');
$schema = $url->getScheme();
$port = $url->getPort();
- $handle = "handle_path";
- if ( ! $is_stripprefix_enabled){
- $handle = "handle";
+ $handle = 'handle_path';
+ if (! $is_stripprefix_enabled) {
+ $handle = 'handle';
}
if (is_null($port) && ! is_null($onlyPort)) {
$port = $onlyPort;
@@ -302,7 +302,6 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$labels->push("caddy_{$loop}.header=-Server");
$labels->push("caddy_{$loop}.try_files={path} /index.html /index.php");
-
if ($port) {
$labels->push("caddy_{$loop}.{$handle}.{$loop}_reverse_proxy={{upstreams $port}}");
} else {
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index f09255ea0..40c5acb21 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -91,8 +91,31 @@ function metrics_dir(): string
return base_configuration_dir() . '/metrics';
}
+function sanitize_string(?string $input = null): ?string
+{
+ if (is_null($input)) {
+ return null;
+ }
+ // Remove any HTML/PHP tags
+ $sanitized = strip_tags($input);
+
+ // Convert special characters to HTML entities
+ $sanitized = htmlspecialchars($sanitized, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+
+ // Remove any control characters
+ $sanitized = preg_replace('/[\x00-\x1F\x7F]/u', '', $sanitized);
+
+ // Trim whitespace
+ $sanitized = trim($sanitized);
+
+ return $sanitized;
+}
+
function generate_readme_file(string $name, string $updated_at): string
{
+ $name = sanitize_string($name);
+ $updated_at = sanitize_string($updated_at);
+
return "Resource name: $name\nLatest Deployment Date: $updated_at";
}
diff --git a/config/constants.php b/config/constants.php
index c947635be..f7d5a7831 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,7 +2,7 @@
return [
'coolify' => [
- 'version' => '4.0.0-beta.374',
+ 'version' => '4.0.0-beta.376',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
diff --git a/config/database.php b/config/database.php
index f48a68082..6f4acbfd2 100644
--- a/config/database.php
+++ b/config/database.php
@@ -49,6 +49,22 @@ return [
'search_path' => 'public',
'sslmode' => 'prefer',
],
+
+ 'testing' => [
+ 'driver' => 'pgsql',
+ 'url' => env('DATABASE_TEST_URL'),
+ 'host' => env('DB_TEST_HOST', 'postgres'),
+ 'port' => env('DB_TEST_PORT', '5432'),
+ 'database' => env('DB_TEST_DATABASE', 'coolify_test'),
+ 'username' => env('DB_TEST_USERNAME', 'coolify'),
+ 'password' => env('DB_TEST_PASSWORD', 'password'),
+ 'charset' => 'utf8',
+ 'prefix' => '',
+ 'prefix_indexes' => true,
+ 'search_path' => 'public',
+ 'sslmode' => 'prefer',
+ ],
+
],
/*
diff --git a/database/factories/ApplicationFactory.php b/database/factories/ApplicationFactory.php
new file mode 100644
index 000000000..ded507c56
--- /dev/null
+++ b/database/factories/ApplicationFactory.php
@@ -0,0 +1,22 @@
+ fake()->unique()->name(),
+ 'destination_id' => 1,
+ 'git_repository' => fake()->url(),
+ 'git_branch' => fake()->word(),
+ 'build_pack' => 'nixpacks',
+ 'ports_exposes' => '3000',
+ 'environment_id' => 1,
+ 'destination_id' => 1,
+ ];
+ }
+}
diff --git a/database/factories/ServerFactory.php b/database/factories/ServerFactory.php
new file mode 100644
index 000000000..29546bf56
--- /dev/null
+++ b/database/factories/ServerFactory.php
@@ -0,0 +1,17 @@
+ fake()->unique()->name(),
+ 'ip' => fake()->unique()->ipv4(),
+ 'private_key_id' => 1,
+ ];
+ }
+}
diff --git a/database/migrations/2024_12_05_091823_add_disable_build_cache_advanced_option.php b/database/migrations/2024_12_05_091823_add_disable_build_cache_advanced_option.php
new file mode 100644
index 000000000..751342302
--- /dev/null
+++ b/database/migrations/2024_12_05_091823_add_disable_build_cache_advanced_option.php
@@ -0,0 +1,22 @@
+boolean('disable_build_cache')->default(false);
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('application_settings', function (Blueprint $table) {
+ $table->dropColumn('disable_build_cache');
+ });
+ }
+};
diff --git a/database/seeders/GithubAppSeeder.php b/database/seeders/GithubAppSeeder.php
index 2ece7a05b..3cfb82e64 100644
--- a/database/seeders/GithubAppSeeder.php
+++ b/database/seeders/GithubAppSeeder.php
@@ -23,6 +23,7 @@ class GithubAppSeeder extends Seeder
GithubApp::create([
'name' => 'coolify-laravel-development-public',
'uuid' => '69420',
+ 'organization' => 'coollabsio',
'api_url' => 'https://api.github.com',
'html_url' => 'https://github.com',
'is_public' => false,
diff --git a/openapi.json b/openapi.json
index 2ec218438..5d35331ec 100644
--- a/openapi.json
+++ b/openapi.json
@@ -3011,7 +3011,7 @@
"type": "string",
"description": "Mongo initdb root password"
},
- "mongo_initdb_init_database": {
+ "mongo_initdb_database": {
"type": "string",
"description": "Mongo initdb init database"
},
@@ -3019,6 +3019,10 @@
"type": "string",
"description": "MySQL root password"
},
+ "mysql_password": {
+ "type": "string",
+ "description": "MySQL password"
+ },
"mysql_user": {
"type": "string",
"description": "MySQL user"
@@ -3842,6 +3846,10 @@
"type": "string",
"description": "MySQL root password"
},
+ "mysql_password": {
+ "type": "string",
+ "description": "MySQL password"
+ },
"mysql_user": {
"type": "string",
"description": "MySQL user"
diff --git a/openapi.yaml b/openapi.yaml
index 2a22c730c..20bf34873 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -2089,12 +2089,15 @@ paths:
mongo_initdb_root_password:
type: string
description: 'Mongo initdb root password'
- mongo_initdb_init_database:
+ mongo_initdb_database:
type: string
description: 'Mongo initdb init database'
mysql_root_password:
type: string
description: 'MySQL root password'
+ mysql_password:
+ type: string
+ description: 'MySQL password'
mysql_user:
type: string
description: 'MySQL user'
@@ -2684,6 +2687,9 @@ paths:
mysql_root_password:
type: string
description: 'MySQL root password'
+ mysql_password:
+ type: string
+ description: 'MySQL password'
mysql_user:
type: string
description: 'MySQL user'
diff --git a/phpunit.xml b/phpunit.xml
index 45cb69439..f1c2be92d 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -13,8 +13,8 @@