diff --git a/README.md b/README.md
index 0a3ce0132..dac48d127 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@ Special thanks to our biggest sponsors!
### Special Sponsors
-
+
* [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry.
* [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions.
@@ -50,6 +50,7 @@ Special thanks to our biggest sponsors!
* [QuantCDN](https://www.quantcdn.io/?ref=coolify.io) - A content delivery network (CDN) optimizing website performance through global content distribution.
* [Arcjet](https://arcjet.com/?ref=coolify.io) - A cloud-based platform providing real-time protection against API abuse and bot attacks.
* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
+* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies.
* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
@@ -90,7 +91,6 @@ Special thanks to our biggest sponsors!
-
@@ -147,10 +147,10 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
# Core Maintainers
-| Andras Bacsai | Peak |
+| Andras Bacsai | 🏔️ Peak |
|------------|------------|
-|
|
|
-|
|
|
+|
|
|
+|
|
|
# Repo Activity
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index 8aae910a9..706356930 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -179,7 +179,7 @@ class GetContainersStatus
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
- // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
+ // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
}
}
} else {
diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php
index 5f9a1e357..75b8501f3 100644
--- a/app/Actions/Server/ServerCheck.php
+++ b/app/Actions/Server/ServerCheck.php
@@ -51,7 +51,6 @@ class ServerCheck
$containerReplicates = null;
$this->isSentinel = true;
-
} else {
['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
// ServerStorageCheckJob::dispatch($this->server);
@@ -148,7 +147,6 @@ class ServerCheck
} else {
$labels = Arr::undot(data_get($container, 'Config.Labels'));
}
-
}
$managed = data_get($labels, 'coolify.managed');
if (! $managed) {
@@ -259,7 +257,7 @@ class ServerCheck
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
- // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
+ // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for database", $this->server));
}
}
}
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index c69640970..614208c78 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -1591,16 +1591,32 @@ class ApplicationsController extends Controller
}
$domains = $request->domains;
if ($request->has('domains') && $server->isProxyShouldRun()) {
- $errors = [];
+ $uuid = $request->uuid;
$fqdn = $request->domains;
$fqdn = str($fqdn)->replaceEnd(',', '')->trim();
$fqdn = str($fqdn)->replaceStart(',', '')->trim();
- $application->fqdn = $fqdn;
- if (! $application->settings->is_container_label_readonly_enabled) {
- $customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
- $application->custom_labels = base64_encode($customLabels);
+ $errors = [];
+ $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
+ $domain = trim($domain);
+ if (filter_var($domain, FILTER_VALIDATE_URL) === false || !preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}/', $domain)) {
+ $errors[] = 'Invalid domain: '.$domain;
+ }
+ return $domain;
+ });
+ if (count($errors) > 0) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+ if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'domains' => 'One of the domain is already used.',
+ ],
+ ], 422);
}
- $request->offsetUnset('domains');
}
$dockerComposeDomainsJson = collect();
@@ -2811,3 +2827,30 @@ class ApplicationsController extends Controller
}
}
}
+
+ $fqdn = str($fqdn)->replaceStart(',', '')->trim();
+ $errors = [];
+ $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) {
+ if (filter_var($domain, FILTER_VALIDATE_URL) === false) {
+ $errors[] = 'Invalid domain: ' . $domain;
+ }
+
+ return str($domain)->trim()->lower();
+ });
+ if (count($errors) > 0) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => $errors,
+ ], 422);
+ }
+ if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) {
+ return response()->json([
+ 'message' => 'Validation failed.',
+ 'errors' => [
+ 'domains' => 'One of the domain is already used.',
+ ],
+ ], 422);
+ }
+ }
+ }
+}
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index ce658d2a2..9366e6300 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -1557,7 +1557,8 @@ class DatabasesController extends Controller
]
)
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1632,9 +1633,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database starting request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1708,9 +1711,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
@@ -1784,9 +1789,11 @@ class DatabasesController extends Controller
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'],
- ])
+ ]
+ )
),
- ]),
+ ]
+ ),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 6a66bb56d..41909fa30 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -2400,7 +2400,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if (! $this->only_this_server) {
$this->deploy_to_additional_destinations();
}
- $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
+ //$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
}
}
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 89674b255..ee702202f 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -306,7 +306,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
- $this->team?->notify(new BackupSuccess($this->backup, $this->database, $database));
+ //$this->team?->notify(new BackupSuccess($this->backup, $this->database, $database));
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index b6c799c4e..eadabba7c 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -172,13 +172,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function getProxyType()
{
- // Set Default Proxy Type
$this->selectProxy(ProxyTypes::TRAEFIK->value);
- // $proxyTypeSet = $this->createdServer->proxy->type;
- // if (!$proxyTypeSet) {
- // $this->currentState = 'select-proxy';
- // return;
- // }
$this->getProjects();
}
@@ -189,7 +183,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return;
}
- $this->createdPrivateKey = PrivateKey::find($this->selectedExistingPrivateKey);
+ $this->createdPrivateKey = PrivateKey::where('team_id', currentTeam()->id)->where('id', $this->selectedExistingPrivateKey)->first();
$this->privateKey = $this->createdPrivateKey->private_key;
$this->currentState = 'create-server';
}
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index fcedf1305..c4e4aae14 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -73,8 +73,8 @@ class Email extends Component
#[Validate(['nullable', 'string'])]
public ?string $resendApiKey = null;
- #[Validate(['required', 'email'])]
- public string $testEmailAddress = '';
+ #[Validate(['nullable', 'email'])]
+ public ?string $testEmailAddress = null;
public function mount()
{
diff --git a/app/Livewire/Project/Application/Configuration.php b/app/Livewire/Project/Application/Configuration.php
index d4ec8f581..cce3bdd39 100644
--- a/app/Livewire/Project/Application/Configuration.php
+++ b/app/Livewire/Project/Application/Configuration.php
@@ -16,24 +16,28 @@ class Configuration extends Component
public function mount()
{
- $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
- if (! $project) {
- return redirect()->route('dashboard');
+ $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'));
+ })
+ ->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->servers = Server::ownedByCurrentTeam()
+ ->select('id', 'name')
+ ->where('id', '!=', $this->application->destination->server_id)
+ ->get();
+ } else {
+ $this->servers = collect();
}
- $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
- if (! $environment) {
- return redirect()->route('dashboard');
- }
- $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
- if (! $application) {
- return redirect()->route('dashboard');
- }
- $this->application = $application;
- $mainServer = $this->application->destination->server;
- $servers = Server::ownedByCurrentTeam()->get();
- $this->servers = $servers->filter(function ($server) use ($mainServer) {
- return $server->id != $mainServer->id;
- });
}
public function render()
diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php
index 17201ea6e..79801987b 100644
--- a/app/Models/BaseModel.php
+++ b/app/Models/BaseModel.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Visus\Cuid2\Cuid2;
@@ -18,4 +19,18 @@ abstract class BaseModel extends Model
}
});
}
+
+ public function name(): Attribute
+ {
+ return new Attribute(
+ get: fn () => sanitize_string($this->getRawOriginal('name')),
+ );
+ }
+
+ public function image(): Attribute
+ {
+ return new Attribute(
+ get: fn () => sanitize_string($this->getRawOriginal('image')),
+ );
+ }
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 27c2b9b99..83b91b254 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -1039,7 +1039,7 @@ $schema://$host {
$this->unreachable_notification_sent = false;
$this->save();
$this->refresh();
- $this->team->notify(new Reachable($this));
+ // $this->team->notify(new Reachable($this));
}
public function sendUnreachableNotification()
@@ -1047,7 +1047,7 @@ $schema://$host {
$this->unreachable_notification_sent = true;
$this->save();
$this->refresh();
- $this->team->notify(new Unreachable($this));
+ // $this->team->notify(new Unreachable($this));
}
public function validateConnection(bool $justCheckingNewKey = false)
diff --git a/app/Models/Team.php b/app/Models/Team.php
index e21aa3a25..6ba044349 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -127,6 +127,13 @@ class Team extends Model implements SendsDiscord, SendsEmail
];
}
+ public function name(): Attribute
+ {
+ return new Attribute(
+ get: fn () => sanitize_string($this->getRawOriginal('name')),
+ );
+ }
+
public function getRecepients($notification)
{
$recipients = data_get($notification, 'emails', null);
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 1a943cd73..d64b5ab6e 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -90,8 +90,28 @@ function metrics_dir(): string
return base_configuration_dir().'/metrics';
}
+function sanitize_string(string $input): string
+{
+ // Remove any HTML/PHP tags
+ $sanitized = strip_tags($input);
+
+ // Convert special characters to HTML entities
+ $sanitized = htmlspecialchars($sanitized, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+
+ // Remove any control characters
+ $sanitized = preg_replace('/[\x00-\x1F\x7F]/u', '', $sanitized);
+
+ // Trim whitespace
+ $sanitized = trim($sanitized);
+
+ return $sanitized;
+}
+
function generate_readme_file(string $name, string $updated_at): string
{
+ $name = sanitize_string($name);
+ $updated_at = sanitize_string($updated_at);
+
return "Resource name: $name\nLatest Deployment Date: $updated_at";
}
diff --git a/config/constants.php b/config/constants.php
index 9ad6dda71..c947635be 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,7 +2,7 @@
return [
'coolify' => [
- 'version' => '4.0.0-beta.373',
+ 'version' => '4.0.0-beta.374',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
diff --git a/public/svgs/overseerr.svg b/public/svgs/overseerr.svg
new file mode 100644
index 000000000..8116787c2
--- /dev/null
+++ b/public/svgs/overseerr.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/public/svgs/prowlarr.svg b/public/svgs/prowlarr.svg
new file mode 100644
index 000000000..a1a5a8ce3
--- /dev/null
+++ b/public/svgs/prowlarr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/pterodactyl.png b/public/svgs/pterodactyl.png
new file mode 100644
index 000000000..a5addb87c
Binary files /dev/null and b/public/svgs/pterodactyl.png differ
diff --git a/public/svgs/radarr.svg b/public/svgs/radarr.svg
new file mode 100644
index 000000000..93a4c9232
--- /dev/null
+++ b/public/svgs/radarr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/sonarr.svg b/public/svgs/sonarr.svg
new file mode 100644
index 000000000..91c04e289
--- /dev/null
+++ b/public/svgs/sonarr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/css/app.css b/resources/css/app.css
index 1195a3058..32d476c1a 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -6,7 +6,7 @@
html,
body {
- @apply h-full bg-neutral-50 text-neutral-800 dark:bg-base dark:text-neutral-400;
+ @apply h-full bg-neutral-50 text-neutral-800 dark:bg-base dark:text-neutral-400 w-full;
}
body {
@@ -322,4 +322,4 @@ section {
.dz-button {
@apply w-full p-4 py-10 my-4 font-bold bg-white border dark:border-coolgray-400 dark:text-white dark:bg-transparent hover:dark:bg-coolgray-400;
-}
+}
\ No newline at end of file
diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php
index f635a6787..7d343c645 100644
--- a/resources/views/components/navbar.blade.php
+++ b/resources/views/components/navbar.blade.php
@@ -7,8 +7,13 @@
}
window.location.reload();
},
+ setZoom(zoom) {
+ localStorage.setItem('zoom', zoom);
+ window.location.reload();
+ },
init() {
this.full = localStorage.getItem('pageWidth');
+ this.zoom = localStorage.getItem('zoom');
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
const userSettings = localStorage.getItem('theme');
if (userSettings !== 'system') {
@@ -21,6 +26,7 @@
}
});
this.queryTheme();
+ this.checkZoom();
},
setTheme(type) {
this.theme = type;
@@ -44,6 +50,30 @@
this.theme = 'system';
document.documentElement.classList.remove('dark');
}
+ },
+ checkZoom() {
+ if (this.zoom === null) {
+ this.setZoom(100);
+ }
+ if (this.zoom === '90') {
+ const style = document.createElement('style');
+ style.textContent = `
+ html {
+ font-size: 93.75%;
+ }
+
+ :root {
+ --vh: 1vh;
+ }
+
+ @media (min-width: 1024px) {
+ html {
+ font-size: 87.5%;
+ }
+ }
+ `;
+ document.head.appendChild(style);
+ }
}
}">
@@ -69,7 +99,7 @@
-
Color
+
Color
@@ -78,6 +108,9 @@
x-show="full === 'full'">Center
+
Zoom
+
+
@@ -163,8 +196,8 @@
class="{{ request()->is('storages*') ? 'menu-item-active menu-item' : 'menu-item' }}"
href="{{ route('storage.index') }}">