diff --git a/.env.development.example b/.env.development.example index 43d3a10fd..c956daafd 100644 --- a/.env.development.example +++ b/.env.development.example @@ -6,6 +6,7 @@ USERID= GROUPID= ############################################################################################################ +APP_NAME=Coolify-localhost APP_ID=development APP_ENV=local APP_KEY= diff --git a/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml new file mode 100644 index 000000000..ef7f5b217 --- /dev/null +++ b/.github/workflows/coolify-helper-next.yml @@ -0,0 +1,84 @@ +name: Coolify Helper Image Development (v4) + +on: + push: + branches: [ "next" ] + paths: + - .github/workflows/coolify-helper.yml + - docker/coolify-helper/Dockerfile + +env: + REGISTRY: ghcr.io + IMAGE_NAME: "coollabsio/coolify-helper" + +jobs: + amd64: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v3 + - name: Login to ghcr.io + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build image and push to registry + uses: docker/build-push-action@v3 + with: + no-cache: true + context: . + file: docker/coolify-helper/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next + aarch64: + runs-on: [ self-hosted, arm64 ] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v3 + - name: Login to ghcr.io + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build image and push to registry + uses: docker/build-push-action@v3 + with: + no-cache: true + context: . + file: docker/coolify-helper/Dockerfile + platforms: linux/aarch64 + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [ amd64, aarch64 ] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to ghcr.io + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Create & publish manifest + run: | + docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index 6a774effc..6962d1dbf 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -2,7 +2,7 @@ name: Coolify Helper Image (v4) on: push: - branches: [ "main", "next" ] + branches: [ "main" ] paths: - .github/workflows/coolify-helper.yml - docker/coolify-helper/Dockerfile @@ -55,7 +55,7 @@ jobs: file: docker/coolify-helper/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64 + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 merge-manifest: runs-on: ubuntu-latest permissions: @@ -78,3 +78,7 @@ jobs: - name: Create & publish manifest run: | docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/.github/workflows/development-build.yml b/.github/workflows/development-build.yml index 7011fb1cc..681cbda3a 100644 --- a/.github/workflows/development-build.yml +++ b/.github/workflows/development-build.yml @@ -3,6 +3,9 @@ name: Development Build (v4) on: push: branches: ["next"] + paths-ignore: + - .github/workflows/coolify-helper.yml + - docker/coolify-helper/Dockerfile env: REGISTRY: ghcr.io @@ -73,4 +76,4 @@ jobs: - uses: sarisia/actions-status-discord@v1 if: always() with: - webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} \ No newline at end of file + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index ba427a93d..c2d7e8a80 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -65,7 +65,7 @@ class StartPostgresql ], 'networks' => [ $this->database->destination->network => [ - 'external' => false, + 'external' => true, 'name' => $this->database->destination->network, 'attachable' => true, ] diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index a7408b2cc..a85981c6b 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -10,8 +10,14 @@ class InstallDocker { public function __invoke(Server $server, Team $team) { - $dockerVersion = '23.0'; - $config = base64_encode('{ "live-restore": true }'); + $dockerVersion = '24.0'; + $config = base64_encode('{ + "log-driver": "json-file", + "log-opts": { + "max-size": "10m", + "max-file": "3" + } + }'); if (isDev()) { $activity = remote_process([ "echo ####### Installing Prerequisites...", diff --git a/app/Console/Commands/InviteFromWaitlist.php b/app/Console/Commands/WaitlistInvite.php similarity index 69% rename from app/Console/Commands/InviteFromWaitlist.php rename to app/Console/Commands/WaitlistInvite.php index 2794b7441..a3b47089a 100644 --- a/app/Console/Commands/InviteFromWaitlist.php +++ b/app/Console/Commands/WaitlistInvite.php @@ -6,20 +6,20 @@ use App\Models\User; use App\Models\Waitlist; use Illuminate\Console\Command; use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; -class InviteFromWaitlist extends Command +class WaitlistInvite extends Command { - public Waitlist|null $next_patient = null; - public User|null $new_user = null; + public Waitlist|User|null $next_patient = null; public string|null $password = null; /** * The name and signature of the console command. * * @var string */ - protected $signature = 'app:invite-from-waitlist {email?}'; + protected $signature = 'waitlist:invite {email?} {--only-email}'; /** * The console command description. @@ -34,7 +34,16 @@ class InviteFromWaitlist extends Command public function handle() { if ($this->argument('email')) { - $this->next_patient = Waitlist::where('email', $this->argument('email'))->first(); + if ($this->option('only-email')) { + $this->next_patient = User::whereEmail($this->argument('email'))->first(); + $this->password = Str::password(); + $this->next_patient->update([ + 'password' => Hash::make($this->password), + 'force_password_reset' => true, + ]); + } else { + $this->next_patient = Waitlist::where('email', $this->argument('email'))->first(); + } if (!$this->next_patient) { $this->error("{$this->argument('email')} not found in the waitlist."); return; @@ -43,6 +52,10 @@ class InviteFromWaitlist extends Command $this->next_patient = Waitlist::orderBy('created_at', 'asc')->where('verified', true)->first(); } if ($this->next_patient) { + if ($this->option('only-email')) { + $this->send_email(); + return; + } $this->register_user(); $this->remove_from_waitlist(); $this->send_email(); @@ -55,7 +68,7 @@ class InviteFromWaitlist extends Command $already_registered = User::whereEmail($this->next_patient->email)->first(); if (!$already_registered) { $this->password = Str::password(); - $this->new_user = User::create([ + User::create([ 'name' => Str::of($this->next_patient->email)->before('@'), 'email' => $this->next_patient->email, 'password' => Hash::make($this->password), @@ -73,10 +86,14 @@ class InviteFromWaitlist extends Command } private function send_email() { + ray($this->next_patient->email, $this->password); + $token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password"); + $loginLink = route('auth.link', ['token' => $token]); $mail = new MailMessage(); $mail->view('emails.waitlist-invitation', [ 'email' => $this->next_patient->email, 'password' => $this->password, + 'loginLink' => $loginLink, ]); $mail->subject('Congratulations! You are invited to join Coolify Cloud.'); send_user_an_email($mail, $this->next_patient->email); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 07032e1a1..dbe48200d 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,14 +2,19 @@ namespace App\Console; +use App\Jobs\ApplicationContainerStatusJob; use App\Jobs\CheckResaleLicenseJob; use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\DatabaseBackupJob; +use App\Jobs\DatabaseContainerStatusJob; use App\Jobs\DockerCleanupJob; use App\Jobs\InstanceAutoUpdateJob; use App\Jobs\ProxyCheckJob; use App\Jobs\ResourceStatusJob; +use App\Models\Application; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; +use App\Models\StandalonePostgresql; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -17,28 +22,46 @@ class Kernel extends ConsoleKernel { protected function schedule(Schedule $schedule): void { - // $schedule->call(fn() => $this->check_scheduled_backups($schedule))->everyTenSeconds(); if (isDev()) { $schedule->command('horizon:snapshot')->everyMinute(); - $schedule->job(new ResourceStatusJob)->everyMinute(); + // $schedule->job(new ResourceStatusJob)->everyMinute(); $schedule->job(new ProxyCheckJob)->everyFiveMinutes(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute(); - // $schedule->job(new CheckResaleLicenseJob)->hourly(); $schedule->job(new DockerCleanupJob)->everyOddHour(); - // $schedule->job(new InstanceAutoUpdateJob(true))->everyMinute(); } else { $schedule->command('horizon:snapshot')->everyFiveMinutes(); - $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); - $schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer(); + $schedule->job(new CleanupInstanceStuffsJob)->everyTenMinutes()->onOneServer(); + // $schedule->job(new ResourceStatusJob)->everyMinute()->onOneServer(); $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); $schedule->job(new ProxyCheckJob)->everyFiveMinutes()->onOneServer(); $schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer(); - $schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes(); } + $this->instance_auto_update($schedule); $this->check_scheduled_backups($schedule); + $this->check_resources($schedule); } + private function check_resources($schedule) + { + $applications = Application::all(); + foreach ($applications as $application) { + $schedule->job(new ApplicationContainerStatusJob($application))->everyMinute()->onOneServer(); + } + $postgresqls = StandalonePostgresql::all(); + foreach ($postgresqls as $postgresql) { + $schedule->job(new DatabaseContainerStatusJob($postgresql))->everyMinute()->onOneServer(); + } + } + private function instance_auto_update($schedule){ + if (isDev()) { + return; + } + $settings = InstanceSettings::get(); + if ($settings->is_auto_update_enabled) { + $schedule->job(new InstanceAutoUpdateJob)->everyTenMinutes()->onOneServer(); + } + } private function check_scheduled_backups($schedule) { ray('check_scheduled_backups'); @@ -57,7 +80,7 @@ class Kernel extends ConsoleKernel } $schedule->job(new DatabaseBackupJob( backup: $scheduled_backup - ))->cron($scheduled_backup->frequency); + ))->cron($scheduled_backup->frequency)->onOneServer(); } } diff --git a/app/Enums/ProxyTypes.php b/app/Enums/ProxyTypes.php index dfbf65c64..5ccf82e21 100644 --- a/app/Enums/ProxyTypes.php +++ b/app/Enums/ProxyTypes.php @@ -4,6 +4,7 @@ namespace App\Enums; enum ProxyTypes: string { + case NONE = 'NONE'; case TRAEFIK_V2 = 'TRAEFIK_V2'; case NGINX = 'NGINX'; case CADDY = 'CADDY'; diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 39b0881e3..29d705faf 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -8,15 +8,41 @@ use App\Models\S3Storage; use App\Models\StandalonePostgresql; use App\Models\TeamInvitation; use App\Models\User; +use Auth; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Routing\Controller as BaseController; +use Illuminate\Support\Facades\Crypt; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Http; use Throwable; +use Str; + class Controller extends BaseController { use AuthorizesRequests, ValidatesRequests; + public function link() + { + $token = request()->get('token'); + if ($token) { + $decrypted = Crypt::decryptString($token); + $email = Str::of($decrypted)->before('@@@'); + $password = Str::of($decrypted)->after('@@@'); + $user = User::whereEmail($email)->first(); + if (!$user) { + return redirect()->route('login'); + } + if (Hash::check($password, $user->password)) { + Auth::login($user); + $team = $user->teams()->first(); + session(['currentTeam' => $team]); + return redirect()->route('dashboard'); + } + } + return redirect()->route('login')->with('error', 'Invalid credentials.'); + } public function subscription() { if (!isCloud()) { @@ -37,10 +63,12 @@ class Controller extends BaseController ]); } - public function force_passoword_reset() { + public function force_passoword_reset() + { return view('auth.force-password-reset'); } - public function boarding() { + public function boarding() + { if (currentTeam()->boarding || isDev()) { return view('boarding'); } else { diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 07e4a2a0f..579a6b167 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -60,9 +60,6 @@ class ProjectController extends Controller 'environment_name' => $environment->name, 'database_uuid' => $standalone_postgresql->uuid, ]); - } - if ($server) { - } return view('project.new', [ 'type' => $type diff --git a/app/Http/Livewire/Dashboard.php b/app/Http/Livewire/Dashboard.php index 874e389e0..7c0505d8a 100644 --- a/app/Http/Livewire/Dashboard.php +++ b/app/Http/Livewire/Dashboard.php @@ -25,6 +25,15 @@ class Dashboard extends Component } $this->projects = $projects->count(); } + // public function getIptables() + // { + // $servers = Server::ownedByCurrentTeam()->get(); + // foreach ($servers as $server) { + // checkRequiredCommands($server); + // $iptables = instant_remote_process(['docker run --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c "iptables -L -n | jc --iptables"'], $server); + // ray($iptables); + // } + // } public function render() { return view('livewire.dashboard'); diff --git a/app/Http/Livewire/ForcePasswordReset.php b/app/Http/Livewire/ForcePasswordReset.php index 96a273e80..df2b37691 100644 --- a/app/Http/Livewire/ForcePasswordReset.php +++ b/app/Http/Livewire/ForcePasswordReset.php @@ -18,22 +18,26 @@ class ForcePasswordReset extends Component 'password' => 'required|min:8', 'password_confirmation' => 'required|same:password', ]; - public function mount() { + public function mount() + { $this->email = auth()->user()->email; } - public function submit() { + public function submit() + { try { $this->rateLimit(10); $this->validate(); + $firstLogin = auth()->user()->created_at == auth()->user()->updated_at; auth()->user()->forceFill([ 'password' => Hash::make($this->password), 'force_password_reset' => false, ])->save(); - auth()->logout(); - return redirect()->route('login')->with('status', 'Your initial password has been set.'); - } catch(\Exception $e) { - return general_error_handler(err:$e, that:$this); + if ($firstLogin) { + send_internal_notification('First login for ' . auth()->user()->email); + } + return redirect()->route('dashboard'); + } catch (\Exception $e) { + return general_error_handler(err: $e, that: $this); } } - } diff --git a/app/Http/Livewire/Help.php b/app/Http/Livewire/Help.php new file mode 100644 index 000000000..107821501 --- /dev/null +++ b/app/Http/Livewire/Help.php @@ -0,0 +1,54 @@ + 'required|min:10', + 'subject' => 'required|min:3' + ]; + public function mount() + { + $this->path = Route::current()->uri(); + if (isDev()) { + $this->description = "I'm having trouble with {$this->path}"; + $this->subject = "Help with {$this->path}"; + } + } + public function submit() + { + try { + $this->rateLimit(1, 60); + $this->validate(); + $subscriptionType = auth()->user()?->subscription?->type() ?? 'unknown'; + $debug = "Route: {$this->path}"; + $mail = new MailMessage(); + $mail->view( + 'emails.help', + [ + 'description' => $this->description, + 'debug' => $debug + ] + ); + $mail->subject("[HELP - {$subscriptionType}]: {$this->subject}"); + send_user_an_email($mail, 'hi@coollabs.io'); + $this->emit('success', 'Your message has been sent successfully. We will get in touch with you as soon as possible.'); + } catch (\Exception $e) { + return general_error_handler($e, $this); + } + } + public function render() + { + return view('livewire.help')->layout('layouts.app'); + } +} diff --git a/app/Http/Livewire/Notifications/DiscordSettings.php b/app/Http/Livewire/Notifications/DiscordSettings.php index f9de6d662..cd7a413a5 100644 --- a/app/Http/Livewire/Notifications/DiscordSettings.php +++ b/app/Http/Livewire/Notifications/DiscordSettings.php @@ -8,26 +8,30 @@ use Livewire\Component; class DiscordSettings extends Component { - public Team $model; + public Team $team; protected $rules = [ - 'model.discord_enabled' => 'nullable|boolean', - 'model.discord_webhook_url' => 'required|url', - 'model.discord_notifications_test' => 'nullable|boolean', - 'model.discord_notifications_deployments' => 'nullable|boolean', - 'model.discord_notifications_status_changes' => 'nullable|boolean', - 'model.discord_notifications_database_backups' => 'nullable|boolean', + 'team.discord_enabled' => 'nullable|boolean', + 'team.discord_webhook_url' => 'required|url', + 'team.discord_notifications_test' => 'nullable|boolean', + 'team.discord_notifications_deployments' => 'nullable|boolean', + 'team.discord_notifications_status_changes' => 'nullable|boolean', + 'team.discord_notifications_database_backups' => 'nullable|boolean', ]; protected $validationAttributes = [ - 'model.discord_webhook_url' => 'Discord Webhook', + 'team.discord_webhook_url' => 'Discord Webhook', ]; + public function mount() + { + $this->team = auth()->user()->currentTeam(); + } public function instantSave() { try { $this->submit(); } catch (\Exception $e) { ray($e->getMessage()); - $this->model->discord_enabled = false; + $this->team->discord_enabled = false; $this->validate(); } } @@ -41,8 +45,8 @@ class DiscordSettings extends Component public function saveModel() { - $this->model->save(); - if (is_a($this->model, Team::class)) { + $this->team->save(); + if (is_a($this->team, Team::class)) { refreshSession(); } $this->emit('success', 'Settings saved.'); @@ -50,7 +54,7 @@ class DiscordSettings extends Component public function sendTestNotification() { - $this->model->notify(new Test); + $this->team->notify(new Test()); $this->emit('success', 'Test notification sent.'); } } diff --git a/app/Http/Livewire/Notifications/EmailSettings.php b/app/Http/Livewire/Notifications/EmailSettings.php index bf805e5ec..4ddfaf87f 100644 --- a/app/Http/Livewire/Notifications/EmailSettings.php +++ b/app/Http/Livewire/Notifications/EmailSettings.php @@ -49,6 +49,7 @@ class EmailSettings extends Component public function mount() { + $this->team = auth()->user()->currentTeam(); ['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits; $this->emails = auth()->user()->email; } diff --git a/app/Http/Livewire/Notifications/TelegramSettings.php b/app/Http/Livewire/Notifications/TelegramSettings.php new file mode 100644 index 000000000..be7081012 --- /dev/null +++ b/app/Http/Livewire/Notifications/TelegramSettings.php @@ -0,0 +1,62 @@ + 'nullable|boolean', + 'team.telegram_token' => 'required|string', + 'team.telegram_chat_id' => 'required|string', + 'team.telegram_notifications_test' => 'nullable|boolean', + 'team.telegram_notifications_deployments' => 'nullable|boolean', + 'team.telegram_notifications_status_changes' => 'nullable|boolean', + 'team.telegram_notifications_database_backups' => 'nullable|boolean', + ]; + protected $validationAttributes = [ + 'team.telegram_token' => 'Token', + 'team.telegram_chat_id' => 'Chat ID', + ]; + + public function mount() + { + $this->team = auth()->user()->currentTeam(); + } + public function instantSave() + { + try { + $this->submit(); + } catch (\Exception $e) { + ray($e->getMessage()); + $this->team->telegram_enabled = false; + $this->validate(); + } + } + + public function submit() + { + $this->resetErrorBag(); + $this->validate(); + $this->saveModel(); + } + + public function saveModel() + { + $this->team->save(); + if (is_a($this->team, Team::class)) { + refreshSession(); + } + $this->emit('success', 'Settings saved.'); + } + + public function sendTestNotification() + { + $this->team->notify(new Test()); + $this->emit('success', 'Test notification sent.'); + } +} diff --git a/app/Http/Livewire/PrivateKey/Change.php b/app/Http/Livewire/PrivateKey/Change.php index bd9332ca2..528c44506 100644 --- a/app/Http/Livewire/PrivateKey/Change.php +++ b/app/Http/Livewire/PrivateKey/Change.php @@ -27,7 +27,7 @@ class Change extends Component if ($this->private_key->isEmpty()) { $this->private_key->delete(); currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get(); - return redirect()->route('private-key.all'); + return redirect()->route('security.private-key.index'); } $this->emit('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.'); } catch (\Exception $e) { diff --git a/app/Http/Livewire/Project/New/Select.php b/app/Http/Livewire/Project/New/Select.php index c6ec91bb3..0ea7fd8c1 100644 --- a/app/Http/Livewire/Project/New/Select.php +++ b/app/Http/Livewire/Project/New/Select.php @@ -22,34 +22,52 @@ class Select extends Component public Collection|array $swarmDockers = []; public array $parameters; + public ?string $existingPostgresqlUrl = null; + protected $queryString = [ 'server', ]; public function mount() { $this->parameters = get_route_parameters(); + if (isDev()) { + $this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432'; + } } - public function set_type(string $type) + // public function addExistingPostgresql() + // { + // try { + // instantCommand("psql {$this->existingPostgresqlUrl} -c 'SELECT 1'"); + // $this->emit('success', 'Successfully connected to the database.'); + // } catch (\Exception $e) { + // return general_error_handler($e, $this); + // } + // } + public function setType(string $type) { $this->type = $type; + if ($type === "existing-postgresql") { + $this->current_step = $type; + return; + } if (count($this->servers) === 1) { $server = $this->servers->first(); - $this->set_server($server); + $this->setServer($server); if (count($server->destinations()) === 1) { - $this->set_destination($server->destinations()->first()->uuid); + $this->setDestination($server->destinations()->first()->uuid); } } if (!is_null($this->server)) { $foundServer = $this->servers->where('id', $this->server)->first(); if ($foundServer) { - return $this->set_server($foundServer); + return $this->setServer($foundServer); } } $this->current_step = 'servers'; } - public function set_server(Server $server) + public function setServer(Server $server) { $this->server_id = $server->id; $this->standaloneDockers = $server->standaloneDockers; @@ -57,7 +75,7 @@ class Select extends Component $this->current_step = 'destinations'; } - public function set_destination(string $destination_uuid) + public function setDestination(string $destination_uuid) { $this->destination_uuid = $destination_uuid; redirect()->route('project.resources.new', [ diff --git a/app/Http/Livewire/Server/New/ByIp.php b/app/Http/Livewire/Server/New/ByIp.php index e6813a55f..7b3297995 100644 --- a/app/Http/Livewire/Server/New/ByIp.php +++ b/app/Http/Livewire/Server/New/ByIp.php @@ -2,6 +2,8 @@ namespace App\Http\Livewire\Server\New; +use App\Enums\ProxyStatus; +use App\Enums\ProxyTypes; use App\Models\Server; use Livewire\Component; @@ -67,6 +69,11 @@ class ByIp extends Component 'port' => $this->port, 'team_id' => currentTeam()->id, 'private_key_id' => $this->private_key_id, + 'proxy' => [ + "type" => ProxyTypes::TRAEFIK_V2->value, + "status" => ProxyStatus::EXITED->value, + ] + ]); $server->settings->is_part_of_swarm = $this->is_part_of_swarm; $server->settings->save(); diff --git a/app/Http/Livewire/Server/Proxy.php b/app/Http/Livewire/Server/Proxy.php index 944b5e898..2e8530e1c 100644 --- a/app/Http/Livewire/Server/Proxy.php +++ b/app/Http/Livewire/Server/Proxy.php @@ -12,7 +12,7 @@ class Proxy extends Component { public Server $server; - public ProxyTypes $selectedProxy = ProxyTypes::TRAEFIK_V2; + public ?string $selectedProxy = null; public $proxy_settings = null; public string|null $redirect_url = null; @@ -20,6 +20,7 @@ class Proxy extends Component public function mount() { + $this->selectedProxy = $this->server->proxy->type; $this->redirect_url = $this->server->proxy->redirect_url; } @@ -35,11 +36,12 @@ class Proxy extends Component $this->emit('proxyStatusUpdated'); } - public function select_proxy(ProxyTypes $proxy_type) + public function select_proxy($proxy_type) { $this->server->proxy->type = $proxy_type; $this->server->proxy->status = 'exited'; $this->server->save(); + $this->selectedProxy = $this->server->proxy->type; $this->emit('proxyStatusUpdated'); } diff --git a/app/Http/Livewire/Source/Github/Create.php b/app/Http/Livewire/Source/Github/Create.php index b5487bcc8..c607af451 100644 --- a/app/Http/Livewire/Source/Github/Create.php +++ b/app/Http/Livewire/Source/Github/Create.php @@ -32,16 +32,19 @@ class Create extends Component "custom_port" => 'required|int', "is_system_wide" => 'required|bool', ]); - $github_app = GithubApp::create([ + $payload = [ 'name' => $this->name, 'organization' => $this->organization, 'api_url' => $this->api_url, 'html_url' => $this->html_url, 'custom_user' => $this->custom_user, 'custom_port' => $this->custom_port, - 'is_system_wide' => $this->is_system_wide, 'team_id' => currentTeam()->id, - ]); + ]; + if (isCloud()) { + $payload['is_system_wide'] = $this->is_system_wide; + } + $github_app = GithubApp::create($payload); if (session('from')) { session(['from' => session('from') + ['source_id' => $github_app->id]]); } diff --git a/app/Http/Livewire/Subscription/PricingPlans.php b/app/Http/Livewire/Subscription/PricingPlans.php index 7d77f68c4..80cc81dcf 100644 --- a/app/Http/Livewire/Subscription/PricingPlans.php +++ b/app/Http/Livewire/Subscription/PricingPlans.php @@ -47,6 +47,9 @@ class PricingPlans extends Component 'tax_id_collection' => [ 'enabled' => true, ], + 'automatic_tax' => [ + 'enabled' => true, + ], 'mode' => 'subscription', 'success_url' => route('dashboard', ['success' => true]), 'cancel_url' => route('subscription.index', ['cancelled' => true]), diff --git a/app/Http/Middleware/CheckForcePasswordReset.php b/app/Http/Middleware/CheckForcePasswordReset.php index 8d1670de5..e8129deda 100644 --- a/app/Http/Middleware/CheckForcePasswordReset.php +++ b/app/Http/Middleware/CheckForcePasswordReset.php @@ -16,6 +16,12 @@ class CheckForcePasswordReset public function handle(Request $request, Closure $next): Response { if (auth()->user()) { + if ($request->path() === 'auth/link') { + auth()->logout(); + request()->session()->invalidate(); + request()->session()->regenerateToken(); + return $next($request); + } $force_password_reset = auth()->user()->force_password_reset; if ($force_password_reset) { if ($request->routeIs('auth.force-password-reset') || $request->path() === 'livewire/message/force-password-reset') { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 715b22b91..d5431b0e8 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -66,12 +66,7 @@ class ApplicationDeploymentJob implements ShouldQueue private $log_model; private Collection $saved_outputs; - public function middleware(): array - { - return [ - (new WithoutOverlapping("dockerimagejobs"))->shared(), - ]; - } + public $tries = 1; public function __construct(int $application_deployment_queue_id) { ray()->clearScreen(); @@ -181,8 +176,8 @@ class ApplicationDeploymentJob implements ShouldQueue $this->execute_in_builder("echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile") ], ); - $this->build_image_name = "{$this->application->git_repository}:build"; - $this->production_image_name = "{$this->application->uuid}:latest"; + $this->build_image_name = Str::lower("{$this->application->git_repository}:build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); $this->generate_compose_file(); $this->generate_build_env_variables(); @@ -206,8 +201,8 @@ class ApplicationDeploymentJob implements ShouldQueue $tag = $tag->substr(0, 128); } - $this->build_image_name = "{$this->application->git_repository}:{$tag}-build"; - $this->production_image_name = "{$this->application->uuid}:{$tag}"; + $this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}"); ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); if (!$this->force_rebuild) { @@ -242,7 +237,7 @@ class ApplicationDeploymentJob implements ShouldQueue } private function health_check() { - ray('New container name: ',$this->container_name); + ray('New container name: ', $this->container_name); if ($this->container_name) { $counter = 0; $this->execute_remote_command( @@ -264,7 +259,7 @@ class ApplicationDeploymentJob implements ShouldQueue ); $this->execute_remote_command( [ - "echo 'New application version health check status: {$this->saved_outputs->get('health_check')}'" + "echo 'New version health check status: {$this->saved_outputs->get('health_check')}'" ], ); if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) { @@ -282,8 +277,8 @@ class ApplicationDeploymentJob implements ShouldQueue } private function deploy_pull_request() { - $this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build"; - $this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}"; + $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); + $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); $this->execute_remote_command([ "echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'", @@ -304,12 +299,19 @@ class ApplicationDeploymentJob implements ShouldQueue private function prepare_builder_image() { + $pull = "--pull=always"; + if (isDev()) { + $pull = "--pull=never"; + } + $helperImage = config('coolify.helper_image'); + $runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; + $this->execute_remote_command( [ - "echo -n 'Pulling latest version of the builder image (ghcr.io/coollabsio/coolify-helper).'", + "echo -n 'Pulling helper image from $helperImage.'", ], [ - "docker run --pull=always -d --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/coollabsio/coolify-helper", + $runCommand, "hidden" => true, ], [ @@ -494,7 +496,7 @@ class ApplicationDeploymentJob implements ShouldQueue ], 'networks' => [ $this->destination->network => [ - 'external' => false, + 'external' => true, 'name' => $this->destination->network, 'attachable' => true, ] @@ -654,12 +656,12 @@ class ApplicationDeploymentJob implements ShouldQueue private function build_image() { $this->execute_remote_command([ - "echo -n 'Building docker image.'", + "echo -n 'Building docker image for your application.'", ]); if ($this->application->settings->is_static) { $this->execute_remote_command([ - $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true + $this->execute_in_builder("docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true ]); $dockerfile = base64_encode("FROM {$this->application->static_image} @@ -692,12 +694,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->execute_in_builder("echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf") ], [ - $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true + $this->execute_in_builder("docker build --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true ] ); } else { $this->execute_remote_command([ - $this->execute_in_builder("docker build -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true + $this->execute_in_builder("docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true ]); } } @@ -706,7 +708,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); { if ($this->currently_running_container_name) { $this->execute_remote_command( - ["echo -n 'Removing old application version.'"], + ["echo -n 'Removing old version of your application.'"], [$this->execute_in_builder("docker rm -f $this->currently_running_container_name >/dev/null 2>&1"), "hidden" => true], ); } diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 6e1ee2f0c..a6e0926f1 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -77,7 +77,7 @@ class DatabaseBackupJob implements ShouldQueue $ip = Str::slug($this->server->ip); $this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip"; } - $this->backup_file = "/dumpall-" . Carbon::now()->timestamp . ".sql"; + $this->backup_file = "/pg_dump-" . Carbon::now()->timestamp . ".dump"; $this->backup_location = $this->backup_dir . $this->backup_file; $this->backup_log = ScheduledDatabaseBackupExecution::create([ @@ -107,7 +107,7 @@ class DatabaseBackupJob implements ShouldQueue try { ray($this->backup_dir); $commands[] = "mkdir -p " . $this->backup_dir; - $commands[] = "docker exec $this->container_name pg_dumpall -U {$this->database->postgres_user} > $this->backup_location"; + $commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location"; $this->backup_output = instant_remote_process($commands, $this->server); diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 4456eee5e..ecdf6dbcb 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Models\ApplicationDeploymentQueue; use App\Models\Server; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -9,7 +10,6 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Str; class DockerCleanupJob implements ShouldQueue { @@ -30,6 +30,11 @@ class DockerCleanupJob implements ShouldQueue } public function handle(): void { + $queue = ApplicationDeploymentQueue::where('status', '==', 'in_progress')->get(); + if ($queue->count() > 0) { + ray('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping')->color('orange'); + return; + } try { ray()->showQueries()->color('orange'); $servers = Server::all(); diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php new file mode 100644 index 000000000..08afbab23 --- /dev/null +++ b/app/Jobs/SendMessageToTelegramJob.php @@ -0,0 +1,72 @@ +token . '/sendMessage'; + $inlineButtons = []; + if (!empty($this->buttons)) { + foreach ($this->buttons as $button) { + $buttonUrl = data_get($button, 'url'); + if ($buttonUrl && Str::contains($buttonUrl, 'http://localhost')) { + $buttonUrl = str_replace('http://localhost', config('app.url'), $buttonUrl); + } + $inlineButtons[] = [ + 'text' => $button['text'], + 'url' => $buttonUrl, + ]; + } + } + $payload = [ + 'parse_mode' => 'markdown', + 'reply_markup' => json_encode([ + 'inline_keyboard' => [ + [...$inlineButtons], + ], + ]), + 'chat_id' => $this->chatId, + 'text' => $this->text, + ]; + ray($payload); + $response = Http::post($url, $payload); + if ($response->failed()) { + throw new \Exception('Telegram notification failed with ' . $response->status() . ' status code.' . $response->body()); + } + } +} diff --git a/app/Models/Team.php b/app/Models/Team.php index b75772569..2d7ef3c9e 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -24,6 +24,14 @@ class Team extends Model implements SendsDiscord, SendsEmail return data_get($this, 'discord_webhook_url', null); } + public function routeNotificationForTelegram() + { + return [ + "token" => data_get($this, 'telegram_token', null), + "chat_id" => data_get($this, 'telegram_chat_id', null) + ]; + } + public function getRecepients($notification) { $recipients = data_get($notification, 'emails', null); diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index b244f4be8..ddbdf1446 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -43,19 +43,7 @@ class DeploymentFailed extends Notification implements ShouldQueue public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_deployments'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_deployments'); - - if ($isEmailEnabled && $isSubscribedToEmailEvent) { - $channels[] = EmailChannel::class; - } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'deployments'); } public function toMail(): MailMessage @@ -89,4 +77,19 @@ class DeploymentFailed extends Notification implements ShouldQueue } return $message; } + public function toTelegram(): array + { + if ($this->preview) { + $message = '❌ Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: '; + } else { + $message = '❌ Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): '; + } + return [ + "message" => $message, + "buttons" => [ + "text" => "View Deployment Logs", + "url" => $this->deployment_url + ], + ]; + } } diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 99bd2532c..f46c44c0e 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -43,19 +43,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = data_get($notifiable, 'smtp_enabled'); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_deployments'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_deployments'); - - if ($isEmailEnabled && $isSubscribedToEmailEvent) { - $channels[] = EmailChannel::class; - } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'deployments'); } public function toMail(): MailMessage @@ -99,4 +87,34 @@ class DeploymentSuccess extends Notification implements ShouldQueue } return $message; } + public function toTelegram(): array + { + if ($this->preview) { + $message = '✅ New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . ''; + if ($this->preview->fqdn) { + $buttons[] = [ + "text" => "Open Application", + "url" => $this->preview->fqdn + ]; + } + } else { + $message = '✅ New version successfully deployed of ' . $this->application_name . ''; + if ($this->fqdn) { + $buttons[] = [ + "text" => "Open Application", + "url" => $this->fqdn + ]; + } + } + $buttons[] = [ + "text" => "Deployment logs", + "url" => $this->deployment_url + ]; + return [ + "message" => $message, + "buttons" => [ + ...$buttons + ], + ]; + } } diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index 9c6b99fc7..0d01d08ab 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -37,19 +37,7 @@ class StatusChanged extends Notification implements ShouldQueue public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = data_get($notifiable, 'smtp_enabled'); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes'); - - if ($isEmailEnabled && $isSubscribedToEmailEvent) { - $channels[] = EmailChannel::class; - } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'status_changes'); } public function toMail(): MailMessage @@ -70,7 +58,20 @@ class StatusChanged extends Notification implements ShouldQueue $message = '⛔ ' . $this->application_name . ' has been stopped. '; - $message .= '[Application URL](' . $this->application_url . ')'; + $message .= '[Open Application in Coolify](' . $this->application_url . ')'; return $message; } + public function toTelegram(): array + { + $message = '⛔ ' . $this->application_name . ' has been stopped.'; + return [ + "message" => $message, + "buttons" => [ + [ + "text" => "Open Application in Coolify", + "url" => $this->application_url + ] + ], + ]; + } } diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 0c042fcfc..e0f9edac0 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -9,7 +9,6 @@ use Illuminate\Support\Facades\Mail; class EmailChannel { - private bool $isResend = false; public function send(SendsEmail $notifiable, Notification $notification): void { $this->bootConfigs($notifiable); @@ -20,33 +19,14 @@ class EmailChannel } $mailMessage = $notification->toMail($notifiable); - // if ($this->isResend) { Mail::send( [], [], fn (Message $message) => $message - ->from( - data_get($notifiable, 'smtp_from_address'), - data_get($notifiable, 'smtp_from_name'), - ) ->to($recepients) ->subject($mailMessage->subject) ->html((string)$mailMessage->render()) ); - // } else { - // Mail::send( - // [], - // [], - // fn (Message $message) => $message - // ->from( - // data_get($notifiable, 'smtp_from_address'), - // data_get($notifiable, 'smtp_from_name'), - // ) - // ->bcc($recepients) - // ->subject($mailMessage->subject) - // ->html((string)$mailMessage->render()) - // ); - // } } private function bootConfigs($notifiable): void @@ -56,13 +36,11 @@ class EmailChannel if (!$type) { throw new Exception('No email settings found.'); } - if ($type === 'resend') { - $this->isResend = true; - } return; } + config()->set('mail.from.address', data_get($notifiable, 'smtp_from_address')); + config()->set('mail.from.name', data_get($notifiable, 'smtp_from_name')); if (data_get($notifiable, 'resend_enabled')) { - $this->isResend = true; config()->set('mail.default', 'resend'); config()->set('resend.api_key', data_get($notifiable, 'resend_api_key')); } diff --git a/app/Notifications/Channels/SendsTelegram.php b/app/Notifications/Channels/SendsTelegram.php new file mode 100644 index 000000000..ee8bd0656 --- /dev/null +++ b/app/Notifications/Channels/SendsTelegram.php @@ -0,0 +1,9 @@ +toTelegram($notifiable); + $telegramData = $notifiable->routeNotificationForTelegram(); + + $message = data_get($data, 'message'); + $buttons = data_get($data, 'buttons', []); + ray($message, $buttons); + $telegramToken = data_get($telegramData, 'token'); + $chatId = data_get($telegramData, 'chat_id'); + + if (!$telegramToken || !$chatId || !$message) { + throw new \Exception('Telegram token, chat id and message are required'); + } + dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId)); + } +} diff --git a/app/Notifications/Channels/TransactionalEmailChannel.php b/app/Notifications/Channels/TransactionalEmailChannel.php index aa5541dee..2985d5183 100644 --- a/app/Notifications/Channels/TransactionalEmailChannel.php +++ b/app/Notifications/Channels/TransactionalEmailChannel.php @@ -12,7 +12,6 @@ use Log; class TransactionalEmailChannel { - private bool $isResend = false; public function send(User $notifiable, Notification $notification): void { $settings = InstanceSettings::get(); @@ -26,33 +25,14 @@ class TransactionalEmailChannel } $this->bootConfigs(); $mailMessage = $notification->toMail($notifiable); - // if ($this->isResend) { Mail::send( [], [], fn (Message $message) => $message - ->from( - data_get($settings, 'smtp_from_address'), - data_get($settings, 'smtp_from_name'), - ) ->to($email) ->subject($mailMessage->subject) ->html((string)$mailMessage->render()) ); - // } else { - // Mail::send( - // [], - // [], - // fn (Message $message) => $message - // ->from( - // data_get($settings, 'smtp_from_address'), - // data_get($settings, 'smtp_from_name'), - // ) - // ->bcc($email) - // ->subject($mailMessage->subject) - // ->html((string)$mailMessage->render()) - // ); - // } } private function bootConfigs(): void @@ -61,8 +41,5 @@ class TransactionalEmailChannel if (!$type) { throw new Exception('No email settings found.'); } - if ($type === 'resend') { - $this->isResend = true; - } } } diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index 79fd9405d..613b0846c 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -25,19 +25,7 @@ class BackupFailed extends Notification implements ShouldQueue public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups'); - - if ($isEmailEnabled && $isSubscribedToEmailEvent) { - $channels[] = EmailChannel::class; - } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'database_backups'); } public function toMail(): MailMessage @@ -56,4 +44,11 @@ class BackupFailed extends Notification implements ShouldQueue { return "❌ Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}"; } + public function toTelegram(): array + { + $message = "❌ Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}"; + return [ + "message" => $message, + ]; + } } diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index 1b279d632..eb6d07c25 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -25,19 +25,7 @@ class BackupSuccess extends Notification implements ShouldQueue public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_database_backups'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_database_backups'); - - if ($isEmailEnabled && $isSubscribedToEmailEvent) { - $channels[] = EmailChannel::class; - } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'database_backups'); } public function toMail(): MailMessage @@ -55,4 +43,11 @@ class BackupSuccess extends Notification implements ShouldQueue { return "✅ Database backup for {$this->name} with frequency of {$this->frequency} was successful."; } + public function toTelegram(): array + { + $message = "✅ Database backup for {$this->name} with frequency of {$this->frequency} was successful."; + return [ + "message" => $message, + ]; + } } diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php index ee13a6cc2..78a76c059 100644 --- a/app/Notifications/Internal/GeneralNotification.php +++ b/app/Notifications/Internal/GeneralNotification.php @@ -3,6 +3,7 @@ namespace App\Notifications\Internal; use App\Notifications\Channels\DiscordChannel; +use App\Notifications\Channels\TelegramChannel; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Notification; @@ -12,16 +13,22 @@ class GeneralNotification extends Notification implements ShouldQueue use Queueable; public function __construct(public string $message) - {} + { + } public function via(object $notifiable): array { - $channels[] = DiscordChannel::class; - return $channels; + return [TelegramChannel::class, DiscordChannel::class]; } public function toDiscord(): string { return $this->message; } + public function toTelegram(): array + { + return [ + "message" => $this->message, + ]; + } } diff --git a/app/Notifications/Server/NotReachable.php b/app/Notifications/Server/NotReachable.php index bc97d033e..083808224 100644 --- a/app/Notifications/Server/NotReachable.php +++ b/app/Notifications/Server/NotReachable.php @@ -22,19 +22,7 @@ class NotReachable extends Notification implements ShouldQueue public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isSubscribedToEmailEvent = data_get($notifiable, 'smtp_notifications_status_changes'); - $isSubscribedToDiscordEvent = data_get($notifiable, 'discord_notifications_status_changes'); - - // if ($isEmailEnabled && $isSubscribedToEmailEvent) { - // $channels[] = EmailChannel::class; - // } - if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { - $channels[] = DiscordChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'status_changes'); } public function toMail(): MailMessage @@ -55,4 +43,10 @@ class NotReachable extends Notification implements ShouldQueue $message = '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.'; return $message; } + public function toTelegram(): array + { + return [ + "message" => '⛔ Server \'' . $this->server->name . '\' is unreachable (could be a temporary issue). If you receive this more than twice in a row, please check your server.' + ]; + } } diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index 36f0e1053..685434cb2 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -4,6 +4,7 @@ namespace App\Notifications; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\TelegramChannel; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -19,18 +20,7 @@ class Test extends Notification implements ShouldQueue public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - - if ($isDiscordEnabled && empty($this->emails)) { - $channels[] = DiscordChannel::class; - } - - if ($isEmailEnabled && !empty($this->emails)) { - $channels[] = EmailChannel::class; - } - return $channels; + return setNotificationChannels($notifiable, 'test'); } public function toMail(): MailMessage @@ -48,4 +38,16 @@ class Test extends Notification implements ShouldQueue $message .= '[Go to your dashboard](' . base_url() . ')'; return $message; } + public function toTelegram(): array + { + return [ + "message" => 'This is a test Telegram notification from Coolify.', + "buttons" => [ + [ + "text" => "Go to your dashboard", + "url" => 'https://coolify.io' + ] + ], + ]; + } } diff --git a/app/Notifications/TransactionalEmails/ResetPassword.php b/app/Notifications/TransactionalEmails/ResetPassword.php index 6844aa705..2a11051b3 100644 --- a/app/Notifications/TransactionalEmails/ResetPassword.php +++ b/app/Notifications/TransactionalEmails/ResetPassword.php @@ -50,10 +50,6 @@ class ResetPassword extends Notification protected function buildMailMessage($url) { $mail = new MailMessage(); - $mail->from( - data_get($this->settings, 'smtp_from_address'), - data_get($this->settings, 'smtp_from_name'), - ); $mail->subject('Reset Password'); $mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]); return $mail; diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index b99dc3a9d..5b7965461 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -77,6 +77,7 @@ function generate_ssh_command(string $private_key_location, string $server_ip, s if ($isMux && config('coolify.mux_enabled')) { $ssh_command .= '-o ControlMaster=auto -o ControlPersist=1m -o ControlPath=/var/www/html/storage/app/ssh/mux/%h_%p_%r '; } + $command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command"; $ssh_command .= "-i {$private_key_location} " . '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ' . '-o PasswordAuthentication=no ' @@ -92,7 +93,18 @@ function generate_ssh_command(string $private_key_location, string $server_ip, s return $ssh_command; } - +function instantCommand(string $command, $throwError = true) { + $process = Process::run($command); + $output = trim($process->output()); + $exitCode = $process->exitCode(); + if ($exitCode !== 0) { + if (!$throwError) { + return null; + } + throw new \RuntimeException($process->errorOutput(), $exitCode); + } + return $output; +} function instant_remote_process(array $command, Server $server, $throwError = true, $repeat = 1) { $command_string = implode("\n", $command); @@ -216,3 +228,29 @@ function check_server_connection(Server $server) $server->save(); } } + +function checkRequiredCommands(Server $server) +{ + $commands = collect(["jq", "jc"]); + foreach ($commands as $command) { + $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false); + if ($commandFound) { + ray($command . ' found'); + continue; + } + try { + instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server); + } catch (\Exception $e) { + ray('could not install ' . $command); + ray($e); + break; + } + $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false); + if ($commandFound) { + ray($command . ' found'); + continue; + } + ray('could not install ' . $command); + break; + } +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 249f76920..ac8beecf3 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2,12 +2,14 @@ use App\Models\InstanceSettings; use App\Models\Team; +use App\Notifications\Channels\DiscordChannel; +use App\Notifications\Channels\EmailChannel; +use App\Notifications\Channels\TelegramChannel; use App\Notifications\Internal\GeneralNotification; use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException; use Illuminate\Database\QueryException; use Illuminate\Mail\Message; use Illuminate\Notifications\Messages\MailMessage; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Route; @@ -149,6 +151,8 @@ function set_transanctional_email_settings(InstanceSettings | null $settings = n if (!$settings) { $settings = InstanceSettings::get(); } + config()->set('mail.from.address', data_get($settings, 'smtp_from_address')); + config()->set('mail.from.name', data_get($settings, 'smtp_from_name')); if (data_get($settings, 'resend_enabled')) { config()->set('mail.default', 'resend'); config()->set('resend.api_key', data_get($settings, 'resend_api_key')); @@ -241,9 +245,9 @@ function validate_cron_expression($expression_to_validate): bool function send_internal_notification(string $message): void { try { - $baseUrl = base_url(false); + $baseUrl = config('app.name'); $team = Team::find(0); - $team->notify(new GeneralNotification("👀 Internal notifications from {$baseUrl}: " . $message)); + $team->notify(new GeneralNotification("👀 {$baseUrl}: " . $message)); } catch (\Throwable $th) { ray($th->getMessage()); } @@ -259,17 +263,32 @@ function send_user_an_email(MailMessage $mail, string $email): void [], [], fn (Message $message) => $message - ->from( - data_get($settings, 'smtp_from_address'), - data_get($settings, 'smtp_from_name') - ) ->to($email) ->subject($mail->subject) ->html((string) $mail->render()) ); - } function isEmailEnabled($notifiable) { return data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') || data_get($notifiable, 'use_instance_email_settings'); } +function setNotificationChannels($notifiable, $event) +{ + $channels = []; + $isEmailEnabled = isEmailEnabled($notifiable); + $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); + $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); + $isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event"); + $isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event"); + + if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { + $channels[] = DiscordChannel::class; + } + if ($isEmailEnabled) { + $channels[] = EmailChannel::class; + } + if ($isTelegramEnabled && $isSubscribedToTelegramEvent) { + $channels[] = TelegramChannel::class; + } + return $channels; +} diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index 7691ca7d6..ae3506782 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -47,6 +47,9 @@ function getEndDate() function isSubscriptionActive() { + if (!isCloud()) { + return false; + } $team = currentTeam(); if (!$team) { return false; diff --git a/config/coolify.php b/config/coolify.php index bb9e67364..cd16d6b6f 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -7,4 +7,5 @@ return [ 'mux_enabled' => env('MUX_ENABLED', true), 'dev_webhook' => env('SERVEO_URL'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), + 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'), ]; diff --git a/config/sentry.php b/config/sentry.php index f0ce8e18d..7b993b0d9 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.23', + 'release' => '4.0.0-beta.24', 'server_name' => env('APP_ID', 'coolify'), // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index a0d53aeca..54f276a01 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ string('stripe_feedback')->nullable()->after('stripe_cancel_at_period_end'); + $table->string('stripe_comment')->nullable()->after('stripe_feedback'); + + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('subscriptions', function (Blueprint $table) { + $table->dropColumn('stripe_feedback'); + $table->dropColumn('stripe_comment'); + }); + } +}; diff --git a/database/migrations/2023_08_22_071055_add_discord_notifications_to_teams.php b/database/migrations/2023_08_22_071055_add_discord_notifications_to_teams.php new file mode 100644 index 000000000..0439cae54 --- /dev/null +++ b/database/migrations/2023_08_22_071055_add_discord_notifications_to_teams.php @@ -0,0 +1,34 @@ +boolean('telegram_enabled')->default(false); + $table->text('telegram_token')->nullable(); + $table->text('telegram_chat_id')->nullable(); + $table->boolean('telegram_notifications_test')->default(true); + $table->boolean('telegram_notifications_deployments')->default(true); + $table->boolean('telegram_notifications_status_changes')->default(true); + $table->boolean('telegram_notifications_database_backups')->default(true); + }); + } + + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('telegram_enabled'); + $table->dropColumn('telegram_token'); + $table->dropColumn('telegram_chat_id'); + $table->dropColumn('telegram_notifications_test'); + $table->dropColumn('telegram_notifications_deployments'); + $table->dropColumn('telegram_notifications_status_changes'); + $table->dropColumn('telegram_notifications_database_backups'); + }); + } +}; diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index 8b6ee3141..9a307c7eb 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -45,7 +45,7 @@ class ProductionSeeder extends Seeder ]); } - if (config('app.name') !== 'coolify-cloud') { + if (config('app.name') !== 'Coolify Cloud') { // Save SSH Keys for the Coolify Host $coolify_key_name = "id.root@host.docker.internal"; $coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}"); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index df1beff70..cefdec07f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -6,6 +6,7 @@ x-testing-host: &testing-host-base context: ./docker/testing-host networks: - coolify + init: true services: coolify: @@ -53,6 +54,7 @@ services: <<: *testing-host-base container_name: coolify-testing-host volumes: + - /:/host - /var/run/docker.sock:/var/run/docker.sock - /data/coolify/:/data/coolify mailpit: diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index 9fe9c317f..301322c63 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -2,15 +2,15 @@ FROM alpine:3.17 ARG TARGETPLATFORM # https://download.docker.com/linux/static/stable/ -ARG DOCKER_VERSION=23.0.6 +ARG DOCKER_VERSION=24.0.5 # https://github.com/docker/compose/releases -ARG DOCKER_COMPOSE_VERSION=2.18.1 +ARG DOCKER_COMPOSE_VERSION=2.21.0 # https://github.com/docker/buildx/releases -ARG DOCKER_BUILDX_VERSION=0.10.5 +ARG DOCKER_BUILDX_VERSION=0.11.2 # https://github.com/buildpacks/pack/releases -ARG PACK_VERSION=0.29.0 +ARG PACK_VERSION=0.30.0 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.12.0 +ARG NIXPACKS_VERSION=1.13.0 USER root WORKDIR /artifacts @@ -38,5 +38,5 @@ COPY --from=minio/mc /usr/bin/mc /usr/bin/mc RUN chmod +x /usr/bin/mc ENTRYPOINT ["/sbin/tini", "--"] -CMD ["sh", "-c", "while true; do sleep 1; done"] +CMD ["tail", "-f", "/dev/null"] diff --git a/docker/testing-host/Dockerfile b/docker/testing-host/Dockerfile index 82871941d..6783e4f69 100644 --- a/docker/testing-host/Dockerfile +++ b/docker/testing-host/Dockerfile @@ -1,29 +1,28 @@ -FROM alpine:3.17 +FROM debian:12-slim ARG TARGETPLATFORM # https://download.docker.com/linux/static/stable/ -ARG DOCKER_VERSION=23.0.6 +ARG DOCKER_VERSION=24.0.5 # https://github.com/docker/compose/releases -ARG DOCKER_COMPOSE_VERSION=2.18.1 +ARG DOCKER_COMPOSE_VERSION=2.21.0 # https://github.com/docker/buildx/releases -ARG DOCKER_BUILDX_VERSION=0.10.5 +ARG DOCKER_BUILDX_VERSION=0.11.2 # https://github.com/buildpacks/pack/releases -ARG PACK_VERSION=0.29.0 +ARG PACK_VERSION=0.30.0 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.12.0 +ARG NIXPACKS_VERSION=1.13.0 USER root WORKDIR /root -RUN apk add --no-cache bash curl git git-lfs openssh-client openssh-server tar tini postgresql-client lsof +ENV PATH "$PATH:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin" + +RUN apt update && apt -y install openssh-client openssh-server curl wget git jq jc RUN mkdir -p ~/.docker/cli-plugins -RUN if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ - curl -sSL https://github.com/docker/buildx/releases/download/v${DOCKER_BUILDX_VERSION}/buildx-v${DOCKER_BUILDX_VERSION}.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx && \ - curl -sSL https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose && \ - (curl -sSL https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz | tar -C /usr/bin/ --no-same-owner -xzv --strip-components=1 docker/docker) && \ - (curl -sSL https://github.com/buildpacks/pack/releases/download/v${PACK_VERSION}/pack-v${PACK_VERSION}-linux.tgz | tar -C /usr/local/bin/ --no-same-owner -xzv pack) && \ - curl -sSL https://nixpacks.com/install.sh | bash && \ - chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack /root/.docker/cli-plugins/docker-buildx \ - ;fi +RUN curl -sSL https://github.com/docker/buildx/releases/download/v${DOCKER_BUILDX_VERSION}/buildx-v${DOCKER_BUILDX_VERSION}.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx +RUN curl -sSL https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose +RUN (curl -sSL https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz | tar -C /usr/bin/ --no-same-owner -xzv --strip-components=1 docker/docker) +RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /root/.docker/cli-plugins/docker-buildx + # Setup sshd RUN ssh-keygen -A @@ -32,6 +31,4 @@ RUN mkdir -p ~/.ssh RUN echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFuGmoeGq/pojrsyP1pszcNVuZx9iFkCELtxrh31QJ68 coolify@coolify-instance" >> ~/.ssh/authorized_keys EXPOSE 22 -ENTRYPOINT ["/sbin/tini", "--"] CMD ["/usr/sbin/sshd", "-D", "-o", "ListenAddress=0.0.0.0"] - diff --git a/resources/js/components/MagicBar.vue b/resources/js/components/MagicBar.vue index 5719bc1d6..8f052e96e 100644 --- a/resources/js/components/MagicBar.vue +++ b/resources/js/components/MagicBar.vue @@ -593,7 +593,7 @@ async function redirect() { targetUrl.pathname = `/source/new` break; case 7: - targetUrl.pathname = `/private-key/new` + targetUrl.pathname = `/security/private-key/new` break; case 8: targetUrl.pathname = `/destination/new` @@ -612,7 +612,7 @@ async function redirect() { targetUrl.pathname = `/servers` break; case 13: - targetUrl.pathname = `/private-keys` + targetUrl.pathname = `/security/private-key` break; case 14: targetUrl.pathname = `/projects` diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 838bc5a11..2ed0564bb 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -51,6 +51,11 @@ {{ session('status') }} @endif + @if (session('error')) +