Compare commits

...

28 Commits

Author SHA1 Message Date
Andras Bacsai
0c1e7c499e Merge pull request #1213 from coollabsio/next
v4.0.0-beta.30
2023-09-09 13:23:59 +02:00
Andras Bacsai
32fead5753 version++ 2023-09-09 13:23:03 +02:00
Andras Bacsai
e5e9faba35 fix: delete database related things when delete database 2023-09-09 13:18:49 +02:00
Andras Bacsai
2852630d6c Update DatabaseBackupJob.php 2023-09-09 10:34:10 +02:00
Andras Bacsai
a4cc406114 Merge pull request #1212 from coollabsio/next
v4.0.0-beta.29
2023-09-08 18:52:41 +02:00
Andras Bacsai
53b15a5762 update sentry dsn 2023-09-08 18:40:32 +02:00
Andras Bacsai
929a4e6474 fix: coolify already exists should not throw error 2023-09-08 18:40:25 +02:00
Andras Bacsai
45b597bbab feat: cache team settings 2023-09-08 18:33:26 +02:00
Andras Bacsai
0d1a2aa5d1 update testemail command 2023-09-08 17:51:19 +02:00
Andras Bacsai
b82353d5e2 update testemail command 2023-09-08 17:42:08 +02:00
Andras Bacsai
b17c09f7a7 update testemail command 2023-09-08 17:31:02 +02:00
Andras Bacsai
f6c3fe7888 fix: test email on for admins or custom smtp 2023-09-08 17:26:59 +02:00
Andras Bacsai
2e855e030f fix: ui 2023-09-08 16:59:49 +02:00
Andras Bacsai
49f86621f4 fix: instance email settings 2023-09-08 16:56:14 +02:00
Andras Bacsai
03d9f93397 fix: retry notifications 2023-09-08 16:53:19 +02:00
Andras Bacsai
c472042a94 fix: ui 2023-09-08 16:46:53 +02:00
Andras Bacsai
9f4356f67d version++ 2023-09-08 16:28:56 +02:00
Andras Bacsai
a50317cc76 Merge pull request #1209 from coollabsio/next
v4.0.0-beta.28
2023-09-08 16:28:04 +02:00
Andras Bacsai
8afa98a1ca fix: sentry 4451028626 2023-09-08 16:19:45 +02:00
Andras Bacsai
f6737f21dd feat: developer view for env variables 2023-09-08 16:16:59 +02:00
Andras Bacsai
e4a51cc116 fix: sentry 4459819517 2023-09-08 16:16:28 +02:00
Andras Bacsai
acd78ae196 feat: Telegram topics separation 2023-09-08 14:15:28 +02:00
Andras Bacsai
953bcfb5bb fix: db backup job 2023-09-08 12:23:46 +02:00
Andras Bacsai
dacfab8b29 Update BUG_REPORT.yml 2023-09-08 12:08:16 +02:00
Andras Bacsai
48b3e99939 Merge pull request #1203 from coollabsio/next
v4.0.0-beta.27
2023-09-08 12:06:34 +02:00
Andras Bacsai
41ad67c7c9 updates 2023-09-08 12:05:15 +02:00
Andras Bacsai
b49725cb1c fix: bug 2023-09-08 09:37:58 +02:00
Andras Bacsai
75e674a966 version++ 2023-09-08 09:27:44 +02:00
40 changed files with 386 additions and 123 deletions

26
.github/ISSUE_TEMPLATE/BUG_REPORT.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Bug report
description: Create a new bug report
title: '[Bug]: '
body:
- type: textarea
attributes:
label: Description
description: A clear and concise description of the problem
validations:
required: true
- type: textarea
attributes:
label: Minimal Reproduction (if possible, example repository)
description: Please provide a step by step guide to reproduce the issue
validations:
required: true
- type: textarea
attributes:
label: Exception or Error
description: Please provide error logs if possible.
- type: input
attributes:
label: Version
description: Coolify's version (see bottom left corner).
validations:
required: true

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 🤔 Community Support (Chat)
url: https://coollabs.io/discord
about: Reach out to us on Discord.
- name: 🙋‍♂️ Feature Requests
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests-ideas
about: All feature requests will be discussed here.

View File

@@ -40,7 +40,7 @@ class InstallDocker
"echo ####### Restarting Docker Engine...", "echo ####### Restarting Docker Engine...",
"systemctl restart docker", "systemctl restart docker",
"echo ####### Creating default network...", "echo ####### Creating default network...",
"docker network create --attachable coolify", "docker network create --attachable coolify >/dev/null 2>&1 || true",
"echo ####### Done!" "echo ####### Done!"
], $server); ], $server);
$found = StandaloneDocker::where('server_id', $server->id); $found = StandaloneDocker::where('server_id', $server->id);

View File

@@ -23,6 +23,7 @@ use Mail;
use Str; use Str;
use function Laravel\Prompts\select; use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
class TestEmail extends Command class TestEmail extends Command
{ {
@@ -44,9 +45,10 @@ class TestEmail extends Command
* Execute the console command. * Execute the console command.
*/ */
private ?MailMessage $mail = null; private ?MailMessage $mail = null;
private string $email = 'andras.bacsai@protonmail.com';
public function handle() public function handle()
{ {
$email = select( $type = select(
'Which Email should be sent?', 'Which Email should be sent?',
options: [ options: [
'emails-test' => 'Test', 'emails-test' => 'Test',
@@ -60,15 +62,15 @@ class TestEmail extends Command
'waitlist-confirmation' => 'Waitlist Confirmation', 'waitlist-confirmation' => 'Waitlist Confirmation',
], ],
); );
$type = set_transanctional_email_settings(); $this->email = text('Email Address to send to');
if (!$type) { set_transanctional_email_settings();
throw new Exception('No email settings found.');
}
$this->mail = new MailMessage(); $this->mail = new MailMessage();
$this->mail->subject("Test Email"); $this->mail->subject("Test Email");
switch ($email) { switch ($type) {
case 'emails-test': case 'emails-test':
$this->mail = (new Test())->toMail(); $this->mail = (new Test())->toMail();
$this->sendEmail();
break; break;
case 'application-deployment-success': case 'application-deployment-success':
$application = Application::all()->first(); $application = Application::all()->first();
@@ -172,11 +174,7 @@ class TestEmail extends Command
[], [],
[], [],
fn (Message $message) => $message fn (Message $message) => $message
->from( ->to($this->email)
'internal@example.com',
'Test Email',
)
->to('test@example.com')
->subject($this->mail->subject) ->subject($this->mail->subject)
->html((string)$this->mail->render()) ->html((string)$this->mail->render())
); );

View File

@@ -74,6 +74,11 @@ class Kernel extends ConsoleKernel
if (!$scheduled_backup->enabled) { if (!$scheduled_backup->enabled) {
continue; continue;
} }
if (is_null(data_get($scheduled_backup,'database'))) {
ray('database not found');
$scheduled_backup->delete();
continue;
}
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];

View File

@@ -6,6 +6,7 @@ use App\Actions\Server\InstallDocker;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use App\Models\Team;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
@@ -70,9 +71,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
} }
public function skipBoarding() public function skipBoarding()
{ {
currentTeam()->update([ Team::find(currentTeam()->id)->update([
'show_boarding' => false 'show_boarding' => false
]); ]);
ray(currentTeam());
refreshSession(); refreshSession();
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }

View File

@@ -17,6 +17,10 @@ class TelegramSettings extends Component
'team.telegram_notifications_deployments' => 'nullable|boolean', 'team.telegram_notifications_deployments' => 'nullable|boolean',
'team.telegram_notifications_status_changes' => 'nullable|boolean', 'team.telegram_notifications_status_changes' => 'nullable|boolean',
'team.telegram_notifications_database_backups' => 'nullable|boolean', 'team.telegram_notifications_database_backups' => 'nullable|boolean',
'team.telegram_notifications_test_message_thread_id' => 'nullable|string',
'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string',
'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string',
'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'team.telegram_token' => 'Token', 'team.telegram_token' => 'Token',

View File

@@ -64,6 +64,7 @@ class General extends Component
return; return;
} }
if ($this->database->is_public) { if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
startPostgresProxy($this->database); startPostgresProxy($this->database);
$this->emit('success', 'Database is now publicly accessible.'); $this->emit('success', 'Database is now publicly accessible.');
} else { } else {

View File

@@ -5,21 +5,84 @@ namespace App\Http\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable; use App\Models\EnvironmentVariable;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use Str;
class All extends Component class All extends Component
{ {
public $resource; public $resource;
public bool $showPreview = false;
public string|null $modalId = null; public string|null $modalId = null;
public ?string $variables = null;
public ?string $variablesPreview = null;
public string $view = 'normal';
protected $listeners = ['refreshEnvs', 'submit']; protected $listeners = ['refreshEnvs', 'submit'];
public function mount() public function mount()
{ {
$resourceClass = get_class($this->resource);
$resourceWithPreviews = ['App\Models\Application'];
$simpleDockerfile = !is_null(data_get($this->resource, 'dockerfile'));
if (Str::of($resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) {
$this->showPreview = true;
}
$this->modalId = new Cuid2(7); $this->modalId = new Cuid2(7);
$this->getDevView();
}
public function getDevView()
{
$this->variables = $this->resource->environment_variables->map(function ($item) {
return "$item->key=$item->value";
})->sort()->join('
');
if ($this->showPreview) {
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
return "$item->key=$item->value";
})->sort()->join('
');
}
}
public function switch()
{
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
}
public function saveVariables($isPreview)
{
if ($isPreview) {
$variables = parseEnvFormatToArray($this->variablesPreview);
$existingVariables = $this->resource->environment_variables_preview();
$this->resource->environment_variables_preview()->delete();
} else {
$variables = parseEnvFormatToArray($this->variables);
$existingVariables = $this->resource->environment_variables();
$this->resource->environment_variables()->delete();
}
foreach ($variables as $key => $variable) {
$found = $existingVariables->where('key', $key)->first();
if ($found) {
$found->value = $variable;
$found->save();
continue;
} else {
$environment = new EnvironmentVariable();
$environment->key = $key;
$environment->value = $variable;
$environment->is_build_time = false;
$environment->is_preview = $isPreview ? true : false;
if ($this->resource->type() === 'application') {
$environment->application_id = $this->resource->id;
}
if ($this->resource->type() === 'standalone-postgresql') {
$environment->standalone_postgresql_id = $this->resource->id;
}
$environment->save();
}
}
$this->refreshEnvs();
} }
public function refreshEnvs() public function refreshEnvs()
{ {
$this->resource->refresh(); $this->resource->refresh();
$this->getDevView();
} }
public function submit($data) public function submit($data)
@@ -43,7 +106,7 @@ class All extends Component
$environment->standalone_postgresql_id = $this->resource->id; $environment->standalone_postgresql_id = $this->resource->id;
} }
$environment->save(); $environment->save();
$this->resource->refresh(); $this->refreshEnvs();
$this->emit('success', 'Environment variable added successfully.'); $this->emit('success', 'Environment variable added successfully.');
} catch (\Exception $e) { } catch (\Exception $e) {
return general_error_handler(err: $e, that: $this); return general_error_handler(err: $e, that: $this);

View File

@@ -60,7 +60,6 @@ class Email extends Component
$this->validate([ $this->validate([
'settings.resend_api_key' => 'required' 'settings.resend_api_key' => 'required'
]); ]);
$this->settings->smtp_enabled = false;
$this->settings->save(); $this->settings->save();
$this->emit('success', 'Settings saved successfully.'); $this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) { } catch (\Exception $e) {
@@ -68,9 +67,18 @@ class Email extends Component
return general_error_handler($e, $this); return general_error_handler($e, $this);
} }
} }
public function instantSaveResend() {
try {
$this->settings->smtp_enabled = false;
$this->submitResend();
} catch (\Exception $e) {
return general_error_handler($e, $this);
}
}
public function instantSave() public function instantSave()
{ {
try { try {
$this->settings->resend_enabled = false;
$this->submit(); $this->submit();
} catch (\Exception $e) { } catch (\Exception $e) {
return general_error_handler($e, $this); return general_error_handler($e, $this);
@@ -89,7 +97,6 @@ class Email extends Component
'settings.smtp_password' => 'nullable', 'settings.smtp_password' => 'nullable',
'settings.smtp_timeout' => 'nullable', 'settings.smtp_timeout' => 'nullable',
]); ]);
$this->settings->resend_enabled = false;
$this->settings->save(); $this->settings->save();
$this->emit('success', 'Settings saved successfully.'); $this->emit('success', 'Settings saved successfully.');
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@@ -23,7 +23,7 @@ class SwitchTeam extends Component
if (!$team_to_switch_to) { if (!$team_to_switch_to) {
return; return;
} }
session(['currentTeam' => $team_to_switch_to]); refreshSession($team_to_switch_to);
return redirect(request()->header('Referer')); return redirect(request()->header('Referer'));
} }
} }

View File

@@ -24,31 +24,27 @@ class DatabaseBackupJob implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Team|null $team = null; public ?Team $team = null;
public Server $server; public Server $server;
public ScheduledDatabaseBackup|null $backup; public ScheduledDatabaseBackup $backup;
public string $database_type;
public StandalonePostgresql $database; public StandalonePostgresql $database;
public string $database_status;
public string|null $container_name = null; public ?string $container_name = null;
public ScheduledDatabaseBackupExecution|null $backup_log = null; public ?ScheduledDatabaseBackupExecution $backup_log = null;
public string $backup_status; public string $backup_status;
public string|null $backup_location = null; public ?string $backup_location = null;
public string $backup_dir; public string $backup_dir;
public string $backup_file; public string $backup_file;
public int $size = 0; public int $size = 0;
public string|null $backup_output = null; public ?string $backup_output = null;
public S3Storage|null $s3 = null; public ?S3Storage $s3 = null;
public function __construct($backup) public function __construct($backup)
{ {
$this->backup = $backup; $this->backup = $backup;
$this->team = Team::find($backup->team_id); $this->team = Team::find($backup->team_id);
$this->database = $this->backup->database; $this->database = data_get($this->backup, 'database');
$this->database_type = $this->database->type();
$this->server = $this->database->destination->server; $this->server = $this->database->destination->server;
$this->database_status = $this->database->status;
$this->s3 = $this->backup->s3; $this->s3 = $this->backup->s3;
} }
@@ -65,7 +61,7 @@ class DatabaseBackupJob implements ShouldQueue
public function handle(): void public function handle(): void
{ {
try { try {
if ($this->database_status !== 'running') { if (data_get($this->database, 'status') !== 'running') {
ray('database not running'); ray('database not running');
return; return;
} }
@@ -84,7 +80,7 @@ class DatabaseBackupJob implements ShouldQueue
'filename' => $this->backup_location, 'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id, 'scheduled_database_backup_id' => $this->backup->id,
]); ]);
if ($this->database_type === 'standalone-postgresql') { if ($this->database->type() === 'standalone-postgresql') {
$this->backup_standalone_postgresql(); $this->backup_standalone_postgresql();
} }
$this->calculate_size(); $this->calculate_size();
@@ -99,7 +95,6 @@ class DatabaseBackupJob implements ShouldQueue
send_internal_notification('DatabaseBackupJob failed with: ' . $th->getMessage()); send_internal_notification('DatabaseBackupJob failed with: ' . $th->getMessage());
throw $th; throw $th;
} }
} }
private function backup_standalone_postgresql(): void private function backup_standalone_postgresql(): void

View File

@@ -31,6 +31,7 @@ class SendMessageToTelegramJob implements ShouldQueue
public array $buttons, public array $buttons,
public string $token, public string $token,
public string $chatId, public string $chatId,
public ?string $topicId = null,
) { ) {
} }
@@ -63,7 +64,9 @@ class SendMessageToTelegramJob implements ShouldQueue
'chat_id' => $this->chatId, 'chat_id' => $this->chatId,
'text' => $this->text, 'text' => $this->text,
]; ];
ray($payload); if ($this->topicId) {
$payload['message_thread_id'] = $this->topicId;
}
$response = Http::post($url, $payload); $response = Http::post($url, $payload);
if ($response->failed()) { if ($response->failed()) {
throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body()); throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body());

View File

@@ -105,7 +105,7 @@ class Application extends BaseModel
public function environment_variables(): HasMany public function environment_variables(): HasMany
{ {
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false); return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
} }
public function runtime_environment_variables(): HasMany public function runtime_environment_variables(): HasMany
@@ -127,7 +127,7 @@ class Application extends BaseModel
public function environment_variables_preview(): HasMany public function environment_variables_preview(): HasMany
{ {
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true); return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
} }
public function runtime_environment_variables_preview(): HasMany public function runtime_environment_variables_preview(): HasMany

View File

@@ -20,13 +20,16 @@ class EnvironmentVariable extends Model
{ {
static::created(function ($environment_variable) { static::created(function ($environment_variable) {
if ($environment_variable->application_id && !$environment_variable->is_preview) { if ($environment_variable->application_id && !$environment_variable->is_preview) {
ModelsEnvironmentVariable::create([ $found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview',true)->first();
'key' => $environment_variable->key, if (!$found) {
'value' => $environment_variable->value, ModelsEnvironmentVariable::create([
'is_build_time' => $environment_variable->is_build_time, 'key' => $environment_variable->key,
'application_id' => $environment_variable->application_id, 'value' => $environment_variable->value,
'is_preview' => true, 'is_build_time' => $environment_variable->is_build_time,
]); 'application_id' => $environment_variable->application_id,
'is_preview' => true,
]);
}
} }
}); });
} }

View File

@@ -23,6 +23,6 @@ class StandaloneDocker extends BaseModel
public function attachedTo() public function attachedTo()
{ {
return $this->applications->count() > 0 || $this->databases->count() > 0; return $this->applications?->count() > 0 || $this->databases?->count() > 0;
} }
} }

View File

@@ -28,6 +28,11 @@ class StandalonePostgresql extends BaseModel
'is_readonly' => true 'is_readonly' => true
]); ]);
}); });
static::deleted(function ($database) {
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false);
});
} }
public function portsMappings(): Attribute public function portsMappings(): Attribute

View File

@@ -28,7 +28,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
{ {
return [ return [
"token" => data_get($this, 'telegram_token', null), "token" => data_get($this, 'telegram_token', null),
"chat_id" => data_get($this, 'telegram_chat_id', null) "chat_id" => data_get($this, 'telegram_chat_id', null),
]; ];
} }

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsEmail; use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword; use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
use Cache;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@@ -94,7 +95,9 @@ class User extends Authenticatable implements SendsEmail
public function currentTeam() public function currentTeam()
{ {
return Team::find(session('currentTeam')->id); return Cache::remember('team:' . auth()->user()->id, 3600, function() {
return Team::find(session('currentTeam')->id);
});
} }
public function otherTeams() public function otherTeams()

View File

@@ -16,6 +16,7 @@ class DeploymentFailed extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5;
public Application $application; public Application $application;
public string $deployment_uuid; public string $deployment_uuid;
public ?ApplicationPreview $preview = null; public ?ApplicationPreview $preview = null;

View File

@@ -16,6 +16,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5;
public Application $application; public Application $application;
public string $deployment_uuid; public string $deployment_uuid;
public ApplicationPreview|null $preview = null; public ApplicationPreview|null $preview = null;

View File

@@ -14,6 +14,7 @@ class StatusChanged extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5;
public $application; public $application;
public string $application_name; public string $application_name;

View File

@@ -10,16 +10,30 @@ class TelegramChannel
{ {
$data = $notification->toTelegram($notifiable); $data = $notification->toTelegram($notifiable);
$telegramData = $notifiable->routeNotificationForTelegram(); $telegramData = $notifiable->routeNotificationForTelegram();
$message = data_get($data, 'message'); $message = data_get($data, 'message');
$buttons = data_get($data, 'buttons', []); $buttons = data_get($data, 'buttons', []);
ray($message, $buttons);
$telegramToken = data_get($telegramData, 'token'); $telegramToken = data_get($telegramData, 'token');
$chatId = data_get($telegramData, 'chat_id'); $chatId = data_get($telegramData, 'chat_id');
$topicId = null;
$topicsInstance = get_class($notification);
if (!$telegramToken || !$chatId || !$message) { switch ($topicsInstance) {
throw new \Exception('Telegram token, chat id and message are required'); case 'App\Notifications\StatusChange':
$topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
break;
case 'App\Notifications\Test':
$topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id');
break;
case 'App\Notifications\Deployment':
$topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id');
break;
case 'App\Notifications\DatabaseBackup':
$topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id');
break;
} }
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId)); if (!$telegramToken || !$chatId || !$message) {
return;
}
dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId));
} }
} }

View File

@@ -14,6 +14,7 @@ class BackupFailed extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5;
public string $name; public string $name;
public string $frequency; public string $frequency;

View File

@@ -14,6 +14,7 @@ class BackupSuccess extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5;
public string $name; public string $name;
public string $frequency; public string $frequency;

View File

@@ -12,6 +12,7 @@ class GeneralNotification extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5;
public function __construct(public string $message) public function __construct(public string $message)
{ {
} }

View File

@@ -15,6 +15,7 @@ class NotReachable extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5;
public function __construct(public Server $server) public function __construct(public Server $server)
{ {

View File

@@ -14,6 +14,7 @@ class Test extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5;
public function __construct(public string|null $emails = null) public function __construct(public string|null $emails = null)
{ {
} }

View File

@@ -15,6 +15,7 @@ class InvitationLink extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5;
public function via(): array public function via(): array
{ {
return [TransactionalEmailChannel::class]; return [TransactionalEmailChannel::class];

View File

@@ -12,6 +12,7 @@ class Test extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable;
public $tries = 5;
public function __construct(public string $emails) public function __construct(public string $emails)
{ {
} }

View File

@@ -57,9 +57,15 @@ function showBoarding(): bool
{ {
return currentTeam()->show_boarding ?? false; return currentTeam()->show_boarding ?? false;
} }
function refreshSession(): void function refreshSession(?Team $team = null): void
{ {
$team = Team::find(currentTeam()->id); if (!$team) {
$team = Team::find(currentTeam()->id);
}
Cache::forget('team:' . auth()->user()->id);
Cache::remember('team:' . auth()->user()->id, 3600, function() use ($team) {
return $team;
});
session(['currentTeam' => $team]); session(['currentTeam' => $team]);
} }
function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed function general_error_handler(Throwable | null $err = null, $that = null, $isJson = false, $customErrorMessage = null): mixed
@@ -296,3 +302,25 @@ function setNotificationChannels($notifiable, $event)
} }
return $channels; return $channels;
} }
function parseEnvFormatToArray($env_file_contents) {
$env_array = array();
$lines = explode("\n", $env_file_contents);
foreach ($lines as $line) {
if ($line === '' || substr($line, 0, 1) === '#') {
continue;
}
$equals_pos = strpos($line, '=');
if ($equals_pos !== false) {
$key = substr($line, 0, $equals_pos);
$value = substr($line, $equals_pos + 1);
if (substr($value, 0, 1) === '"' && substr($value, -1) === '"') {
$value = substr($value, 1, -1);
}
elseif (substr($value, 0, 1) === "'" && substr($value, -1) === "'") {
$value = substr($value, 1, -1);
}
$env_array[$key] = $value;
}
}
return $env_array;
}

View File

@@ -3,11 +3,11 @@
return [ return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://abe219b6573947128ecf523c835f5f38@o1082494.ingest.sentry.io/4505347448045568', 'dsn' => 'https://62de992090e4e0cb28f18231835ea006@o1082494.ingest.sentry.io/4505347448045568',
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.26', 'release' => '4.0.0-beta.30',
'server_name' => env('APP_ID', 'coolify'), 'server_name' => env('APP_ID', 'coolify'),
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.26'; return '4.0.0-beta.30';

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->text('telegram_notifications_test_message_thread_id')->nullable();
$table->text('telegram_notifications_deployments_message_thread_id')->nullable();
$table->text('telegram_notifications_status_changes_message_thread_id')->nullable();
$table->text('telegram_notifications_database_backups_message_thread_id')->nullable();
});
}
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->dropColumn('telegram_message_thread_id');
$table->dropColumn('telegram_notifications_test_message_thread_id');
$table->dropColumn('telegram_notifications_deployments_message_thread_id');
$table->dropColumn('telegram_notifications_status_changes_message_thread_id');
$table->dropColumn('telegram_notifications_database_backups_message_thread_id');
});
}
};

View File

@@ -20,20 +20,31 @@
id="team.discord_webhook_url" label="Webhook" /> id="team.discord_webhook_url" label="Webhook" />
</form> </form>
@if (data_get($team, 'discord_enabled')) @if (data_get($team, 'discord_enabled'))
<h3 class="mt-4">Subscribe to events</h3> <h2 class="mt-4">Subscribe to events</h2>
<div class="w-64"> <div class="w-64">
@if (isDev()) @if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Test" /> <h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Enabled" />
</div>
@endif @endif
<h4 class="mt-4">General</h4> <h3 class="mt-4">Container Status Changes</h3>
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes" <div class="flex items-end gap-10">
label="Container Status Changes" /> <x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes"
<h4 class="mt-4">Applications</h4> label="Enabled" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments" </div>
label="Deployments" /> <h3 class="mt-4">Application Deployments</h3>
<h4 class="mt-4">Databases</h4> <div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups" <x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments"
label="Backup Statuses" /> label="Enabled" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups"
label="Enabled" />
</div>
</div> </div>
@endif @endif
</div> </div>

View File

@@ -21,7 +21,7 @@
Copy from Instance Settings Copy from Instance Settings
</x-forms.button> </x-forms.button>
@endif @endif
@if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings')) @if (isEmailEnabled($team) && auth()->user()->isAdminFromSession())
<x-forms.button onclick="sendTestEmail.showModal()" <x-forms.button onclick="sendTestEmail.showModal()"
class="text-white normal-case btn btn-xs no-animation btn-primary"> class="text-white normal-case btn btn-xs no-animation btn-primary">
Send Test Email Send Test Email
@@ -36,12 +36,11 @@
label="Use hosted email service" /> label="Use hosted email service" />
</div> </div>
@else @else
<div class="pb-4 w-96"> <div class="pb-4 w-96">
<x-forms.checkbox disabled id="team.use_instance_email_settings" <x-forms.checkbox disabled id="team.use_instance_email_settings"
label="Use hosted email service (Pro+ subscription required)" /> label="Use hosted email service (Pro+ subscription required)" />
</div> </div>
@endif @endif
<h3 class="pb-4">Custom Email Service</h3>
@if (!$team->use_instance_email_settings) @if (!$team->use_instance_email_settings)
<form class="flex flex-col items-end gap-2 pb-4 xl:flex-row" wire:submit.prevent='submitFromFields'> <form class="flex flex-col items-end gap-2 pb-4 xl:flex-row" wire:submit.prevent='submitFromFields'>
<x-forms.input required id="team.smtp_from_name" helper="Name used in emails." label="From Name" /> <x-forms.input required id="team.smtp_from_name" helper="Name used in emails." label="From Name" />
@@ -110,19 +109,27 @@
</div> </div>
@endif @endif
@if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings')) @if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings'))
<h3 class="mt-4">Subscribe to events</h3> <h2 class="mt-4">Subscribe to events</h2>
<div class="w-64"> <div class="w-64">
@if (isDev()) @if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_test" label="Test" /> <h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_test" label="Enabled" />
</div>
@endif @endif
<h4 class="mt-4">General</h4> <h3 class="mt-4">Container Status Changes</h3>
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_status_changes" <div class="flex items-end gap-10">
label="Container Status Changes" /> <x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_status_changes" label="Enabled" />
<h4 class="mt-4">Applications</h4> </div>
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_deployments" label="Deployments" /> <h3 class="mt-4">Application Deployments</h3>
<h4 class="mt-4">Databases</h4> <div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups" <x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_deployments" label="Enabled" />
label="Backup Statuses" /> </div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.smtp_notifications_database_backups"
label="Enabled" />
</div>
</div> </div>
@endif @endif
</div> </div>

View File

@@ -16,27 +16,50 @@
<x-forms.checkbox instantSave id="team.telegram_enabled" label="Notification Enabled" /> <x-forms.checkbox instantSave id="team.telegram_enabled" label="Notification Enabled" />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input type="password" helper="Get it from the <a class='inline-block text-white underline' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram." required <x-forms.input type="password"
id="team.telegram_token" label="Token" /> helper="Get it from the <a class='inline-block text-white underline' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram."
<x-forms.input type="password" helper="Recommended to add your bot to a group chat and add its Chat ID here." required required id="team.telegram_token" label="Token" />
<x-forms.input
helper="Recommended to add your bot to a group chat and add its Chat ID here." required
id="team.telegram_chat_id" label="Chat ID" /> id="team.telegram_chat_id" label="Chat ID" />
</div> </div>
</form>
@if (data_get($team, 'telegram_enabled')) @if (data_get($team, 'telegram_enabled'))
<h3 class="mt-4">Subscribe to events</h3> <h2 class="mt-4">Subscribe to events</h2>
<div class="w-64"> <div class="w-96">
@if (isDev()) @if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test" label="Test" /> <h3 class="mt-4">Test</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test" label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_test_message_thread_id" label="Custom Topic ID" />
</div>
@endif @endif
<h4 class="mt-4">General</h4> <h3 class="mt-4">Container Status Changes</h3>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_status_changes" <div class="flex items-end gap-10">
label="Container Status Changes" /> <x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_status_changes"
<h4 class="mt-4">Applications</h4> label="Enabled" />
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_deployments" <x-forms.input
label="Deployments" /> helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
<h4 class="mt-4">Databases</h4> id="team.telegram_notifications_status_changes_message_thread_id" label="Custom Topic ID" />
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_database_backups" </div>
label="Backup Statuses" /> <h3 class="mt-4">Application Deployments</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_deployments"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_deployments_message_thread_id" label="Custom Topic ID" />
</div>
<h3 class="mt-4">Backup Status</h3>
<div class="flex items-end gap-10">
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_database_backups"
label="Enabled" />
<x-forms.input
helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used."
id="team.telegram_notifications_database_backups_message_thread_id" label="Custom Topic ID" />
</div>
</div> </div>
@endif @endif
</form>
</div> </div>

View File

@@ -4,23 +4,40 @@
<h2>Environment Variables</h2> <h2>Environment Variables</h2>
<x-forms.button class="btn" onclick="newVariable.showModal()">+ Add</x-forms.button> <x-forms.button class="btn" onclick="newVariable.showModal()">+ Add</x-forms.button>
<livewire:project.shared.environment-variable.add /> <livewire:project.shared.environment-variable.add />
<x-forms.button
wire:click='switch'>{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}</x-forms.button>
</div> </div>
<div>Environment (secrets) variables for this resource.</div> <div>Environment variables (secrets) for this resource.</div>
</div> </div>
@forelse ($resource->environment_variables as $env) @if ($view === 'normal')
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" @forelse ($resource->environment_variables as $env)
:env="$env" />
@empty
<div class="text-neutral-500">No environment variables found.</div>
@endforelse
@if ($resource->type() === 'application' && $resource->environment_variables_preview->count() > 0)
<div>
<h3>Preview Deployments</h3>
<div>Environment (secrets) variables for Preview Deployments.</div>
</div>
@foreach ($resource->environment_variables_preview as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}" <livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" /> :env="$env" />
@endforeach @empty
<div class="text-neutral-500">No environment variables found.</div>
@endforelse
@if ($resource->type() === 'application' && $resource->environment_variables_preview->count() > 0 && $showPreview)
<div>
<h3>Preview Deployments</h3>
<div>Environment (secrets) variables for Preview Deployments.</div>
</div>
@foreach ($resource->environment_variables_preview as $env)
<livewire:project.shared.environment-variable.show wire:key="environment-{{ $env->id }}"
:env="$env" />
@endforeach
@endif
@else
<form wire:submit.prevent='saveVariables(false)' class="flex flex-col gap-2">
<x-forms.textarea rows=5 class="whitespace-pre-wrap" label="Environment Variables"
id="variables"></x-forms.textarea>
<x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button>
</form>
@if ($showPreview)
<form wire:submit.prevent='saveVariables(true)' class="flex flex-col gap-2">
<x-forms.textarea rows=5 class="whitespace-pre-wrap" label="Preview Environment Variables"
id="variablesPreview"></x-forms.textarea>
<x-forms.button type="submit" class="btn btn-primary">Save</x-forms.button>
</form>
@endif
@endif @endif
</div> </div>

View File

@@ -22,12 +22,12 @@
<x-forms.button type="submit"> <x-forms.button type="submit">
Save Save
</x-forms.button> </x-forms.button>
@if ($settings->resend_enabled || $settings->smtp_enabled) @if (isEmailEnabled($settings))
<x-forms.button onclick="sendTestEmail.showModal()" <x-forms.button onclick="sendTestEmail.showModal()"
class="text-white normal-case btn btn-xs no-animation btn-primary"> class="text-white normal-case btn btn-xs no-animation btn-primary">
Send Test Email Send Test Email
</x-forms.button> </x-forms.button>
@endif @endif
</div> </div>
</form> </form>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
@@ -67,14 +67,15 @@
<summary class="text-xl collapse-title"> <summary class="text-xl collapse-title">
<div>Resend</div> <div>Resend</div>
<div class="w-32"> <div class="w-32">
<x-forms.checkbox instantSave='submitResend' id="settings.resend_enabled" label="Enabled" /> <x-forms.checkbox instantSave='instantSaveResend' id="settings.resend_enabled" label="Enabled" />
</div> </div>
</summary> </summary>
<div class="collapse-content"> <div class="collapse-content">
<form wire:submit.prevent='submitResend' class="flex flex-col"> <form wire:submit.prevent='submitResend' class="flex flex-col">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row"> <div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" id="settings.resend_api_key" placeholder="API key" label="Host" /> <x-forms.input type="password" id="settings.resend_api_key" placeholder="API key"
label="Host" />
</div> </div>
</div> </div>
<div class="flex justify-end gap-4 pt-6"> <div class="flex justify-end gap-4 pt-6">

View File

@@ -4,7 +4,7 @@
"version": "3.12.36" "version": "3.12.36"
}, },
"v4": { "v4": {
"version": "4.0.0-beta.25" "version": "4.0.0-beta.30"
} }
} }
} }