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)
- @if (str($service->status())->contains('running')) + @if (str($service->status)->contains('running')) Advanced @@ -70,7 +70,7 @@ Stop - @elseif (str($service->status())->contains('degraded')) + @elseif (str($service->status)->contains('degraded'))