diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 19d22ae21..2ed3ee454 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -218,7 +218,7 @@ class Kernel extends ConsoleKernel
}
}
if ($service) {
- if (str($service->status())->contains('running') === false) {
+ if (str($service->status)->contains('running') === false) {
continue;
}
}
diff --git a/app/Http/Controllers/Api/ResourcesController.php b/app/Http/Controllers/Api/ResourcesController.php
index 4180cef9a..ad12c83ab 100644
--- a/app/Http/Controllers/Api/ResourcesController.php
+++ b/app/Http/Controllers/Api/ResourcesController.php
@@ -53,11 +53,7 @@ class ResourcesController extends Controller
$resources = $resources->flatten();
$resources = $resources->map(function ($resource) {
$payload = $resource->toArray();
- if ($resource->getMorphClass() === \App\Models\Service::class) {
- $payload['status'] = $resource->status();
- } else {
- $payload['status'] = $resource->status;
- }
+ $payload['status'] = $resource->status;
$payload['type'] = $resource->type();
return $payload;
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index f37040bdd..b1deb5321 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -154,11 +154,7 @@ class ServersController extends Controller
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
- if ($resource->type() === 'service') {
- $payload['status'] = $resource->status();
- } else {
- $payload['status'] = $resource->status;
- }
+ $payload['status'] = $resource->status;
return $payload;
});
@@ -237,11 +233,7 @@ class ServersController extends Controller
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
- if ($resource->type() === 'service') {
- $payload['status'] = $resource->status();
- } else {
- $payload['status'] = $resource->status;
- }
+ $payload['status'] = $resource->status;
return $payload;
});
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index ed9af8c90..bcaba7107 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -1072,7 +1072,7 @@ class ServicesController extends Controller
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
- if (str($service->status())->contains('running')) {
+ if (str($service->status)->contains('running')) {
return response()->json(['message' => 'Service is already running.'], 400);
}
StartService::dispatch($service);
@@ -1150,7 +1150,7 @@ class ServicesController extends Controller
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
- if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) {
+ if (str($service->status)->contains('stopped') || str($service->status)->contains('exited')) {
return response()->json(['message' => 'Service is already stopped.'], 400);
}
StopService::dispatch($service);
diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php
index ee43dc911..22fc1c0d6 100644
--- a/app/Livewire/Project/Service/Navbar.php
+++ b/app/Livewire/Project/Service/Navbar.php
@@ -27,7 +27,7 @@ class Navbar extends Component
public function mount()
{
- if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
+ if (str($this->service->status)->contains('running') && is_null($this->service->config_hash)) {
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
}
diff --git a/app/Livewire/SettingsOauth.php b/app/Livewire/SettingsOauth.php
index d5f0be14a..857de1df8 100644
--- a/app/Livewire/SettingsOauth.php
+++ b/app/Livewire/SettingsOauth.php
@@ -35,16 +35,26 @@ class SettingsOauth extends Component
}, []);
}
- private function updateOauthSettings()
+ private function updateOauthSettings(?string $provider = null)
{
- foreach (array_values($this->oauth_settings_map) as &$setting) {
- $setting->save();
+ if ($provider) {
+ $oauth = $this->oauth_settings_map[$provider];
+ if (! $oauth->couldBeEnabled()) {
+ $oauth->update(['enabled' => false]);
+ throw new \Exception('OAuth settings are not complete for '.$oauth->provider.'.
Please fill in all required fields.');
+ }
+ $oauth->save();
+ $this->dispatch('success', 'OAuth settings for '.$oauth->provider.' updated successfully!');
}
}
- public function instantSave()
+ public function instantSave(string $provider)
{
- $this->updateOauthSettings();
+ try {
+ $this->updateOauthSettings($provider);
+ } catch (\Exception $e) {
+ return handleError($e, $this);
+ }
}
public function submit()
diff --git a/app/Models/OauthSetting.php b/app/Models/OauthSetting.php
index c17c318f1..3d82e89f2 100644
--- a/app/Models/OauthSetting.php
+++ b/app/Models/OauthSetting.php
@@ -11,6 +11,8 @@ class OauthSetting extends Model
{
use HasFactory;
+ protected $fillable = ['provider', 'client_id', 'client_secret', 'redirect_uri', 'tenant', 'base_url', 'enabled'];
+
protected function clientSecret(): Attribute
{
return Attribute::make(
@@ -18,4 +20,16 @@ class OauthSetting extends Model
set: fn (?string $value) => empty($value) ? null : Crypt::encryptString($value),
);
}
+
+ public function couldBeEnabled(): bool
+ {
+ switch ($this->provider) {
+ case 'azure':
+ return filled($this->client_id) && filled($this->client_secret) && filled($this->redirect_uri) && filled($this->tenant);
+ case 'authentik':
+ return filled($this->client_id) && filled($this->client_secret) && filled($this->redirect_uri) && filled($this->base_url);
+ default:
+ return filled($this->client_id) && filled($this->client_secret) && filled($this->redirect_uri);
+ }
+ }
}
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 117677d53..5a2690490 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -46,7 +46,7 @@ class Service extends BaseModel
protected $guarded = [];
- protected $appends = ['server_status'];
+ protected $appends = ['server_status', 'status'];
protected static function booted()
{
@@ -105,12 +105,12 @@ class Service extends BaseModel
public function isRunning()
{
- return (bool) str($this->status())->contains('running');
+ return (bool) str($this->status)->contains('running');
}
public function isExited()
{
- return (bool) str($this->status())->contains('exited');
+ return (bool) str($this->status)->contains('exited');
}
public function type()
@@ -213,7 +213,7 @@ class Service extends BaseModel
instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
- public function status()
+ public function getStatusAttribute()
{
$applications = $this->applications;
$databases = $this->databases;
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 015434bd2..2ce94201c 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -3,6 +3,7 @@
namespace App\Providers;
use App\Models\PersonalAccessToken;
+use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
@@ -19,6 +20,9 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
+ Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
+ $event->extendSocialite('authentik', \SocialiteProviders\Authentik\Provider::class);
+ });
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
Password::defaults(function () {
diff --git a/app/View/Components/Status/Services.php b/app/View/Components/Status/Services.php
index 70db62172..291841854 100644
--- a/app/View/Components/Status/Services.php
+++ b/app/View/Components/Status/Services.php
@@ -17,7 +17,7 @@ class Services extends Component
public string $complexStatus = 'exited',
public bool $showRefreshButton = true
) {
- $this->complexStatus = $service->status();
+ $this->complexStatus = $service->status;
}
/**
diff --git a/config/constants.php b/config/constants.php
index a02d6616a..8c6c12da4 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,7 +2,7 @@
return [
'coolify' => [
- 'version' => '4.0.0-beta.377',
+ 'version' => '4.0.0-beta.378',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
diff --git a/config/services.php b/config/services.php
index 509e73756..46fd12ec3 100644
--- a/config/services.php
+++ b/config/services.php
@@ -30,4 +30,19 @@ return [
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
+
+ 'azure' => [
+ 'client_id' => env('AZURE_CLIENT_ID'),
+ 'client_secret' => env('AZURE_CLIENT_SECRET'),
+ 'redirect' => env('AZURE_REDIRECT_URI'),
+ 'tenant' => env('AZURE_TENANT_ID'),
+ 'proxy' => env('AZURE_PROXY'),
+ ],
+
+ 'authentik' => [
+ 'base_url' => env('AUTHENTIK_BASE_URL'),
+ 'client_id' => env('AUTHENTIK_CLIENT_ID'),
+ 'client_secret' => env('AUTHENTIK_CLIENT_SECRET'),
+ 'redirect' => env('AUTHENTIK_REDIRECT_URI'),
+ ],
];
diff --git a/database/migrations/2024_12_13_103007_encrypt_resend_api_key_in_instance_settings.php b/database/migrations/2024_12_13_103007_encrypt_resend_api_key_in_instance_settings.php
new file mode 100644
index 000000000..ab9b3416a
--- /dev/null
+++ b/database/migrations/2024_12_13_103007_encrypt_resend_api_key_in_instance_settings.php
@@ -0,0 +1,46 @@
+exists()) {
+ $settings = DB::table('instance_settings')->get();
+ foreach ($settings as $setting) {
+ try {
+ DB::table('instance_settings')->where('id', $setting->id)->update([
+ 'resend_api_key' => $setting->resend_api_key ? Crypt::encryptString($setting->resend_api_key) : null,
+ ]);
+ } catch (Exception $e) {
+ \Log::error('Error encrypting resend_api_key: '.$e->getMessage());
+ }
+ }
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ if (DB::table('instance_settings')->exists()) {
+ $settings = DB::table('instance_settings')->get();
+ foreach ($settings as $setting) {
+ try {
+ DB::table('instance_settings')->where('id', $setting->id)->update([
+ 'resend_api_key' => $setting->resend_api_key ? Crypt::decryptString($setting->resend_api_key) : null,
+ ]);
+ } catch (Exception $e) {
+ \Log::error('Error decrypting resend_api_key: '.$e->getMessage());
+ }
+ }
+ }
+ }
+};
diff --git a/other/nightly/versions.json b/other/nightly/versions.json
index 2d1633ed6..2d96743f4 100644
--- a/other/nightly/versions.json
+++ b/other/nightly/versions.json
@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.376"
+ "version": "4.0.0-beta.378"
},
"nightly": {
- "version": "4.0.0-beta.377"
+ "version": "4.0.0-beta.379"
},
"helper": {
"version": "1.0.4"
diff --git a/resources/views/components/forms/monaco-editor.blade.php b/resources/views/components/forms/monaco-editor.blade.php
index d3793785b..a4bc88051 100644
--- a/resources/views/components/forms/monaco-editor.blade.php
+++ b/resources/views/components/forms/monaco-editor.blade.php
@@ -7,6 +7,9 @@
monacoLoader: true,
monacoFontSize: '15px',
monacoId: $id('monaco-editor'),
+ isDarkMode() {
+ return document.documentElement.classList.contains('dark') || localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
+ },
monacoEditor(editor) {
editor.onDidChangeModelContent((e) => {
this.monacoContent = editor.getValue();
@@ -41,357 +44,9 @@
let proxy = URL.createObjectURL(new Blob([` self.MonacoEnvironment = { baseUrl: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.39.0/min' }; importScripts('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.39.0/min/vs/base/worker/workerMain.min.js');`], { type: 'text/javascript' }));
window.MonacoEnvironment = { getWorkerUrl: () => proxy };
require(['vs/editor/editor.main'], () => {
- monaco.editor.defineTheme('blackboard', {
- 'base': 'vs-dark',
- 'inherit': true,
- 'rules': [{
- 'background': editorBackground,
- 'token': ''
- },
- {
- 'foreground': '959da5',
- 'token': 'comment'
- },
- {
- 'foreground': '959da5',
- 'token': 'punctuation.definition.comment'
- },
- {
- 'foreground': '959da5',
- 'token': 'string.comment'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'constant'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'entity.name.constant'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'variable.other.constant'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'variable.language'
- },
- {
- 'foreground': 'b392f0',
- 'token': 'entity'
- },
- {
- 'foreground': 'b392f0',
- 'token': 'entity.name'
- },
- {
- 'foreground': 'f6f8fa',
- 'token': 'variable.parameter.function'
- },
- {
- 'foreground': '7bcc72',
- 'token': 'entity.name.tag'
- },
- {
- 'foreground': 'ea4a5a',
- 'token': 'keyword'
- },
- {
- 'foreground': 'ea4a5a',
- 'token': 'storage'
- },
- {
- 'foreground': 'ea4a5a',
- 'token': 'storage.type'
- },
- {
- 'foreground': 'f6f8fa',
- 'token': 'storage.modifier.package'
- },
- {
- 'foreground': 'f6f8fa',
- 'token': 'storage.modifier.import'
- },
- {
- 'foreground': 'f6f8fa',
- 'token': 'storage.type.java'
- },
- {
- 'foreground': '79b8ff',
- 'token': 'string'
- },
- {
- 'foreground': '79b8ff',
- 'token': 'punctuation.definition.string'
- },
- {
- 'foreground': '79b8ff',
- 'token': 'string punctuation.section.embedded source'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'support'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'meta.property-name'
- },
- {
- 'foreground': 'fb8532',
- 'token': 'variable'
- },
- {
- 'foreground': 'f6f8fa',
- 'token': 'variable.other'
- },
- {
- 'foreground': 'd73a49',
- 'fontStyle': 'bold italic underline',
- 'token': 'invalid.broken'
- },
- {
- 'foreground': 'd73a49',
- 'fontStyle': 'bold italic underline',
- 'token': 'invalid.deprecated'
- },
- {
- 'foreground': 'fafbfc',
- 'background': 'd73a49',
- 'fontStyle': 'italic underline',
- 'token': 'invalid.illegal'
- },
- {
- 'foreground': 'fafbfc',
- 'background': 'd73a49',
- 'fontStyle': 'italic underline',
- 'token': 'carriage-return'
- },
- {
- 'foreground': 'd73a49',
- 'fontStyle': 'bold italic underline',
- 'token': 'invalid.unimplemented'
- },
- {
- 'foreground': 'd73a49',
- 'token': 'message.error'
- },
- {
- 'foreground': 'f6f8fa',
- 'token': 'string source'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'string variable'
- },
- {
- 'foreground': '79b8ff',
- 'token': 'source.regexp'
- },
- {
- 'foreground': '79b8ff',
- 'token': 'string.regexp'
- },
- {
- 'foreground': '79b8ff',
- 'token': 'string.regexp.character-class'
- },
- {
- 'foreground': '79b8ff',
- 'token': 'string.regexp constant.character.escape'
- },
- {
- 'foreground': '79b8ff',
- 'token': 'string.regexp source.ruby.embedded'
- },
- {
- 'foreground': '79b8ff',
- 'token': 'string.regexp string.regexp.arbitrary-repitition'
- },
- {
- 'foreground': '7bcc72',
- 'fontStyle': 'bold',
- 'token': 'string.regexp constant.character.escape'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'support.constant'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'support.variable'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'meta.module-reference'
- },
- {
- 'foreground': 'fb8532',
- 'token': 'markup.list'
- },
- {
- 'foreground': '0366d6',
- 'fontStyle': 'bold',
- 'token': 'markup.heading'
- },
- {
- 'foreground': '0366d6',
- 'fontStyle': 'bold',
- 'token': 'markup.heading entity.name'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'markup.quote'
- },
- {
- 'foreground': 'f6f8fa',
- 'fontStyle': 'italic',
- 'token': 'markup.italic'
- },
- {
- 'foreground': 'f6f8fa',
- 'fontStyle': 'bold',
- 'token': 'markup.bold'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'markup.raw'
- },
- {
- 'foreground': 'b31d28',
- 'background': 'ffeef0',
- 'token': 'markup.deleted'
- },
- {
- 'foreground': 'b31d28',
- 'background': 'ffeef0',
- 'token': 'meta.diff.header.from-file'
- },
- {
- 'foreground': 'b31d28',
- 'background': 'ffeef0',
- 'token': 'punctuation.definition.deleted'
- },
- {
- 'foreground': '176f2c',
- 'background': 'f0fff4',
- 'token': 'markup.inserted'
- },
- {
- 'foreground': '176f2c',
- 'background': 'f0fff4',
- 'token': 'meta.diff.header.to-file'
- },
- {
- 'foreground': '176f2c',
- 'background': 'f0fff4',
- 'token': 'punctuation.definition.inserted'
- },
- {
- 'foreground': 'b08800',
- 'background': 'fffdef',
- 'token': 'markup.changed'
- },
- {
- 'foreground': 'b08800',
- 'background': 'fffdef',
- 'token': 'punctuation.definition.changed'
- },
- {
- 'foreground': '2f363d',
- 'background': '959da5',
- 'token': 'markup.ignored'
- },
- {
- 'foreground': '2f363d',
- 'background': '959da5',
- 'token': 'markup.untracked'
- },
- {
- 'foreground': 'b392f0',
- 'fontStyle': 'bold',
- 'token': 'meta.diff.range'
- },
- {
- 'foreground': 'c8e1ff',
- 'token': 'meta.diff.header'
- },
- {
- 'foreground': '0366d6',
- 'fontStyle': 'bold',
- 'token': 'meta.separator'
- },
- {
- 'foreground': '0366d6',
- 'token': 'meta.output'
- },
- {
- 'foreground': 'ffeef0',
- 'token': 'brackethighlighter.tag'
- },
- {
- 'foreground': 'ffeef0',
- 'token': 'brackethighlighter.curly'
- },
- {
- 'foreground': 'ffeef0',
- 'token': 'brackethighlighter.round'
- },
- {
- 'foreground': 'ffeef0',
- 'token': 'brackethighlighter.square'
- },
- {
- 'foreground': 'ffeef0',
- 'token': 'brackethighlighter.angle'
- },
- {
- 'foreground': 'ffeef0',
- 'token': 'brackethighlighter.quote'
- },
- {
- 'foreground': 'd73a49',
- 'token': 'brackethighlighter.unmatched'
- },
- {
- 'foreground': 'd73a49',
- 'token': 'sublimelinter.mark.error'
- },
- {
- 'foreground': 'fb8532',
- 'token': 'sublimelinter.mark.warning'
- },
- {
- 'foreground': '6a737d',
- 'token': 'sublimelinter.gutter-mark'
- },
- {
- 'foreground': '79b8ff',
- 'fontStyle': 'underline',
- 'token': 'constant.other.reference.link'
- },
- {
- 'foreground': '79b8ff',
- 'fontStyle': 'underline',
- 'token': 'string.other.link'
- }
- ],
- 'colors': {
- 'editor.foreground': '#f6f8fa',
- 'editor.background': editorBackground,
- 'editor.selectionBackground': '#4c2889',
- 'editor.inactiveSelectionBackground': '#444d56',
- 'editor.lineHighlightBackground': '#444d56',
- 'editorCursor.foreground': '#ffffff',
- 'editorWhitespace.foreground': '#6a737d',
- 'editorIndentGuide.background': '#6a737d',
- 'editorIndentGuide.activeBackground': '#f6f8fa',
- 'editor.selectionHighlightBorder': '#444d56'
- }
- });
-
const editor = monaco.editor.create($refs.monacoEditorElement, {
value: monacoContent,
- theme: editorTheme,
+ theme: document.documentElement.classList.contains('dark') ? 'vs-dark' : 'vs',
wordWrap: 'on',
readOnly: '{{ $readonly ?? false }}',
minimap: { enabled: false },
@@ -399,7 +54,20 @@
lineNumbersMinChars: 3,
automaticLayout: true,
language: '{{ $language }}'
+ });
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ if (mutation.attributeName === 'class') {
+ const isDark = document.documentElement.classList.contains('dark');
+ monaco.editor.setTheme(isDark ? 'vs-dark' : 'vs');
+ }
+ });
+ });
+
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['class']
});
monacoEditor(editor);
@@ -411,7 +79,6 @@
updatePlaceholder(editor.getValue());
- // Watch for changes in monacoContent from Livewire
$watch('monacoContent', value => {
if (editor.getValue() !== value) {
editor.setValue(value);
diff --git a/resources/views/livewire/project/service/navbar.blade.php b/resources/views/livewire/project/service/navbar.blade.php
index 342c071d4..f268096f8 100644
--- a/resources/views/livewire/project/service/navbar.blade.php
+++ b/resources/views/livewire/project/service/navbar.blade.php
@@ -22,7 +22,7 @@
@if ($service->isDeployable)