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/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php
index 7c93720cb..9bc506d9b 100644
--- a/app/Actions/Proxy/StartProxy.php
+++ b/app/Actions/Proxy/StartProxy.php
@@ -2,6 +2,7 @@
namespace App\Actions\Proxy;
+use App\Enums\ProxyTypes;
use App\Events\ProxyStarted;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -37,11 +38,16 @@ class StartProxy
"echo 'Successfully started coolify-proxy.'",
]);
} else {
- $caddfile = 'import /dynamic/*.caddy';
+ if (isDev()) {
+ if ($proxyType === ProxyTypes::CADDY->value) {
+ $proxy_path = '/data/coolify/proxy/caddy';
+ }
+ }
+ $caddyfile = 'import /dynamic/*.caddy';
$commands = $commands->merge([
"mkdir -p $proxy_path/dynamic",
"cd $proxy_path",
- "echo '$caddfile' > $proxy_path/dynamic/Caddyfile",
+ "echo '$caddyfile' > $proxy_path/dynamic/Caddyfile",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
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 614208c78..f02c4255d 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -25,26 +25,24 @@ class ApplicationsController extends Controller
{
private function removeSensitiveData($application)
{
- $token = auth()->user()->currentAccessToken();
$application->makeHidden([
'id',
]);
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($application);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $application->makeHidden([
+ 'custom_labels',
+ 'dockerfile',
+ 'docker_compose',
+ 'docker_compose_raw',
+ 'manual_webhook_secret_bitbucket',
+ 'manual_webhook_secret_gitea',
+ 'manual_webhook_secret_github',
+ 'manual_webhook_secret_gitlab',
+ 'private_key_id',
+ 'value',
+ 'real_value',
+ ]);
}
- $application->makeHidden([
- 'custom_labels',
- 'dockerfile',
- 'docker_compose',
- 'docker_compose_raw',
- 'manual_webhook_secret_bitbucket',
- 'manual_webhook_secret_gitea',
- 'manual_webhook_secret_github',
- 'manual_webhook_secret_gitlab',
- 'private_key_id',
- 'value',
- 'real_value',
- ]);
return serializeApiResponse($application);
}
@@ -70,7 +68,8 @@ class ApplicationsController extends Controller
items: new OA\Items(ref: '#/components/schemas/Application')
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -180,8 +179,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -284,8 +285,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -388,8 +391,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -476,8 +481,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -561,8 +568,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -612,8 +621,10 @@ class ApplicationsController extends Controller
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -1268,7 +1279,8 @@ class ApplicationsController extends Controller
ref: '#/components/schemas/Application'
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1340,7 +1352,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1466,8 +1479,10 @@ class ApplicationsController extends Controller
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
],
- )),
- ]),
+ )
+ ),
+ ]
+ ),
responses: [
new OA\Response(
response: 200,
@@ -1482,7 +1497,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1598,9 +1614,10 @@ class ApplicationsController extends Controller
$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)) {
+ 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) {
@@ -1706,7 +1723,8 @@ class ApplicationsController extends Controller
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1812,7 +1830,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2000,7 +2019,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2181,7 +2201,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2330,7 +2351,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2422,9 +2444,11 @@ class ApplicationsController extends Controller
properties: [
'message' => ['type' => 'string', 'example' => 'Deployment request queued.', 'description' => 'Message.'],
'deployment_uuid' => ['type' => 'string', 'example' => 'doogksw', 'description' => 'UUID of the deployment.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2510,7 +2534,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -2584,7 +2609,8 @@ class ApplicationsController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
@@ -2827,30 +2853,3 @@ 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 9366e6300..917171e5c 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -19,26 +19,23 @@ class DatabasesController extends Controller
{
private function removeSensitiveData($database)
{
- $token = auth()->user()->currentAccessToken();
$database->makeHidden([
'id',
'laravel_through_key',
]);
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($database);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $database->makeHidden([
+ 'internal_db_url',
+ 'external_db_url',
+ 'postgres_password',
+ 'dragonfly_password',
+ 'redis_password',
+ 'mongo_initdb_root_password',
+ 'keydb_password',
+ 'clickhouse_admin_password',
+ ]);
}
- $database->makeHidden([
- 'internal_db_url',
- 'external_db_url',
- 'postgres_password',
- 'dragonfly_password',
- 'redis_password',
- 'mongo_initdb_root_password',
- 'keydb_password',
- 'clickhouse_admin_password',
- ]);
-
return serializeApiResponse($database);
}
@@ -211,8 +208,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 +239,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 +411,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 +441,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 +908,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 +1013,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 +1220,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 +1457,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)) {
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
index 666dc55a5..73b452f86 100644
--- a/app/Http/Controllers/Api/DeployController.php
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -16,15 +16,12 @@ class DeployController extends Controller
{
private function removeSensitiveData($deployment)
{
- $token = auth()->user()->currentAccessToken();
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($deployment);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $deployment->makeHidden([
+ 'logs',
+ ]);
}
- $deployment->makeHidden([
- 'logs',
- ]);
-
return serializeApiResponse($deployment);
}
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index b7190ab1e..a14b0da20 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -11,13 +11,11 @@ class SecurityController extends Controller
{
private function removeSensitiveData($team)
{
- $token = auth()->user()->currentAccessToken();
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($team);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $team->makeHidden([
+ 'private_key',
+ ]);
}
- $team->makeHidden([
- 'private_key',
- ]);
return serializeApiResponse($team);
}
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index 8c13b1a01..f37040bdd 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -19,25 +19,22 @@ class ServersController extends Controller
{
private function removeSensitiveDataFromSettings($settings)
{
- $token = auth()->user()->currentAccessToken();
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($settings);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $settings = $settings->makeHidden([
+ 'sentinel_token',
+ ]);
}
- $settings = $settings->makeHidden([
- 'sentinel_token',
- ]);
return serializeApiResponse($settings);
}
private function removeSensitiveData($server)
{
- $token = auth()->user()->currentAccessToken();
$server->makeHidden([
'id',
]);
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($server);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ // Do nothing
}
return serializeApiResponse($server);
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index bf90322e2..e6b7e9854 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -18,19 +18,16 @@ class ServicesController extends Controller
{
private function removeSensitiveData($service)
{
- $token = auth()->user()->currentAccessToken();
$service->makeHidden([
'id',
]);
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($service);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $service->makeHidden([
+ 'docker_compose_raw',
+ 'docker_compose',
+ ]);
}
- $service->makeHidden([
- 'docker_compose_raw',
- 'docker_compose',
- ]);
-
return serializeApiResponse($service);
}
diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php
index 3f951c6f7..d4b24d8ab 100644
--- a/app/Http/Controllers/Api/TeamController.php
+++ b/app/Http/Controllers/Api/TeamController.php
@@ -10,20 +10,18 @@ class TeamController extends Controller
{
private function removeSensitiveData($team)
{
- $token = auth()->user()->currentAccessToken();
$team->makeHidden([
'custom_server_limit',
'pivot',
]);
- if ($token->can('view:sensitive')) {
- return serializeApiResponse($team);
+ if (request()->attributes->get('can_read_sensitive', false) === false) {
+ $team->makeHidden([
+ 'smtp_username',
+ 'smtp_password',
+ 'resend_api_key',
+ 'telegram_token',
+ ]);
}
- $team->makeHidden([
- 'smtp_username',
- 'smtp_password',
- 'resend_api_key',
- 'telegram_token',
- ]);
return serializeApiResponse($team);
}
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/Http/Kernel.php b/app/Http/Kernel.php
index 5f1731071..a1ce20295 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -69,5 +69,7 @@ class Kernel extends HttpKernel
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
+ 'api.ability' => \App\Http\Middleware\ApiAbility::class,
+ 'api.sensitive' => \App\Http\Middleware\ApiSensitiveData::class,
];
}
diff --git a/app/Http/Middleware/ApiAbility.php b/app/Http/Middleware/ApiAbility.php
new file mode 100644
index 000000000..324eeebaa
--- /dev/null
+++ b/app/Http/Middleware/ApiAbility.php
@@ -0,0 +1,27 @@
+user()->tokenCan('root')) {
+ return $next($request);
+ }
+
+ return parent::handle($request, $next, ...$abilities);
+ } catch (\Illuminate\Auth\AuthenticationException $e) {
+ return response()->json([
+ 'message' => 'Unauthenticated.',
+ ], 401);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'message' => 'Missing required permissions: '.implode(', ', $abilities),
+ ], 403);
+ }
+ }
+}
diff --git a/app/Http/Middleware/ApiSensitiveData.php b/app/Http/Middleware/ApiSensitiveData.php
new file mode 100644
index 000000000..49584ddb3
--- /dev/null
+++ b/app/Http/Middleware/ApiSensitiveData.php
@@ -0,0 +1,21 @@
+user()->currentAccessToken();
+
+ // Allow access to sensitive data if token has root or read:sensitive permission
+ $request->attributes->add([
+ 'can_read_sensitive' => $token->can('root') || $token->can('read:sensitive'),
+ ]);
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/IgnoreReadOnlyApiToken.php b/app/Http/Middleware/IgnoreReadOnlyApiToken.php
deleted file mode 100644
index bd6cd1f8a..000000000
--- a/app/Http/Middleware/IgnoreReadOnlyApiToken.php
+++ /dev/null
@@ -1,28 +0,0 @@
-user()->currentAccessToken();
- if ($token->can('*')) {
- return $next($request);
- }
- if ($token->can('read-only')) {
- return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
- }
-
- return $next($request);
- }
-}
diff --git a/app/Http/Middleware/OnlyRootApiToken.php b/app/Http/Middleware/OnlyRootApiToken.php
deleted file mode 100644
index 8ff1fa0e5..000000000
--- a/app/Http/Middleware/OnlyRootApiToken.php
+++ /dev/null
@@ -1,25 +0,0 @@
-user()->currentAccessToken();
- if ($token->can('*')) {
- return $next($request);
- }
-
- return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
- }
-}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 41909fa30..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 {
diff --git a/app/Jobs/SendMessageToSlackJob.php b/app/Jobs/SendMessageToSlackJob.php
new file mode 100644
index 000000000..470002d23
--- /dev/null
+++ b/app/Jobs/SendMessageToSlackJob.php
@@ -0,0 +1,59 @@
+onQueue('high');
+ }
+
+ public function handle(): void
+ {
+ Http::post($this->webhookUrl, [
+ 'blocks' => [
+ [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'plain_text',
+ 'text' => 'Coolify Notification',
+ ],
+ ],
+ ],
+ 'attachments' => [
+ [
+ 'color' => $this->message->color,
+ 'blocks' => [
+ [
+ 'type' => 'header',
+ 'text' => [
+ 'type' => 'plain_text',
+ 'text' => $this->message->title,
+ ],
+ ],
+ [
+ 'type' => 'section',
+ 'text' => [
+ 'type' => 'mrkdwn',
+ 'text' => $this->message->description,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ }
+}
diff --git a/app/Listeners/ProxyStartedNotification.php b/app/Listeners/ProxyStartedNotification.php
index d0541b162..9045b1e5c 100644
--- a/app/Listeners/ProxyStartedNotification.php
+++ b/app/Listeners/ProxyStartedNotification.php
@@ -14,7 +14,7 @@ class ProxyStartedNotification
public function handle(ProxyStarted $event): void
{
$this->server = data_get($event, 'data');
- $this->server->setupDefault404Redirect();
+ $this->server->setupDefaultRedirect();
$this->server->setupDynamicProxyConfiguration();
$this->server->proxy->force_stop = false;
$this->server->save();
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index c4e4aae14..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'])]
diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php
new file mode 100644
index 000000000..06b7643ea
--- /dev/null
+++ b/app/Livewire/Notifications/Slack.php
@@ -0,0 +1,131 @@
+team = auth()->user()->currentTeam();
+ $this->syncData();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function syncData(bool $toModel = false)
+ {
+ if ($toModel) {
+ $this->validate();
+ $this->team->slack_enabled = $this->slackEnabled;
+ $this->team->slack_webhook_url = $this->slackWebhookUrl;
+ $this->team->slack_notifications_test = $this->slackNotificationsTest;
+ $this->team->slack_notifications_deployments = $this->slackNotificationsDeployments;
+ $this->team->slack_notifications_status_changes = $this->slackNotificationsStatusChanges;
+ $this->team->slack_notifications_database_backups = $this->slackNotificationsDatabaseBackups;
+ $this->team->slack_notifications_scheduled_tasks = $this->slackNotificationsScheduledTasks;
+ $this->team->slack_notifications_server_disk_usage = $this->slackNotificationsServerDiskUsage;
+ $this->team->save();
+ refreshSession();
+ } else {
+ $this->slackEnabled = $this->team->slack_enabled;
+ $this->slackWebhookUrl = $this->team->slack_webhook_url;
+ $this->slackNotificationsTest = $this->team->slack_notifications_test;
+ $this->slackNotificationsDeployments = $this->team->slack_notifications_deployments;
+ $this->slackNotificationsStatusChanges = $this->team->slack_notifications_status_changes;
+ $this->slackNotificationsDatabaseBackups = $this->team->slack_notifications_database_backups;
+ $this->slackNotificationsScheduledTasks = $this->team->slack_notifications_scheduled_tasks;
+ $this->slackNotificationsServerDiskUsage = $this->team->slack_notifications_server_disk_usage;
+ }
+ }
+
+ public function instantSaveSlackEnabled()
+ {
+ try {
+ $this->validate([
+ 'slackWebhookUrl' => 'required',
+ ], [
+ 'slackWebhookUrl.required' => 'Slack Webhook URL is required.',
+ ]);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ $this->slackEnabled = false;
+
+ return handleError($e, $this);
+ }
+ }
+
+ public function instantSave()
+ {
+ try {
+ $this->syncData(true);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function submit()
+ {
+ try {
+ $this->resetErrorBag();
+ $this->syncData(true);
+ $this->saveModel();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function saveModel()
+ {
+ $this->syncData(true);
+ refreshSession();
+ $this->dispatch('success', 'Settings saved.');
+ }
+
+ public function sendTestNotification()
+ {
+ try {
+ $this->team->notify(new Test);
+ $this->dispatch('success', 'Test notification sent.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.notifications.slack');
+ }
+}
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 cce3bdd39..5261a0800 100644
--- a/app/Livewire/Project/Application/Configuration.php
+++ b/app/Livewire/Project/Application/Configuration.php
@@ -16,24 +16,26 @@ class Configuration extends Component
public function mount()
{
- $this->application = Application::query()
- ->whereHas('environment.project', function ($query) {
- $query->where('team_id', currentTeam()->id)
- ->where('uuid', request()->route('project_uuid'));
- })
- ->whereHas('environment', function ($query) {
- $query->where('name', request()->route('environment_name'));
- })
+ $project = currentTeam()
+ ->projects()
+ ->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'))
- ->with(['destination' => function ($query) {
- $query->select('id', 'server_id');
- }])
->firstOrFail();
- if ($this->application->destination && $this->application->destination->server_id) {
+ $this->application = $application;
+ if ($application->destination && $application->destination->server) {
+ $mainServer = $application->destination->server;
$this->servers = Server::ownedByCurrentTeam()
->select('id', 'name')
- ->where('id', '!=', $this->application->destination->server_id)
+ ->where('id', '!=', $mainServer->id)
->get();
} else {
$this->servers = collect();
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
index e71cd9f42..6294d97c6 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
@@ -88,6 +88,9 @@ class Show extends Component
public function lock()
{
$this->env->is_shown_once = true;
+ if ($this->isSharedVariable) {
+ unset($this->env->is_required);
+ }
$this->serialize();
$this->env->save();
$this->checkEnvs();
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/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php
index 0710e37ff..74eac7132 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php
@@ -24,6 +24,14 @@ class Executions extends Component
#[Locked]
public ?string $serverTimezone = null;
+ public $currentPage = 1;
+
+ public $logsPerPage = 100;
+
+ public $selectedExecution = null;
+
+ public $isPollingActive = false;
+
public function getListeners()
{
$teamId = Auth::user()->currentTeam()->id;
@@ -54,16 +62,84 @@ class Executions extends Component
public function refreshExecutions(): void
{
$this->executions = $this->task->executions()->take(20)->get();
+ if ($this->selectedKey) {
+ $this->selectedExecution = $this->task->executions()->find($this->selectedKey);
+ if ($this->selectedExecution && $this->selectedExecution->status !== 'running') {
+ $this->isPollingActive = false;
+ }
+ }
}
public function selectTask($key): void
{
if ($key == $this->selectedKey) {
$this->selectedKey = null;
+ $this->selectedExecution = null;
+ $this->currentPage = 1;
+ $this->isPollingActive = false;
return;
}
$this->selectedKey = $key;
+ $this->selectedExecution = $this->task->executions()->find($key);
+ $this->currentPage = 1;
+
+ // Start polling if task is running
+ if ($this->selectedExecution && $this->selectedExecution->status === 'running') {
+ $this->isPollingActive = true;
+ }
+ }
+
+ public function polling()
+ {
+ if ($this->selectedExecution && $this->isPollingActive) {
+ $this->selectedExecution->refresh();
+ if ($this->selectedExecution->status !== 'running') {
+ $this->isPollingActive = false;
+ }
+ }
+ }
+
+ public function loadMoreLogs()
+ {
+ $this->currentPage++;
+ }
+
+ public function getLogLinesProperty()
+ {
+ if (! $this->selectedExecution) {
+ return collect();
+ }
+
+ if (! $this->selectedExecution->message) {
+ return collect(['Waiting for task output...']);
+ }
+
+ $lines = collect(explode("\n", $this->selectedExecution->message));
+
+ return $lines->take($this->currentPage * $this->logsPerPage);
+ }
+
+ public function downloadLogs(int $executionId)
+ {
+ $execution = $this->executions->firstWhere('id', $executionId);
+ if (! $execution) {
+ return;
+ }
+
+ return response()->streamDownload(function () use ($execution) {
+ echo $execution->message;
+ }, 'task-execution-'.$execution->id.'.log');
+ }
+
+ public function hasMoreLogs()
+ {
+ if (! $this->selectedExecution || ! $this->selectedExecution->message) {
+ return false;
+ }
+ $lines = collect(explode("\n", $this->selectedExecution->message));
+
+ return $lines->count() > ($this->currentPage * $this->logsPerPage);
}
public function formatDateInServerTimezone($date)
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/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php
index fe68a8ba5..72684bdc6 100644
--- a/app/Livewire/Security/ApiTokens.php
+++ b/app/Livewire/Security/ApiTokens.php
@@ -11,13 +11,7 @@ class ApiTokens extends Component
public $tokens = [];
- public bool $viewSensitiveData = false;
-
- public bool $readOnly = true;
-
- public bool $rootAccess = false;
-
- public array $permissions = ['read-only'];
+ public array $permissions = ['read'];
public $isApiEnabled;
@@ -29,51 +23,28 @@ class ApiTokens extends Component
public function mount()
{
$this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
+ $this->getTokens();
+ }
+
+ private function getTokens()
+ {
$this->tokens = auth()->user()->tokens->sortByDesc('created_at');
}
- public function updatedViewSensitiveData()
+ public function updatedPermissions($permissionToUpdate)
{
- if ($this->viewSensitiveData) {
- $this->permissions[] = 'view:sensitive';
- $this->permissions = array_diff($this->permissions, ['*']);
- $this->rootAccess = false;
+ if ($permissionToUpdate == 'root') {
+ $this->permissions = ['root'];
+ } elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions)) {
+ $this->permissions[] = 'read';
+ } elseif ($permissionToUpdate == 'deploy') {
+ $this->permissions = ['deploy'];
} else {
- $this->permissions = array_diff($this->permissions, ['view:sensitive']);
- }
- $this->makeSureOneIsSelected();
- }
-
- public function updatedReadOnly()
- {
- if ($this->readOnly) {
- $this->permissions[] = 'read-only';
- $this->permissions = array_diff($this->permissions, ['*']);
- $this->rootAccess = false;
- } else {
- $this->permissions = array_diff($this->permissions, ['read-only']);
- }
- $this->makeSureOneIsSelected();
- }
-
- public function updatedRootAccess()
- {
- if ($this->rootAccess) {
- $this->permissions = ['*'];
- $this->readOnly = false;
- $this->viewSensitiveData = false;
- } else {
- $this->readOnly = true;
- $this->permissions = ['read-only'];
- }
- }
-
- public function makeSureOneIsSelected()
- {
- if (count($this->permissions) == 0) {
- $this->permissions = ['read-only'];
- $this->readOnly = true;
+ if (count($this->permissions) == 0) {
+ $this->permissions = ['read'];
+ }
}
+ sort($this->permissions);
}
public function addNewToken()
@@ -82,8 +53,8 @@ class ApiTokens extends Component
$this->validate([
'description' => 'required|min:3|max:255',
]);
- $token = auth()->user()->createToken($this->description, $this->permissions);
- $this->tokens = auth()->user()->tokens;
+ $token = auth()->user()->createToken($this->description, array_values($this->permissions));
+ $this->getTokens();
session()->flash('token', $token->plainTextToken);
} catch (\Exception $e) {
return handleError($e, $this);
@@ -92,8 +63,12 @@ class ApiTokens extends Component
public function revoke(int $id)
{
- $token = auth()->user()->tokens()->where('id', $id)->first();
- $token->delete();
- $this->tokens = auth()->user()->tokens;
+ try {
+ $token = auth()->user()->tokens()->where('id', $id)->firstOrFail();
+ $token->delete();
+ $this->getTokens();
+ } catch (\Exception $e) {
+ return handleError($e, $this);
+ }
}
}
diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php
index 0b069ddb9..4e325c1ff 100644
--- a/app/Livewire/Server/Proxy.php
+++ b/app/Livewire/Server/Proxy.php
@@ -15,6 +15,8 @@ class Proxy extends Component
public $proxy_settings = null;
+ public bool $redirect_enabled = true;
+
public ?string $redirect_url = null;
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
@@ -26,6 +28,7 @@ class Proxy extends Component
public function mount()
{
$this->selectedProxy = $this->server->proxyType();
+ $this->redirect_enabled = data_get($this->server, 'proxy.redirect_enabled', true);
$this->redirect_url = data_get($this->server, 'proxy.redirect_url');
}
@@ -38,7 +41,7 @@ class Proxy extends Component
{
$this->server->proxy = null;
$this->server->save();
- $this->dispatch('proxyChanged');
+ $this->dispatch('reloadWindow');
}
public function selectProxy($proxy_type)
@@ -46,7 +49,7 @@ class Proxy extends Component
try {
$this->server->changeProxy($proxy_type, async: false);
$this->selectedProxy = $this->server->proxy->type;
- $this->dispatch('proxyStatusUpdated');
+ $this->dispatch('reloadWindow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -63,13 +66,25 @@ class Proxy extends Component
}
}
+ public function instantSaveRedirect()
+ {
+ try {
+ $this->server->proxy->redirect_enabled = $this->redirect_enabled;
+ $this->server->save();
+ $this->server->setupDefaultRedirect();
+ $this->dispatch('success', 'Proxy configuration saved.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
public function submit()
{
try {
SaveConfiguration::run($this->server, $this->proxy_settings);
$this->server->proxy->redirect_url = $this->redirect_url;
$this->server->save();
- $this->server->setupDefault404Redirect();
+ $this->server->setupDefaultRedirect();
$this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php
index 8fcff85d6..4f9d41092 100644
--- a/app/Livewire/Server/Proxy/Deploy.php
+++ b/app/Livewire/Server/Proxy/Deploy.php
@@ -65,7 +65,7 @@ class Deploy extends Component
public function restart()
{
try {
- $this->stop(forceStop: false);
+ $this->stop();
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -105,6 +105,7 @@ class Deploy extends Component
$startTime = Carbon::now()->getTimestamp();
while ($process->running()) {
+ ray('running');
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopContainer($containerName);
break;
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/SharedVariables/Environment/Show.php b/app/Livewire/SharedVariables/Environment/Show.php
index daf1df212..6a33eb60d 100644
--- a/app/Livewire/SharedVariables/Environment/Show.php
+++ b/app/Livewire/SharedVariables/Environment/Show.php
@@ -16,7 +16,7 @@ class Show extends Component
public array $parameters;
- protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey'];
+ protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey', 'environmentVariableDeleted' => '$refresh'];
public function saveKey($data)
{
diff --git a/app/Livewire/SharedVariables/Project/Show.php b/app/Livewire/SharedVariables/Project/Show.php
index 8d4844442..0171283c4 100644
--- a/app/Livewire/SharedVariables/Project/Show.php
+++ b/app/Livewire/SharedVariables/Project/Show.php
@@ -9,7 +9,7 @@ class Show extends Component
{
public Project $project;
- protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey'];
+ protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey', 'environmentVariableDeleted' => '$refresh'];
public function saveKey($data)
{
diff --git a/app/Livewire/SharedVariables/Team/Index.php b/app/Livewire/SharedVariables/Team/Index.php
index a3085304a..a76ccf58a 100644
--- a/app/Livewire/SharedVariables/Team/Index.php
+++ b/app/Livewire/SharedVariables/Team/Index.php
@@ -9,7 +9,7 @@ class Index extends Component
{
public Team $team;
- protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey'];
+ protected $listeners = ['refreshEnvs' => '$refresh', 'saveKey' => 'saveKey', 'environmentVariableDeleted' => '$refresh'];
public function saveKey($data)
{
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..d1efd3f33 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';
@@ -1320,17 +1321,43 @@ class Application extends BaseModel
if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
}
+ $getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false);
+ $gitVersion = str($getGitVersion)->explode(' ')->last();
- $commands = collect([
- "rm -rf /tmp/{$uuid}",
- "mkdir -p /tmp/{$uuid}",
- "cd /tmp/{$uuid}",
- $cloneCommand,
- 'git sparse-checkout init --cone',
- "git sparse-checkout set {$fileList->implode(' ')}",
- 'git read-tree -mu HEAD',
- "cat .$workdir$composeFile",
- ]);
+ if (version_compare($gitVersion, '2.35.1', '<')) {
+ $fileList = $fileList->map(function ($file) {
+ $parts = explode('/', trim($file, '.'));
+ $paths = collect();
+ $currentPath = '';
+ foreach ($parts as $part) {
+ $currentPath .= ($currentPath ? '/' : '').$part;
+ $paths->push($currentPath);
+ }
+
+ return $paths;
+ })->flatten()->unique()->values();
+ $commands = collect([
+ "rm -rf /tmp/{$uuid}",
+ "mkdir -p /tmp/{$uuid}",
+ "cd /tmp/{$uuid}",
+ $cloneCommand,
+ 'git sparse-checkout init --cone',
+ "git sparse-checkout set {$fileList->implode(' ')}",
+ 'git read-tree -mu HEAD',
+ "cat .$workdir$composeFile",
+ ]);
+ } else {
+ $commands = collect([
+ "rm -rf /tmp/{$uuid}",
+ "mkdir -p /tmp/{$uuid}",
+ "cd /tmp/{$uuid}",
+ $cloneCommand,
+ 'git sparse-checkout init --cone',
+ "git sparse-checkout set {$fileList->implode(' ')}",
+ 'git read-tree -mu HEAD',
+ "cat .$workdir$composeFile",
+ ]);
+ }
try {
$composeFileContent = instant_remote_process($commands, $this->destination->server);
} catch (\Exception $e) {
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 83b91b254..6dfb0a4a1 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;
@@ -104,6 +105,14 @@ class Server extends BaseModel
]);
}
}
+ if (! isset($server->proxy->redirect_enabled)) {
+ $server->proxy->redirect_enabled = true;
+ }
+ });
+ static::retrieved(function ($server) {
+ if (! isset($server->proxy->redirect_enabled)) {
+ $server->proxy->redirect_enabled = true;
+ }
});
static::forceDeleting(function ($server) {
@@ -183,73 +192,80 @@ class Server extends BaseModel
return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
}
- public function setupDefault404Redirect()
+ public function setupDefaultRedirect()
{
+ $banner =
+ "# This file is generated by Coolify, do not edit it manually.\n".
+ "# Disable the default redirect to customize (only if you know what are you doing).\n\n";
$dynamic_conf_path = $this->proxyPath().'/dynamic';
$proxy_type = $this->proxyType();
+ $redirect_enabled = $this->proxy->redirect_enabled ?? true;
$redirect_url = $this->proxy->redirect_url;
- if ($proxy_type === ProxyTypes::TRAEFIK->value) {
- $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
- } elseif ($proxy_type === ProxyTypes::CADDY->value) {
- $default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy";
- }
- if (empty($redirect_url)) {
+ if (isDev()) {
if ($proxy_type === ProxyTypes::CADDY->value) {
- $conf = ':80, :443 {
-respond 404
-}';
- $conf =
- "# This file is automatically generated by Coolify.\n".
- "# Do not edit it manually (only if you know what are you doing).\n\n".
- $conf;
- $base64 = base64_encode($conf);
- instant_remote_process([
- "mkdir -p $dynamic_conf_path",
- "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
- ], $this);
- $this->reloadCaddy();
-
- return;
+ $dynamic_conf_path = '/data/coolify/proxy/caddy/dynamic';
}
- instant_remote_process([
- "mkdir -p $dynamic_conf_path",
- "rm -f $default_redirect_file",
- ], $this);
-
- return;
}
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
- $dynamic_conf = [
- 'http' => [
- 'routers' => [
- 'catchall' => [
- 'entryPoints' => [
- 0 => 'http',
- 1 => 'https',
- ],
- 'service' => 'noop',
- 'rule' => 'HostRegexp(`.+`)',
- 'tls' => [
- 'certResolver' => 'letsencrypt',
- ],
- 'priority' => 1,
- 'middlewares' => [
- 0 => 'redirect-regexp',
+ $default_redirect_file = "$dynamic_conf_path/default_redirect_503.yaml";
+ } elseif ($proxy_type === ProxyTypes::CADDY->value) {
+ $default_redirect_file = "$dynamic_conf_path/default_redirect_503.caddy";
+ }
+
+ instant_remote_process([
+ "mkdir -p $dynamic_conf_path",
+ "rm -f $dynamic_conf_path/default_redirect_404.yaml",
+ "rm -f $dynamic_conf_path/default_redirect_404.caddy",
+ ], $this);
+
+ if ($redirect_enabled === false) {
+ instant_remote_process(["rm -f $default_redirect_file"], $this);
+ } else {
+ if ($proxy_type === ProxyTypes::CADDY->value) {
+ if (filled($redirect_url)) {
+ $conf = ":80, :443 {
+ redir $redirect_url
+}";
+ } else {
+ $conf = ':80, :443 {
+ respond 503
+}';
+ }
+ } elseif ($proxy_type === ProxyTypes::TRAEFIK->value) {
+ $dynamic_conf = [
+ 'http' => [
+ 'routers' => [
+ 'catchall' => [
+ 'entryPoints' => [
+ 0 => 'http',
+ 1 => 'https',
+ ],
+ 'service' => 'noop',
+ 'rule' => 'PathPrefix(`/`)',
+ 'tls' => [
+ 'certResolver' => 'letsencrypt',
+ ],
+ 'priority' => -1000,
],
],
- ],
- 'services' => [
- 'noop' => [
- 'loadBalancer' => [
- 'servers' => [
- 0 => [
- 'url' => '',
- ],
+ 'services' => [
+ 'noop' => [
+ 'loadBalancer' => [
+ 'servers' => [],
],
],
],
],
- 'middlewares' => [
+ ];
+ if (filled($redirect_url)) {
+ $dynamic_conf['http']['routers']['catchall']['middlewares'] = [
+ 0 => 'redirect-regexp',
+ ];
+
+ $dynamic_conf['http']['services']['noop']['loadBalancer']['servers'][0] = [
+ 'url' => '',
+ ];
+ $dynamic_conf['http']['middlewares'] = [
'redirect-regexp' => [
'redirectRegex' => [
'regex' => '(.*)',
@@ -257,32 +273,17 @@ respond 404
'permanent' => false,
],
],
- ],
- ],
- ];
- $conf = Yaml::dump($dynamic_conf, 12, 2);
- $conf =
- "# This file is automatically generated by Coolify.\n".
- "# Do not edit it manually (only if you know what are you doing).\n\n".
- $conf;
-
- $base64 = base64_encode($conf);
- } elseif ($proxy_type === ProxyTypes::CADDY->value) {
- $conf = ":80, :443 {
- redir $redirect_url
-}";
- $conf =
- "# This file is automatically generated by Coolify.\n".
- "# Do not edit it manually (only if you know what are you doing).\n\n".
- $conf;
+ ];
+ }
+ $conf = Yaml::dump($dynamic_conf, 12, 2);
+ }
+ $conf = $banner.$conf;
$base64 = base64_encode($conf);
+ instant_remote_process([
+ "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
+ ], $this);
}
- instant_remote_process([
- "mkdir -p $dynamic_conf_path",
- "echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
- ], $this);
-
if ($proxy_type === 'CADDY') {
$this->reloadCaddy();
}
@@ -610,7 +611,9 @@ $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();
diff --git a/app/Models/Team.php b/app/Models/Team.php
index 6ba044349..ecf662787 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsDiscord;
use App\Notifications\Channels\SendsEmail;
+use App\Notifications\Channels\SendsSlack;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
@@ -70,7 +71,7 @@ use OpenApi\Attributes as OA;
),
]
)]
-class Team extends Model implements SendsDiscord, SendsEmail
+class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack
{
use Notifiable;
@@ -127,11 +128,9 @@ class Team extends Model implements SendsDiscord, SendsEmail
];
}
- public function name(): Attribute
+ public function routeNotificationForSlack()
{
- return new Attribute(
- get: fn () => sanitize_string($this->getRawOriginal('name')),
- );
+ return data_get($this, 'slack_webhook_url', null);
}
public function getRecepients($notification)
diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php
index ce1f99d77..7648aaac2 100644
--- a/app/Notifications/Application/DeploymentFailed.php
+++ b/app/Notifications/Application/DeploymentFailed.php
@@ -6,6 +6,7 @@ use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class DeploymentFailed extends CustomEmailNotification
@@ -128,4 +129,31 @@ class DeploymentFailed extends CustomEmailNotification
],
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ if ($this->preview) {
+ $title = "Pull request #{$this->preview->pull_request_id} deployment failed";
+ $description = "Pull request deployment failed for {$this->application_name}";
+ if ($this->preview->fqdn) {
+ $description .= "\nPreview URL: {$this->preview->fqdn}";
+ }
+ } else {
+ $title = 'Deployment failed';
+ $description = "Deployment failed for {$this->application_name}";
+ if ($this->fqdn) {
+ $description .= "\nApplication URL: {$this->fqdn}";
+ }
+ }
+
+ $description .= "\n\n**Project:** ".data_get($this->application, 'environment.project.name');
+ $description .= "\n**Environment:** {$this->environment_name}";
+ $description .= "\n**Deployment Logs:** {$this->deployment_url}";
+
+ return new SlackMessage(
+ title: $title,
+ description: $description,
+ color: SlackMessage::errorColor()
+ );
+ }
}
diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php
index 391601257..79ae19f66 100644
--- a/app/Notifications/Application/DeploymentSuccess.php
+++ b/app/Notifications/Application/DeploymentSuccess.php
@@ -6,6 +6,7 @@ use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class DeploymentSuccess extends CustomEmailNotification
@@ -143,4 +144,31 @@ class DeploymentSuccess extends CustomEmailNotification
],
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ if ($this->preview) {
+ $title = "Pull request #{$this->preview->pull_request_id} successfully deployed";
+ $description = "New version successfully deployed for {$this->application_name}";
+ if ($this->preview->fqdn) {
+ $description .= "\nPreview URL: {$this->preview->fqdn}";
+ }
+ } else {
+ $title = 'New version successfully deployed';
+ $description = "New version successfully deployed for {$this->application_name}";
+ if ($this->fqdn) {
+ $description .= "\nApplication URL: {$this->fqdn}";
+ }
+ }
+
+ $description .= "\n\n**Project:** ".data_get($this->application, 'environment.project.name');
+ $description .= "\n**Environment:** {$this->environment_name}";
+ $description .= "\n**Deployment Logs:** {$this->deployment_url}";
+
+ return new SlackMessage(
+ title: $title,
+ description: $description,
+ color: SlackMessage::successColor()
+ );
+ }
}
diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php
index c757495cb..2167e9f13 100644
--- a/app/Notifications/Application/StatusChanged.php
+++ b/app/Notifications/Application/StatusChanged.php
@@ -5,6 +5,7 @@ namespace App\Notifications\Application;
use App\Models\Application;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class StatusChanged extends CustomEmailNotification
@@ -75,4 +76,20 @@ class StatusChanged extends CustomEmailNotification
],
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ $title = 'Application stopped';
+ $description = "{$this->resource_name} has been stopped";
+
+ $description .= "\n\n**Project:** ".data_get($this->resource, 'environment.project.name');
+ $description .= "\n**Environment:** {$this->environment_name}";
+ $description .= "\n**Application URL:** {$this->resource_url}";
+
+ return new SlackMessage(
+ title: $title,
+ description: $description,
+ color: SlackMessage::errorColor()
+ );
+ }
}
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/app/Notifications/Channels/SendsSlack.php b/app/Notifications/Channels/SendsSlack.php
new file mode 100644
index 000000000..ab2dd6f11
--- /dev/null
+++ b/app/Notifications/Channels/SendsSlack.php
@@ -0,0 +1,8 @@
+toSlack();
+ $webhookUrl = $notifiable->routeNotificationForSlack();
+ if (! $webhookUrl) {
+ return;
+ }
+ SendMessageToSlackJob::dispatch($message, $webhookUrl);
+ }
+}
diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php
index eb709535f..f9becd0e8 100644
--- a/app/Notifications/Container/ContainerRestarted.php
+++ b/app/Notifications/Container/ContainerRestarted.php
@@ -5,6 +5,7 @@ namespace App\Notifications\Container;
use App\Models\Server;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class ContainerRestarted extends CustomEmailNotification
@@ -66,4 +67,20 @@ class ContainerRestarted extends CustomEmailNotification
return $payload;
}
+
+ public function toSlack(): SlackMessage
+ {
+ $title = 'Resource restarted';
+ $description = "A resource ({$this->name}) has been restarted automatically on {$this->server->name}";
+
+ if ($this->url) {
+ $description .= "\n**Resource URL:** {$this->url}";
+ }
+
+ return new SlackMessage(
+ title: $title,
+ description: $description,
+ color: SlackMessage::warningColor()
+ );
+ }
}
diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php
index a73e984a0..eae2cf552 100644
--- a/app/Notifications/Container/ContainerStopped.php
+++ b/app/Notifications/Container/ContainerStopped.php
@@ -5,6 +5,7 @@ namespace App\Notifications\Container;
use App\Models\Server;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class ContainerStopped extends CustomEmailNotification
@@ -66,4 +67,20 @@ class ContainerStopped extends CustomEmailNotification
return $payload;
}
+
+ public function toSlack(): SlackMessage
+ {
+ $title = 'Resource stopped';
+ $description = "A resource ({$this->name}) has been stopped unexpectedly on {$this->server->name}";
+
+ if ($this->url) {
+ $description .= "\n**Resource URL:** {$this->url}";
+ }
+
+ return new SlackMessage(
+ title: $title,
+ description: $description,
+ color: SlackMessage::errorColor()
+ );
+ }
}
diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php
index beeea0804..2056255d6 100644
--- a/app/Notifications/Database/BackupFailed.php
+++ b/app/Notifications/Database/BackupFailed.php
@@ -5,6 +5,7 @@ namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class BackupFailed extends CustomEmailNotification
@@ -62,4 +63,19 @@ class BackupFailed extends CustomEmailNotification
'message' => $message,
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ $title = 'Database backup failed';
+ $description = "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.";
+
+ $description .= "\n\n**Frequency:** {$this->frequency}";
+ $description .= "\n\n**Error Output:**\n{$this->output}";
+
+ return new SlackMessage(
+ title: $title,
+ description: $description,
+ color: SlackMessage::errorColor()
+ );
+ }
}
diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php
index d8bab069b..71866f9fc 100644
--- a/app/Notifications/Database/BackupSuccess.php
+++ b/app/Notifications/Database/BackupSuccess.php
@@ -5,6 +5,7 @@ namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class BackupSuccess extends CustomEmailNotification
@@ -60,4 +61,18 @@ class BackupSuccess extends CustomEmailNotification
'message' => $message,
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ $title = 'Database backup successful';
+ $description = "Database backup for {$this->name} (db:{$this->database_name}) was successful.";
+
+ $description .= "\n\n**Frequency:** {$this->frequency}";
+
+ return new SlackMessage(
+ title: $title,
+ description: $description,
+ color: SlackMessage::successColor()
+ );
+ }
}
diff --git a/app/Notifications/Dto/SlackMessage.php b/app/Notifications/Dto/SlackMessage.php
new file mode 100644
index 000000000..879bf6547
--- /dev/null
+++ b/app/Notifications/Dto/SlackMessage.php
@@ -0,0 +1,32 @@
+ $this->message,
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ return new SlackMessage(
+ title: 'Coolify: General Notification',
+ description: $this->message,
+ color: SlackMessage::infoColor(),
+ );
+ }
}
diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php
index 701f61277..753da7ff0 100644
--- a/app/Notifications/ScheduledTask/TaskFailed.php
+++ b/app/Notifications/ScheduledTask/TaskFailed.php
@@ -5,6 +5,7 @@ namespace App\Notifications\ScheduledTask;
use App\Models\ScheduledTask;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class TaskFailed extends CustomEmailNotification
@@ -68,4 +69,24 @@ class TaskFailed extends CustomEmailNotification
'message' => $message,
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ $title = 'Scheduled task failed';
+ $description = "Scheduled task ({$this->task->name}) failed.";
+
+ if ($this->output) {
+ $description .= "\n\n**Error Output:**\n{$this->output}";
+ }
+
+ if ($this->url) {
+ $description .= "\n\n**Task URL:** {$this->url}";
+ }
+
+ return new SlackMessage(
+ title: $title,
+ description: $description,
+ color: SlackMessage::errorColor()
+ );
+ }
}
diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php
index 2d007a262..46b730c7b 100644
--- a/app/Notifications/Server/DockerCleanup.php
+++ b/app/Notifications/Server/DockerCleanup.php
@@ -7,6 +7,7 @@ use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
class DockerCleanup extends CustomEmailNotification
{
@@ -21,7 +22,7 @@ class DockerCleanup extends CustomEmailNotification
// $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
-
+ $isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
@@ -31,6 +32,9 @@ class DockerCleanup extends CustomEmailNotification
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
+ if ($isSlackEnabled) {
+ $channels[] = SlackChannel::class;
+ }
return $channels;
}
@@ -62,4 +66,13 @@ class DockerCleanup extends CustomEmailNotification
'message' => "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}",
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ return new SlackMessage(
+ title: 'Server cleanup job done',
+ description: "Server '{$this->server->name}' cleanup job done!\n\n{$this->message}",
+ color: SlackMessage::successColor()
+ );
+ }
}
diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php
index eabf8b334..143917dde 100644
--- a/app/Notifications/Server/ForceDisabled.php
+++ b/app/Notifications/Server/ForceDisabled.php
@@ -5,9 +5,11 @@ namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
+use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class ForceDisabled extends CustomEmailNotification
@@ -23,7 +25,7 @@ class ForceDisabled extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
-
+ $isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
@@ -33,6 +35,9 @@ class ForceDisabled extends CustomEmailNotification
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
+ if ($isSlackEnabled) {
+ $channels[] = SlackChannel::class;
+ }
return $channels;
}
@@ -67,4 +72,18 @@ class ForceDisabled extends CustomEmailNotification
'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).",
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ $title = 'Server disabled';
+ $description = "Server ({$this->server->name}) disabled because it is not paid!\n";
+ $description .= "All automations and integrations are stopped.\n\n";
+ $description .= 'Please update your subscription to enable the server again: https://app.coolify.io/subscriptions';
+
+ return new SlackMessage(
+ title: $title,
+ description: $description,
+ color: SlackMessage::errorColor()
+ );
+ }
}
diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php
index 0c21ed6b8..3b83882d8 100644
--- a/app/Notifications/Server/ForceEnabled.php
+++ b/app/Notifications/Server/ForceEnabled.php
@@ -5,9 +5,11 @@ namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
+use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class ForceEnabled extends CustomEmailNotification
@@ -23,7 +25,7 @@ class ForceEnabled extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
-
+ $isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
@@ -33,6 +35,9 @@ class ForceEnabled extends CustomEmailNotification
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
+ if ($isSlackEnabled) {
+ $channels[] = SlackChannel::class;
+ }
return $channels;
}
@@ -63,4 +68,13 @@ class ForceEnabled extends CustomEmailNotification
'message' => "Coolify: Server ({$this->server->name}) enabled again!",
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ return new SlackMessage(
+ title: 'Server enabled',
+ description: "Server '{$this->server->name}' enabled again!",
+ color: SlackMessage::successColor()
+ );
+ }
}
diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php
index 7cec2e892..baff49508 100644
--- a/app/Notifications/Server/HighDiskUsage.php
+++ b/app/Notifications/Server/HighDiskUsage.php
@@ -5,6 +5,7 @@ namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class HighDiskUsage extends CustomEmailNotification
@@ -55,4 +56,22 @@ class HighDiskUsage extends CustomEmailNotification
'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->server_disk_usage_notification_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.",
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ $description = "Server '{$this->server->name}' high disk usage detected!\n";
+ $description .= "Disk usage: {$this->disk_usage}%\n";
+ $description .= "Threshold: {$this->server_disk_usage_notification_threshold}%\n\n";
+ $description .= "Please cleanup your disk to prevent data-loss.\n";
+ $description .= "Tips for cleanup: https://coolify.io/docs/knowledge-base/server/automated-cleanup\n";
+ $description .= "Change settings:\n";
+ $description .= '- Threshold: '.base_url().'/server/'.$this->server->uuid."#advanced\n";
+ $description .= '- Notifications: '.base_url().'/notifications/discord';
+
+ return new SlackMessage(
+ title: 'High disk usage detected',
+ description: $description,
+ color: SlackMessage::errorColor()
+ );
+ }
}
diff --git a/app/Notifications/Server/Reachable.php b/app/Notifications/Server/Reachable.php
index 44189c3b5..62ece34e8 100644
--- a/app/Notifications/Server/Reachable.php
+++ b/app/Notifications/Server/Reachable.php
@@ -5,9 +5,11 @@ namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
+use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class Reachable extends CustomEmailNotification
@@ -32,7 +34,7 @@ class Reachable extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
-
+ $isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
@@ -42,6 +44,9 @@ class Reachable extends CustomEmailNotification
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
+ if ($isSlackEnabled) {
+ $channels[] = SlackChannel::class;
+ }
return $channels;
}
@@ -72,4 +77,13 @@ class Reachable extends CustomEmailNotification
'message' => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!",
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ return new SlackMessage(
+ title: 'Server revived',
+ description: "Server '{$this->server->name}' revived.\nAll automations & integrations are turned on again!",
+ color: SlackMessage::successColor()
+ );
+ }
}
diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php
index 6fb792bdc..2a90d7552 100644
--- a/app/Notifications/Server/Unreachable.php
+++ b/app/Notifications/Server/Unreachable.php
@@ -5,9 +5,11 @@ namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
+use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class Unreachable extends CustomEmailNotification
@@ -32,6 +34,7 @@ class Unreachable extends CustomEmailNotification
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
+ $isSlackEnabled = data_get($notifiable, 'slack_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
@@ -42,6 +45,9 @@ class Unreachable extends CustomEmailNotification
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
+ if ($isSlackEnabled) {
+ $channels[] = SlackChannel::class;
+ }
return $channels;
}
@@ -76,4 +82,17 @@ class Unreachable extends CustomEmailNotification
'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.",
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ $description = "Your server '{$this->server->name}' is unreachable.\n";
+ $description .= "All automations & integrations are turned off!\n\n";
+ $description .= '*IMPORTANT:* We automatically try to revive your server and turn on all automations & integrations.';
+
+ return new SlackMessage(
+ title: 'Server unreachable',
+ description: $description,
+ color: SlackMessage::errorColor()
+ );
+ }
}
diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php
index 64f9bb0a5..03f6c3296 100644
--- a/app/Notifications/Test.php
+++ b/app/Notifications/Test.php
@@ -3,6 +3,7 @@
namespace App\Notifications;
use App\Notifications\Dto\DiscordMessage;
+use App\Notifications\Dto\SlackMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -67,4 +68,12 @@ class Test extends Notification implements ShouldQueue
],
];
}
+
+ public function toSlack(): SlackMessage
+ {
+ return new SlackMessage(
+ title: 'Test Slack Notification',
+ description: 'This is a test Slack notification from Coolify.'
+ );
+ }
}
diff --git a/app/View/Components/Forms/Checkbox.php b/app/View/Components/Forms/Checkbox.php
index 0bdebe7e4..e46598e8e 100644
--- a/app/View/Components/Forms/Checkbox.php
+++ b/app/View/Components/Forms/Checkbox.php
@@ -15,6 +15,7 @@ class Checkbox extends Component
public ?string $id = null,
public ?string $name = null,
public ?string $value = null,
+ public ?string $domValue = null,
public ?string $label = null,
public ?string $helper = null,
public string|bool|null $checked = false,
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/proxy.php b/bootstrap/helpers/proxy.php
index a8ef0fe5a..463e89b6f 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -173,13 +173,12 @@ function generate_default_proxy_configuration(Server $server)
],
'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro',
- "{$proxy_path}:/traefik",
+
],
'command' => [
'--ping=true',
'--ping.entrypoint=http',
'--api.dashboard=true',
- '--api.insecure=false',
'--entrypoints.http.address=:80',
'--entrypoints.https.address=:443',
'--entrypoints.http.http.encodequerysemicolons=true',
@@ -187,21 +186,26 @@ function generate_default_proxy_configuration(Server $server)
'--entrypoints.https.http.encodequerysemicolons=true',
'--entryPoints.https.http2.maxConcurrentStreams=50',
'--entrypoints.https.http3',
- '--providers.docker.exposedbydefault=false',
'--providers.file.directory=/traefik/dynamic/',
+ '--providers.docker.exposedbydefault=false',
'--providers.file.watch=true',
'--certificatesresolvers.letsencrypt.acme.httpchallenge=true',
- '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json',
'--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http',
+ '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json',
],
'labels' => $labels,
],
],
];
if (isDev()) {
- // $config['services']['traefik']['command'][] = "--log.level=debug";
+ $config['services']['traefik']['command'][] = '--api.insecure=true';
+ $config['services']['traefik']['command'][] = '--log.level=debug';
$config['services']['traefik']['command'][] = '--accesslog.filepath=/traefik/access.log';
$config['services']['traefik']['command'][] = '--accesslog.bufferingsize=100';
+ $config['services']['traefik']['volumes'][] = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/:/traefik';
+ } else {
+ $config['services']['traefik']['command'][] = '--api.insecure=false';
+ $config['services']['traefik']['volumes'][] = "{$proxy_path}:/traefik";
}
if ($server->isSwarm()) {
data_forget($config, 'services.traefik.container_name');
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index d64b5ab6e..835ead24c 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -27,6 +27,7 @@ use App\Models\Team;
use App\Models\User;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
+use App\Notifications\Channels\SlackChannel;
use App\Notifications\Channels\TelegramChannel;
use App\Notifications\Internal\GeneralNotification;
use Carbon\CarbonImmutable;
@@ -90,8 +91,11 @@ function metrics_dir(): string
return base_configuration_dir().'/metrics';
}
-function sanitize_string(string $input): string
+function sanitize_string(?string $input = null): ?string
{
+ if (is_null($input)) {
+ return null;
+ }
// Remove any HTML/PHP tags
$sanitized = strip_tags($input);
@@ -466,11 +470,13 @@ function setNotificationChannels($notifiable, $event)
{
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
+ $isSlackEnabled = data_get($notifiable, 'slack_enabled');
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, "smtp_notifications_$event");
$isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event");
$isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event");
+ $isSubscribedToSlackEvent = data_get($notifiable, "slack_notifications_$event");
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class;
@@ -481,6 +487,9 @@ function setNotificationChannels($notifiable, $event)
if ($isTelegramEnabled && $isSubscribedToTelegramEvent) {
$channels[] = TelegramChannel::class;
}
+ if ($isSlackEnabled && $isSubscribedToSlackEvent) {
+ $channels[] = SlackChannel::class;
+ }
return $channels;
}
diff --git a/config/constants.php b/config/constants.php
index c947635be..42abd09cb 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.377',
'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_10_30_074601_rename_token_permissions.php b/database/migrations/2024_10_30_074601_rename_token_permissions.php
new file mode 100644
index 000000000..2ca98d090
--- /dev/null
+++ b/database/migrations/2024_10_30_074601_rename_token_permissions.php
@@ -0,0 +1,60 @@
+abilities)) {
+ $abilities->push('root');
+ }
+ if (in_array('read-only', $token->abilities)) {
+ $abilities->push('read');
+ }
+ if (in_array('view:sensitive', $token->abilities)) {
+ $abilities->push('read', 'read:sensitive');
+ }
+ $token->abilities = $abilities->unique()->values()->all();
+ $token->save();
+ }
+ } catch (\Exception $e) {
+ \Log::error('Error renaming token permissions: '.$e->getMessage());
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ try {
+ $tokens = PersonalAccessToken::all();
+ foreach ($tokens as $token) {
+ $abilities = collect();
+ if (in_array('write', $token->abilities)) {
+ $abilities->push('*');
+ } else {
+ if (in_array('read', $token->abilities)) {
+ $abilities->push('read-only');
+ }
+ if (in_array('read:sensitive', $token->abilities)) {
+ $abilities->push('view:sensitive');
+ }
+ }
+ $token->abilities = $abilities->unique()->values()->all();
+ $token->save();
+ }
+ } catch (\Exception $e) {
+ \Log::error('Error renaming token permissions: '.$e->getMessage());
+ }
+ }
+};
diff --git a/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php b/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php
new file mode 100644
index 000000000..a6457269a
--- /dev/null
+++ b/database/migrations/2024_11_12_213200_add_slack_notifications_to_teams_table.php
@@ -0,0 +1,38 @@
+boolean('slack_enabled')->default(false);
+ $table->string('slack_webhook_url')->nullable();
+ $table->boolean('slack_notifications_test')->default(true);
+ $table->boolean('slack_notifications_deployments')->default(true);
+ $table->boolean('slack_notifications_status_changes')->default(true);
+ $table->boolean('slack_notifications_database_backups')->default(true);
+ $table->boolean('slack_notifications_scheduled_tasks')->default(true);
+ $table->boolean('slack_notifications_server_disk_usage')->default(true);
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('teams', function (Blueprint $table) {
+ $table->dropColumn([
+ 'slack_enabled',
+ 'slack_webhook_url',
+ 'slack_notifications_test',
+ 'slack_notifications_deployments',
+ 'slack_notifications_status_changes',
+ 'slack_notifications_database_backups',
+ 'slack_notifications_scheduled_tasks',
+ 'slack_notifications_server_disk_usage',
+ ]);
+ });
+ }
+};
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 @@