Merge pull request #1200 from coollabsio/next

v4.0.0-beta.24
This commit is contained in:
Andras Bacsai 2023-09-06 15:16:44 +02:00 committed by GitHub
commit 38f9761b67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 1283 additions and 1092 deletions

View File

@ -6,6 +6,7 @@
USERID=
GROUPID=
############################################################################################################
APP_NAME=Coolify-localhost
APP_ID=development
APP_ENV=local
APP_KEY=

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 }}
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@ -65,7 +65,7 @@ class StartPostgresql
],
'networks' => [
$this->database->destination->network => [
'external' => false,
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]

View File

@ -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...",

View File

@ -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);

View File

@ -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();
}
}

View File

@ -4,6 +4,7 @@ namespace App\Enums;
enum ProxyTypes: string
{
case NONE = 'NONE';
case TRAEFIK_V2 = 'TRAEFIK_V2';
case NGINX = 'NGINX';
case CADDY = 'CADDY';

View File

@ -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 {

View File

@ -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

View File

@ -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');

View File

@ -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);
}
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Livewire;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Notifications\Messages\MailMessage;
use Livewire\Component;
use Route;
class Help extends Component
{
use WithRateLimiting;
public string $description;
public string $subject;
public ?string $path = null;
protected $rules = [
'description' => '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');
}
}

View File

@ -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.');
}
}

View File

@ -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;
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Http\Livewire\Notifications;
use App\Models\Team;
use App\Notifications\Test;
use Livewire\Component;
class TelegramSettings extends Component
{
public Team $team;
protected $rules = [
'team.telegram_enabled' => '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.');
}
}

View File

@ -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) {

View File

@ -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', [

View File

@ -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();

View File

@ -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');
}

View File

@ -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]]);
}

View File

@ -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]),

View File

@ -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') {

View File

@ -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],
);
}

View File

@ -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);

View File

@ -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();

View File

@ -0,0 +1,72 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Str;
class SendMessageToTelegramJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 5;
/**
* The maximum number of unhandled exceptions to allow before failing.
*/
public int $maxExceptions = 3;
public function __construct(
public string $text,
public array $buttons,
public string $token,
public string $chatId,
) {
}
/**
* Execute the job.
*/
public function handle(): void
{
$url = 'https://api.telegram.org/bot' . $this->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());
}
}
}

View File

@ -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);

View File

@ -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
],
];
}
}

View File

@ -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
],
];
}
}

View File

@ -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
]
],
];
}
}

View File

@ -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'));
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Notifications\Channels;
interface SendsTelegram
{
public function routeNotificationForTelegram();
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Notifications\Channels;
use App\Jobs\SendMessageToTelegramJob;
class TelegramChannel
{
public function send($notifiable, $notification): void
{
$data = $notification->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));
}
}

View File

@ -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;
}
}
}

View File

@ -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,
];
}
}

View File

@ -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,
];
}
}

View File

@ -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,
];
}
}

View File

@ -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.'
];
}
}

View File

@ -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'
]
],
];
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -47,6 +47,9 @@ function getEndDate()
function isSubscriptionActive()
{
if (!isCloud()) {
return false;
}
$team = currentTeam();
if (!$team) {
return false;

View File

@ -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'),
];

View File

@ -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'),

View File

@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.23';
return '4.0.0-beta.24';

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->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');
});
}
};

View File

@ -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}");

View File

@ -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:

View File

@ -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"]

View File

@ -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"]

View File

@ -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`

View File

@ -51,6 +51,11 @@
{{ session('status') }}
</div>
@endif
@if (session('error'))
<div class="mb-4 font-medium text-red-600">
{{ session('error') }}
</div>
@endif
</form>
</div>
</div>

View File

@ -1,6 +1,6 @@
{{ Illuminate\Mail\Markdown::parse('---') }}
Thank you.<br>
Thank you,<br>
{{ config('app.name') ?? 'Coolify' }}
{{ Illuminate\Mail\Markdown::parse('[Contact Support](https://docs.coollabs.io)') }}

View File

@ -26,7 +26,8 @@
wire:model.defer={{ $id }} wire:dirty.class.remove='text-white'
wire:dirty.class="input-warning" wire:loading.attr="disabled" type="{{ $type }}"
@readonly($readonly) @disabled($disabled) id="{{ $id }}" name="{{ $name }}"
placeholder="{{ $attributes->get('placeholder') }}">
placeholder="{{ $attributes->get('placeholder') }}"
aria-placeholder="{{ $attributes->get('placeholder') }}">
</div>
@else

View File

@ -5,8 +5,8 @@
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="absolute hidden text-xs rounded group-hover:block border-coolgray-400 bg-coolgray-500">
<div class="p-4 card-body">
<div class="absolute z-40 hidden text-xs rounded group-hover:block border-coolgray-400 bg-coolgray-500">
<div class="p-4">
{!! $helper !!}
</div>
</div>

View File

@ -43,10 +43,6 @@
@endisset
@if ($modalSubmit)
{{ $modalSubmit }}
@else
<x-forms.button onclick="{{ $modalId }}.close()" type="submit">
Save
</x-forms.button>
@endif
</form>

View File

@ -50,6 +50,23 @@
</svg>
</a>
</li>
<li title="Source">
<a class="hover:bg-transparent" href="{{ route('source.all') }}">
<svg class="icon" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor"
d="m6.793 1.207l.353.354l-.353-.354ZM1.207 6.793l-.353-.354l.353.354Zm0 1.414l.354-.353l-.354.353Zm5.586 5.586l-.354.353l.354-.353Zm1.414 0l-.353-.354l.353.354Zm5.586-5.586l.353.354l-.353-.354Zm0-1.414l-.354.353l.354-.353ZM8.207 1.207l.354-.353l-.354.353ZM6.44.854L.854 6.439l.707.707l5.585-5.585L6.44.854ZM.854 8.56l5.585 5.585l.707-.707l-5.585-5.585l-.707.707Zm7.707 5.585l5.585-5.585l-.707-.707l-5.585 5.585l.707.707Zm5.585-7.707L8.561.854l-.707.707l5.585 5.585l.707-.707Zm0 2.122a1.5 1.5 0 0 0 0-2.122l-.707.707a.5.5 0 0 1 0 .708l.707.707ZM6.44 14.146a1.5 1.5 0 0 0 2.122 0l-.707-.707a.5.5 0 0 1-.708 0l-.707.707ZM.854 6.44a1.5 1.5 0 0 0 0 2.122l.707-.707a.5.5 0 0 1 0-.708L.854 6.44Zm6.292-4.878a.5.5 0 0 1 .708 0L8.56.854a1.5 1.5 0 0 0-2.122 0l.707.707Zm-2 1.293l1 1l.708-.708l-1-1l-.708.708ZM7.5 5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 6V5Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 4.5H8ZM7.5 4a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 3v1Zm0-1A1.5 1.5 0 0 0 6 4.5h1a.5.5 0 0 1 .5-.5V3Zm.646 2.854l1.5 1.5l.707-.708l-1.5-1.5l-.707.708ZM10.5 8a.5.5 0 0 1-.5-.5H9A1.5 1.5 0 0 0 10.5 9V8Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 12 7.5h-1Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 10.5 6v1Zm0-1A1.5 1.5 0 0 0 9 7.5h1a.5.5 0 0 1 .5-.5V6ZM7 5.5v4h1v-4H7Zm.5 5.5a.5.5 0 0 1-.5-.5H6A1.5 1.5 0 0 0 7.5 12v-1Zm.5-.5a.5.5 0 0 1-.5.5v1A1.5 1.5 0 0 0 9 10.5H8Zm-.5-.5a.5.5 0 0 1 .5.5h1A1.5 1.5 0 0 0 7.5 9v1Zm0-1A1.5 1.5 0 0 0 6 10.5h1a.5.5 0 0 1 .5-.5V9Z" />
</svg>
</a>
</li>
<li title="Security">
<a class="hover:bg-transparent" href="{{ route('security.private-key.index') }}">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="m16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1-4.069 0l-.301-.301l-6.558 6.558a2 2 0 0 1-1.239.578L5.172 21H4a1 1 0 0 1-.993-.883L3 20v-1.172a2 2 0 0 1 .467-1.284l.119-.13L4 17h2v-2h2v-2l2.144-2.144l-.301-.301a2.877 2.877 0 0 1 0-4.069l2.643-2.643a2.877 2.877 0 0 1 4.069 0zM15 9h.01" />
</svg>
</a>
</li>
<li title="Teams">
<a class="hover:bg-transparent" href="{{ route('team.index') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"
@ -64,6 +81,7 @@
</svg>
</a>
</li>
<div class="flex-1"></div>
@if (isInstanceAdmin() && !isCloud())
<livewire:upgrade />
@ -95,6 +113,20 @@
</a>
</li>
@endif
@if (isSubscriptionActive() || isDev())
<li title="Help" class="mt-auto">
<div class="justify-center icons" wire:click="help" onclick="help.showModal()">
<svg class="{{ request()->is('help*') ? 'text-warning icon' : 'icon' }}" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0-18 0m9 4v.01" />
<path d="M12 13a2 2 0 0 0 .914-3.782a1.98 1.98 0 0 0-2.414.483" />
</g>
</svg>
</div>
</li>
@endif
<li class="pb-6" title="Logout">
<form action="/logout" method="POST" class=" hover:bg-transparent">
@csrf

View File

@ -0,0 +1,17 @@
<div class="pb-6">
<h1>Security</h1>
<nav class="flex pt-2 pb-10">
<ol class="inline-flex items-center">
<li>
<div class="flex items-center">
<span>Security related settings</span>
</div>
</li>
</ol>
</nav>
<nav class="navbar-main">
<a class="{{ request()->routeIs('security.private-key.index') ? 'text-white' : '' }}" href="{{ route('security.private-key.index') }}">
<button>Private Keys</button>
</a>
</nav>
</div>

View File

@ -0,0 +1,5 @@
{{ $description }}
{{ Illuminate\Mail\Markdown::parse('---') }}
{{ Illuminate\Mail\Markdown::parse($debug) }}

View File

@ -1,5 +1,5 @@
<x-emails.layout>
A password reset has been requested for this email address on [{{ config('app.name') }}]({{ config('app.url') }}).
A password reset has been requested for this email address.
Click [here]({{ $url }}) to reset your password.

View File

@ -1,19 +1,4 @@
<x-emails.layout>
You have been invited to join the Coolify Cloud.
[Login here]({{base_url()}}/login)
Here is your initial login information.
Email:
**{{ $email }}**
Initial Password:
**{{ $password }}**
(You will forced to change it on first login.)
You have been invited to join the Coolify Cloud: [Get Started]({{$loginLink}})
</x-emails.layout>

View File

@ -1,5 +1,20 @@
@extends('errors::minimal')
@section('title', __('Unauthorized'))
@section('code', '401')
@section('message', __('Unauthorized'))
@extends('layouts.base')
<div class="min-h-screen hero">
<div class="text-center hero-content">
<div class="">
<p class="font-mono text-6xl font-semibold text-warning">401</p>
<h1 class="mt-4 font-bold tracking-tight text-white">You shall not pass!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">You don't have permission to access this page.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact
support
<x-external-link />
</a>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,20 @@
@extends('errors::minimal')
@section('title', __('Forbidden'))
@section('code', '403')
@section('message', __($exception->getMessage() ?: 'Forbidden'))
@extends('layouts.base')
<div class="min-h-screen hero">
<div class="text-center hero-content">
<div class="">
<p class="font-mono text-6xl font-semibold text-warning">403</p>
<h1 class="mt-4 font-bold tracking-tight text-white">You shall not pass!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">You don't have permission to access this page.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact
support
<x-external-link />
</a>
</div>
</div>
</div>
</div>

View File

@ -1,22 +1,21 @@
<x-layout>
<div class="min-h-screen hero">
<div class="text-center hero-content">
<div class="">
<p class="font-mono text-6xl font-semibold text-warning">404</p>
<h1 class="mt-4 font-bold tracking-tight text-white">How did you got here?</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking
for.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button isHighlighted>Go back home</x-forms.button>
</a>
<a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact
support
<x-external-link />
</a>
</div>
@extends('layouts.base')
<div class="min-h-screen hero">
<div class="text-center hero-content">
<div class="">
<p class="font-mono text-6xl font-semibold text-warning">404</p>
<h1 class="mt-4 font-bold tracking-tight text-white">How did you got here?</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking
for.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a target="_blank" class="text-xs" href="https://docs.coollabs.io/contact.html">Contact
support
<x-external-link />
</a>
</div>
</div>
</div>
</x-layout>
</div>

View File

@ -1,21 +1,20 @@
<x-layout>
<div class="min-h-screen hero">
<div class="text-center hero-content">
<div class="">
<p class="font-mono text-6xl font-semibold text-warning">419</p>
<h1 class="mt-4 font-bold tracking-tight text-white">This page is definitely old</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking
for.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/"
class="rounded-md bg-coollabs px-3.5 py-2.5 font-semibold text-white shadow-sm hover:bg-coollabs-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 hover:no-underline">Go
back home</a>
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
support
<span aria-hidden="true">&rarr;</span></a>
</div>
@extends('layouts.base')
<div class="min-h-screen hero">
<div class="text-center hero-content">
<div class="">
<p class="font-mono text-6xl font-semibold text-warning">419</p>
<h1 class="mt-4 font-bold tracking-tight text-white">This page is definitely old, not like you!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">Sorry, we couldnt find the page youre looking
for.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
support
<span aria-hidden="true">&rarr;</span></a>
</div>
</div>
</div>
</x-layout>
</div>

View File

@ -1,5 +1,19 @@
@extends('errors::minimal')
@section('title', __('Too Many Requests'))
@section('code', '429')
@section('message', __('Too Many Requests'))
@extends('layouts.base')
<div class="min-h-screen hero">
<div class="text-center hero-content">
<div class="">
<p class="font-mono text-6xl font-semibold text-warning">429</p>
<h1 class="mt-4 font-bold tracking-tight text-white">Woah, slow down there!</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">You're making too many requests. Please wait a few seconds before trying again.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
support
<span aria-hidden="true">&rarr;</span></a>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,23 @@
@extends('errors::minimal')
@section('title', __('Server Error'))
@section('code', '500')
@section('message', __('Server Error'))
@extends('layouts.base')
<div class="min-h-screen hero ">
<div class="text-center hero-content">
<div>
<p class="font-mono text-6xl font-semibold text-warning">500</p>
<h1 class="mt-4 font-bold tracking-tight text-white">Something is not okay, are you okay?</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">There has been an error, we are working on it.
</p>
@if ($exception->getMessage() !== '')
<p class="mt-6 text-base leading-7 text-red-500">Error: {{ $exception->getMessage() }}
</p>
@endif
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="/">
<x-forms.button>Go back home</x-forms.button>
</a>
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
support
<span aria-hidden="true">&rarr;</span></a>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,17 @@
@extends('errors::minimal')
@section('title', __('Service Unavailable'))
@section('code', '503')
@section('message', __('Service Unavailable'))
@extends('layouts.base')
<div class="min-h-screen hero ">
<div class="text-center hero-content">
<div>
<p class="font-mono text-6xl font-semibold text-warning">503</p>
<h1 class="mt-4 font-bold tracking-tight text-white">We are working on serious things.</h1>
<p class="mt-6 text-base leading-7 text-neutral-300">Service Unavailable. Be right back. Thanks for your
patience.
</p>
<div class="flex items-center justify-center mt-10 gap-x-6">
<a href="https://docs.coollabs.io/contact.html" class="font-semibold text-white ">Contact
support
<span aria-hidden="true">&rarr;</span></a>
</div>
</div>
</div>
</div>

View File

@ -1,57 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title')</title>
<!-- Styles -->
<style>
html,
body {
background-color: #fff;
color: #636b6f;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-weight: 100;
height: 100vh;
margin: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.content {
text-align: center;
}
.title {
font-size: 36px;
padding: 20px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div class="content">
<div class="title">
@yield('message')
</div>
</div>
</div>
</body>
</html>

View File

@ -1,552 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title')</title>
<style>
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%
}
body {
margin: 0
}
a {
background-color: transparent
}
code {
font-family: monospace, monospace;
font-size: 1em
}
[hidden] {
display: none
}
html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5
}
*,
:after,
:before {
box-sizing: border-box;
border: 0 solid #e2e8f0
}
a {
color: inherit;
text-decoration: inherit
}
code {
font-family: Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace
}
svg,
video {
display: block;
vertical-align: middle
}
video {
max-width: 100%;
height: auto
}
.bg-white {
--bg-opacity: 1;
background-color: #fff;
background-color: rgba(255, 255, 255, var(--bg-opacity))
}
.bg-gray-100 {
--bg-opacity: 1;
background-color: #f7fafc;
background-color: rgba(247, 250, 252, var(--bg-opacity))
}
.border-gray-200 {
--border-opacity: 1;
border-color: #edf2f7;
border-color: rgba(237, 242, 247, var(--border-opacity))
}
.border-gray-400 {
--border-opacity: 1;
border-color: #cbd5e0;
border-color: rgba(203, 213, 224, var(--border-opacity))
}
.border-t {
border-top-width: 1px
}
.border-r {
border-right-width: 1px
}
.flex {
display: flex
}
.grid {
display: grid
}
.hidden {
display: none
}
.items-center {
align-items: center
}
.justify-center {
justify-content: center
}
.font-semibold {
font-weight: 600
}
.h-5 {
height: 1.25rem
}
.h-8 {
height: 2rem
}
.h-16 {
height: 4rem
}
.text-sm {
font-size: .875rem
}
.text-lg {
font-size: 1.125rem
}
.leading-7 {
line-height: 1.75rem
}
.mx-auto {
margin-left: auto;
margin-right: auto
}
.ml-1 {
margin-left: .25rem
}
.mt-2 {
margin-top: .5rem
}
.mr-2 {
margin-right: .5rem
}
.ml-2 {
margin-left: .5rem
}
.mt-4 {
margin-top: 1rem
}
.ml-4 {
margin-left: 1rem
}
.mt-8 {
margin-top: 2rem
}
.ml-12 {
margin-left: 3rem
}
.-mt-px {
margin-top: -1px
}
.max-w-xl {
max-width: 36rem
}
.max-w-6xl {
max-width: 72rem
}
.min-h-screen {
min-height: 100vh
}
.overflow-hidden {
overflow: hidden
}
.p-6 {
padding: 1.5rem
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem
}
.pt-8 {
padding-top: 2rem
}
.fixed {
position: fixed
}
.relative {
position: relative
}
.top-0 {
top: 0
}
.right-0 {
right: 0
}
.shadow {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06)
}
.text-center {
text-align: center
}
.text-gray-200 {
--text-opacity: 1;
color: #edf2f7;
color: rgba(237, 242, 247, var(--text-opacity))
}
.text-gray-300 {
--text-opacity: 1;
color: #e2e8f0;
color: rgba(226, 232, 240, var(--text-opacity))
}
.text-gray-400 {
--text-opacity: 1;
color: #cbd5e0;
color: rgba(203, 213, 224, var(--text-opacity))
}
.text-gray-500 {
--text-opacity: 1;
color: #a0aec0;
color: rgba(160, 174, 192, var(--text-opacity))
}
.text-gray-600 {
--text-opacity: 1;
color: #718096;
color: rgba(113, 128, 150, var(--text-opacity))
}
.text-gray-700 {
--text-opacity: 1;
color: #4a5568;
color: rgba(74, 85, 104, var(--text-opacity))
}
.text-gray-900 {
--text-opacity: 1;
color: #1a202c;
color: rgba(26, 32, 44, var(--text-opacity))
}
.uppercase {
text-transform: uppercase
}
.underline {
text-decoration: underline
}
.antialiased {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale
}
.tracking-wider {
letter-spacing: .05em
}
.w-5 {
width: 1.25rem
}
.w-8 {
width: 2rem
}
.w-auto {
width: auto
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr))
}
@-webkit-keyframes spin {
0% {
transform: rotate(0deg)
}
to {
transform: rotate(1turn)
}
}
@keyframes spin {
0% {
transform: rotate(0deg)
}
to {
transform: rotate(1turn)
}
}
@-webkit-keyframes ping {
0% {
transform: scale(1);
opacity: 1
}
75%,
to {
transform: scale(2);
opacity: 0
}
}
@keyframes ping {
0% {
transform: scale(1);
opacity: 1
}
75%,
to {
transform: scale(2);
opacity: 0
}
}
@-webkit-keyframes pulse {
0%,
to {
opacity: 1
}
50% {
opacity: .5
}
}
@keyframes pulse {
0%,
to {
opacity: 1
}
50% {
opacity: .5
}
}
@-webkit-keyframes bounce {
0%,
to {
transform: translateY(-25%);
-webkit-animation-timing-function: cubic-bezier(.8, 0, 1, 1);
animation-timing-function: cubic-bezier(.8, 0, 1, 1)
}
50% {
transform: translateY(0);
-webkit-animation-timing-function: cubic-bezier(0, 0, .2, 1);
animation-timing-function: cubic-bezier(0, 0, .2, 1)
}
}
@keyframes bounce {
0%,
to {
transform: translateY(-25%);
-webkit-animation-timing-function: cubic-bezier(.8, 0, 1, 1);
animation-timing-function: cubic-bezier(.8, 0, 1, 1)
}
50% {
transform: translateY(0);
-webkit-animation-timing-function: cubic-bezier(0, 0, .2, 1);
animation-timing-function: cubic-bezier(0, 0, .2, 1)
}
}
@media (min-width: 640px) {
.sm\:rounded-lg {
border-radius: .5rem
}
.sm\:block {
display: block
}
.sm\:items-center {
align-items: center
}
.sm\:justify-start {
justify-content: flex-start
}
.sm\:justify-between {
justify-content: space-between
}
.sm\:h-20 {
height: 5rem
}
.sm\:ml-0 {
margin-left: 0
}
.sm\:px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem
}
.sm\:pt-0 {
padding-top: 0
}
.sm\:text-left {
text-align: left
}
.sm\:text-right {
text-align: right
}
}
@media (min-width: 768px) {
.md\:border-t-0 {
border-top-width: 0
}
.md\:border-l {
border-left-width: 1px
}
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr))
}
}
@media (min-width: 1024px) {
.lg\:px-8 {
padding-left: 2rem;
padding-right: 2rem
}
}
@media (prefers-color-scheme: dark) {
.dark\:bg-gray-800 {
--bg-opacity: 1;
background-color: #2d3748;
background-color: rgba(45, 55, 72, var(--bg-opacity))
}
.dark\:bg-gray-900 {
--bg-opacity: 1;
background-color: #1a202c;
background-color: rgba(26, 32, 44, var(--bg-opacity))
}
.dark\:border-gray-700 {
--border-opacity: 1;
border-color: #4a5568;
border-color: rgba(74, 85, 104, var(--border-opacity))
}
.dark\:text-white {
--text-opacity: 1;
color: #fff;
color: rgba(255, 255, 255, var(--text-opacity))
}
.dark\:text-gray-400 {
--text-opacity: 1;
color: #cbd5e0;
color: rgba(203, 213, 224, var(--text-opacity))
}
}
</style>
<style>
body {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
</style>
</head>
<body class="antialiased">
<div
class="relative flex justify-center min-h-screen bg-gray-100 items-top dark:bg-gray-900 sm:items-center sm:pt-0">
<div class="max-w-xl mx-auto sm:px-6 lg:px-8">
<div class="flex items-center pt-8 sm:justify-start sm:pt-0">
<div class="px-4 text-lg tracking-wider text-gray-500 border-r border-gray-400">
@yield('code')
</div>
<div class="ml-4 text-lg tracking-wider text-gray-500 uppercase">
@yield('message')
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -25,6 +25,14 @@
<body>
@livewireScripts
@if (isSubscriptionActive() || isDev())
<dialog id="help" class="modal">
<livewire:help />
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
@endif
<x-toaster-hub />
<x-version class="fixed left-2 bottom-1" />
<script>

View File

@ -35,5 +35,5 @@
<div class="stat-value">{{ $s3s }}</div>
</div>
</div>
{{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
</div>

View File

@ -0,0 +1,10 @@
<div class="flex flex-col gap-2 rounded modal-box">
<h3>How can we help?</h3>
<div>You can report bug about the current page (details will be included automatically), or send us general feedback.</div>
<form wire:submit.prevent="submit" class="flex flex-col gap-4 pt-4">
<x-forms.input id="subject" label="Subject" placeholder="Summary of your problem."></x-forms.input>
<x-forms.textarea id="description" label="Message"
placeholder="Please provide as much information as possible."></x-forms.textarea>
<x-forms.button class="w-full mt-4" type="submit">Send Request</x-forms.button>
</form>
</div>

View File

@ -5,7 +5,7 @@
<x-forms.button type="submit">
Save
</x-forms.button>
@if ($model->discord_enabled)
@if ($team->discord_enabled)
<x-forms.button class="text-white normal-case btn btn-xs no-animation btn-primary"
wire:click="sendTestNotification">
Send Test Notifications
@ -13,26 +13,26 @@
@endif
</div>
<div class="w-48">
<x-forms.checkbox instantSave id="model.discord_enabled" label="Notification Enabled" />
<x-forms.checkbox instantSave id="team.discord_enabled" label="Notification Enabled" />
</div>
<x-forms.input type="password"
helper="Generate a webhook in Discord.<br>Example: https://discord.com/api/webhooks/...." required
id="model.discord_webhook_url" label="Webhook" />
id="team.discord_webhook_url" label="Webhook" />
</form>
@if (data_get($model, 'discord_enabled'))
@if (data_get($team, 'discord_enabled'))
<h3 class="mt-4">Subscribe to events</h3>
<div class="w-64">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="model.discord_notifications_test" label="Test" />
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_test" label="Test" />
@endif
<h4 class="mt-4">General</h4>
<x-forms.checkbox instantSave="saveModel" id="model.discord_notifications_status_changes"
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_status_changes"
label="Container Status Changes" />
<h4 class="mt-4">Applications</h4>
<x-forms.checkbox instantSave="saveModel" id="model.discord_notifications_deployments"
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_deployments"
label="Deployments" />
<h4 class="mt-4">Databases</h4>
<x-forms.checkbox instantSave="saveModel" id="model.discord_notifications_database_backups"
<x-forms.checkbox instantSave="saveModel" id="team.discord_notifications_database_backups"
label="Backup Statuses" />
</div>
@endif

View File

@ -35,7 +35,13 @@
<x-forms.checkbox instantSave="instantSaveInstance" id="team.use_instance_email_settings"
label="Use hosted email service" />
</div>
@else
<div class="pb-4 w-96">
<x-forms.checkbox disabled id="team.use_instance_email_settings"
label="Use hosted email service (Pro+ subscription required)" />
</div>
@endif
<h3 class="pb-4">Custom Email Service</h3>
@if (!$team->use_instance_email_settings)
<form class="flex flex-col items-end gap-2 pb-4 xl:flex-row" wire:submit.prevent='submitFromFields'>
<x-forms.input required id="team.smtp_from_name" helper="Name used in emails." label="From Name" />
@ -89,8 +95,8 @@
<form wire:submit.prevent='submitResend' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" id="team.resend_api_key" placeholder="API key"
label="Host" />
<x-forms.input required type="password" id="team.resend_api_key" placeholder="API key"
label="API Key" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">

View File

@ -0,0 +1,42 @@
<div>
<form wire:submit.prevent='submit' class="flex flex-col">
<div class="flex items-center gap-2">
<h2>Telegram</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
@if ($team->telegram_enabled)
<x-forms.button class="text-white normal-case btn btn-xs no-animation btn-primary"
wire:click="sendTestNotification">
Send Test Notifications
</x-forms.button>
@endif
</div>
<div class="w-48">
<x-forms.checkbox instantSave id="team.telegram_enabled" label="Notification Enabled" />
</div>
<div class="flex gap-2">
<x-forms.input type="password" helper="Get it from the <a class='inline-block text-white underline' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram." required
id="team.telegram_token" label="Token" />
<x-forms.input type="password" helper="Recommended to add your bot to a group chat and add its Chat ID here." required
id="team.telegram_chat_id" label="Chat ID" />
</div>
</form>
@if (data_get($team, 'telegram_enabled'))
<h3 class="mt-4">Subscribe to events</h3>
<div class="w-64">
@if (isDev())
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_test" label="Test" />
@endif
<h4 class="mt-4">General</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_status_changes"
label="Container Status Changes" />
<h4 class="mt-4">Applications</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_deployments"
label="Deployments" />
<h4 class="mt-4">Databases</h4>
<x-forms.checkbox instantSave="saveModel" id="team.telegram_notifications_database_backups"
label="Backup Statuses" />
</div>
@endif
</div>

View File

@ -6,7 +6,7 @@
</x-modal>
<form class="flex flex-col gap-2" wire:submit.prevent='changePrivateKey'>
<div class="flex items-end gap-2">
<h1>Private Key</h1>
<h2>Private Key</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
@ -16,7 +16,6 @@
</x-forms.button>
@endif
</div>
<div class="pb-8">Private Key used for SSH connection</div>
<x-forms.input id="private_key.name" label="Name" required />
<x-forms.input id="private_key.description" label="Description" />
<div>

View File

@ -10,8 +10,7 @@
</ul>
<h2>Applications</h2>
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
<div class="box group"
wire:click="set_type('public')">
<div class="box group" wire:click="setType('public')">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
Public Repository
@ -21,8 +20,7 @@
</div>
</div>
</div>
<div class="box group"
wire:click="set_type('private-gh-app')">
<div class="box group" wire:click="setType('private-gh-app')">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
Private Repository
@ -32,8 +30,7 @@
</div>
</div>
</div>
<div class="box group"
wire:click="set_type('private-deploy-key')">
<div class="box group" wire:click="setType('private-deploy-key')">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
Private Repository (with deploy key)
@ -45,8 +42,7 @@
</div>
</div>
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
<div class="box group"
wire:click="set_type('dockerfile')">
<div class="box group" wire:click="setType('dockerfile')">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
Based on a Dockerfile
@ -58,18 +54,27 @@
</div>
</div>
<h2 class="py-4">Databases</h2>
<div class="flex flex-col justify-start gap-2 text-left xl:flex-row">
<div class="box group"
wire:click="set_type('postgresql')">
<div class="grid justify-start grid-cols-1 gap-2 text-left xl:grid-cols-3">
<div class="box group" wire:click="setType('postgresql')">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
PostgreSQL
New PostgreSQL
</div>
<div class="text-xs group-hover:text-white">
The most loved relational database in the world.
</div>
</div>
</div>
{{-- <div class="box group" wire:click="setType('existing-postgresql')">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
Backup Existing PostgreSQL
</div>
<div class="text-xs group-hover:text-white">
Schedule a backup of an existing PostgreSQL database.
</div>
</div>
</div> --}}
</div>
@endif
@if ($current_step === 'servers')
@ -80,8 +85,7 @@
</ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row">
@forelse($servers as $server)
<div class="box group"
wire:click="set_server({{ $server }})">
<div class="box group" wire:click="setServer({{ $server }})">
<div class="flex flex-col mx-6">
<div class="group-hover:text-white">
{{ $server->name }}
@ -92,9 +96,9 @@
</div>
@empty
<div>
<div>No validated & reachable servers found. <a class="text-white underline" href="/servers">
Go to servers page
</a></div>
<div>No validated & reachable servers found. <a class="text-white underline" href="/servers">
Go to servers page
</a></div>
<x-use-magic-bar link="/server/new" />
</div>
@ -109,11 +113,10 @@
</ul>
<div class="flex flex-col justify-center gap-2 text-left xl:flex-row">
@foreach ($standaloneDockers as $standaloneDocker)
<div class="box group"
wire:click="set_destination('{{ $standaloneDocker->uuid }}')">
<div class="box group" wire:click="setDestination('{{ $standaloneDocker->uuid }}')">
<div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white">
Standalone Docker <span class="text-xs">({{ $standaloneDocker->name }})</span>
Standalone Docker <span class="text-xs">({{ $standaloneDocker->name }})</span>
</div>
<div class="text-xs group-hover:text-white">
network: {{ $standaloneDocker->network }}</div>
@ -121,16 +124,21 @@
</div>
@endforeach
@foreach ($swarmDockers as $swarmDocker)
<div class="box group"
wire:click="set_destination('{{ $swarmDocker->uuid }}')">
<div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white">
Swarm Docker <span class="text-xs">({{ $swarmDocker->name }})</span>
<div class="box group" wire:click="setDestination('{{ $swarmDocker->uuid }}')">
<div class="flex flex-col mx-6">
<div class="font-bold group-hover:text-white">
Swarm Docker <span class="text-xs">({{ $swarmDocker->name }})</span>
</div>
</div>
</div>
</div>
@endforeach
@endforeach
</div>
@endif
@if ($current_step === 'existing-postgresql')
<form wire:submit.prevent='addExistingPostgresql' class="flex items-end gap-2">
<x-forms.input placeholder="postgres://username:password@database:5432" label="Database URL" id="existingPostgresqlUrl" />
<x-forms.button type="submit">Add Database</x-forms.button>
</form>
@endif
</div>
</div>

View File

@ -1,9 +1,8 @@
@php use App\Enums\ProxyTypes; @endphp
<div>
@if ($server->settings->is_usable)
@if ($server->proxy->type)
<div x-init="$wire.load_proxy_configuration">
@if ($selectedProxy->value === 'TRAEFIK_V2')
@if ($selectedProxy === 'TRAEFIK_V2')
<form wire:submit.prevent='submit'>
<div class="flex items-center gap-2">
<h2>Proxy</h2>
@ -40,13 +39,31 @@
@endif
</div>
</form>
@elseif($selectedProxy === 'NONE')
<div class="flex items-center gap-2">
<h2>Proxy</h2>
@if ($server->proxy->status === 'exited')
<x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
@endif
</div>
<div class="pt-3 pb-4">None</div>
@else
<div class="flex items-center gap-2">
<h2>Proxy</h2>
@if ($server->proxy->status === 'exited')
<x-forms.button wire:click.prevent="change_proxy">Switch Proxy</x-forms.button>
@endif
</div>
@endif
@else
<div>
<h2>Proxy</h2>
<div class="subtitle ">Select a proxy you would like to use on this server.</div>
<div class="flex gap-2">
<x-forms.button class="w-32 box" wire:click="select_proxy('{{ ProxyTypes::TRAEFIK_V2 }}')">
<x-forms.button class="w-32 box" wire:click="select_proxy('NONE')">
Custom (None)
</x-forms.button>
<x-forms.button class="w-32 box" wire:click="select_proxy('TRAEFIK_V2')">
Traefik
v2
</x-forms.button>

View File

@ -1,7 +1,7 @@
<div>
<div class="flex items-end gap-2 pb-6 ">
<h2>Private Key</h2>
<a href="{{ route('private-key.new') }}">
<a href="{{ route('security.private-key.new') }}">
<x-forms.button>Add a new Private Key</x-forms.button>
</a>
<x-forms.button wire:click.prevent='checkConnection'>
@ -13,7 +13,7 @@
@if (data_get($server, 'privateKey.uuid'))
<div>
Currently attached Private Key:
<a href="{{ route('private-key.show', ['private_key_uuid' => data_get($server, 'privateKey.uuid')]) }}">
<a href="{{ route('security.private-key.show', ['private_key_uuid' => data_get($server, 'privateKey.uuid')]) }}">
<button class="text-white btn-link">{{ data_get($server, 'privateKey.name') }}</button>
</a>
</div>
@ -29,7 +29,7 @@
</div>
@empty
<div>No private keys found.
<x-use-magic-bar />
<x-use-magic-bar link="/security/private-key/new" />
</div>
@endforelse
</div>

View File

@ -43,40 +43,41 @@
Install Repositories on GitHub
</a>
@else
<div class="w-48">
<x-forms.checkbox label="System Wide?"
helper="If checked, this GitHub App will be available for everyone in this Coolify instance."
instantSave id="is_system_wide" />
</div>
<div class="flex gap-2">
<x-forms.input id="github_app.name" label="App Name" disabled />
<x-forms.input id="github_app.organization" label="Organization" disabled
placeholder="If empty, personal user will be used" />
</div>
<div class="flex gap-2">
<x-forms.input id="github_app.html_url" label="HTML Url" disabled />
<x-forms.input id="github_app.api_url" label="API Url" disabled />
</div>
<div class="flex gap-2">
@if ($github_app->html_url === 'https://github.com')
<x-forms.input id="github_app.custom_user" label="User" disabled />
<x-forms.input type="number" id="github_app.custom_port" label="Port" disabled />
@else
<x-forms.input id="github_app.custom_user" label="User" required />
<x-forms.input type="number" id="github_app.custom_port" label="Port" required />
@if (!isCloud())
<div class="w-48">
<x-forms.checkbox label="System Wide?"
helper="If checked, this GitHub App will be available for everyone in this Coolify instance."
instantSave id="is_system_wide" />
</div>
@endif
</div>
<div class="flex gap-2">
<x-forms.input type="number" id="github_app.app_id" label="App Id" disabled />
<x-forms.input type="number" id="github_app.installation_id" label="Installation Id" disabled />
</div>
<div class="flex gap-2">
<x-forms.input id="github_app.client_id" label="Client Id" type="password" disabled />
<x-forms.input id="github_app.client_secret" label="Client Secret" type="password" />
<x-forms.input id="github_app.webhook_secret" label="Webhook Secret" type="password" />
</div>
<div class="flex gap-2">
<x-forms.input id="github_app.name" label="App Name" disabled />
<x-forms.input id="github_app.organization" label="Organization" disabled
placeholder="If empty, personal user will be used" />
</div>
<div class="flex gap-2">
<x-forms.input id="github_app.html_url" label="HTML Url" disabled />
<x-forms.input id="github_app.api_url" label="API Url" disabled />
</div>
<div class="flex gap-2">
@if ($github_app->html_url === 'https://github.com')
<x-forms.input id="github_app.custom_user" label="User" disabled />
<x-forms.input type="number" id="github_app.custom_port" label="Port" disabled />
@else
<x-forms.input id="github_app.custom_user" label="User" required />
<x-forms.input type="number" id="github_app.custom_port" label="Port" required />
@endif
</div>
<div class="flex gap-2">
<x-forms.input type="number" id="github_app.app_id" label="App Id" disabled />
<x-forms.input type="number" id="github_app.installation_id" label="Installation Id" disabled />
</div>
<div class="flex gap-2">
<x-forms.input id="github_app.client_id" label="Client Id" type="password" disabled />
<x-forms.input id="github_app.client_secret" label="Client Secret" type="password" />
<x-forms.input id="github_app.webhook_secret" label="Webhook Secret" type="password" />
</div>
@endif
@else
<div class="mb-10 rounded alert alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"

View File

@ -14,8 +14,10 @@
<x-forms.input id="custom_user" label="Custom Git User" required />
<x-forms.input id="custom_port" label="Custom Git Port" required />
</div>
<x-forms.checkbox class="pt-2" id="is_system_wide" label="System Wide" />
<x-forms.button type="submit">
@if (!isCloud())
<x-forms.checkbox class="pt-2" id="is_system_wide" label="System Wide" />
@endif
<x-forms.button class="mt-4" type="submit">
Save New Source
</x-forms.button>
</form>

View File

@ -24,8 +24,11 @@
<x-forms.button type="submit">Join Waitlist</x-forms.button>
</form>
Waiting in the line: <span class="font-bold text-warning">{{ $waitingInLine }}</span>
<div class="pt-8">
This is a paid & hosted version of Coolify.<br> See the pricing <a href="https://coolify.io/pricing" class="text-warning">here</a>.
</div>
<div class="pt-4">
See the pricing <a href="https://coolify.io/pricing" class="text-warning">here</a>.
If you are looking for the self-hosted version go <a href="https://coolify.io" class="text-warning">here</a>.
</div>
</div>
</div>

View File

@ -1,5 +0,0 @@
<x-layout>
<h1>Create a new Private Key</h1>
<div class="subtitle ">Private Keys are used for connection to servers.</div>
<livewire:private-key.create />
</x-layout>

View File

@ -0,0 +1,9 @@
<x-layout>
<x-security.navbar />
<a class="text-center hover:no-underline group"
href="{{ route('security.private-key.index')}}">
<div class="group-hover:text-white">
<div>Private Keys</div>
</div>
</a>
</x-layout>

View File

@ -1,10 +1,13 @@
<x-layout>
<h1>Private Keys</h1>
<div class="subtitle ">All Private Keys</div>
<x-security.navbar />
<div class="flex gap-2">
<h2 class="pb-4">Private Keys</h2>
<a href="{{ route('security.private-key.new') }}"><x-forms.button>+ Add</x-forms.button></a>
</div>
<div class="grid gap-2 lg:grid-cols-2">
@forelse ($privateKeys as $key)
<a class="text-center hover:no-underline box group"
href="{{ route('private-key.show', ['private_key_uuid' => data_get($key, 'uuid')]) }}">
href="{{ route('security.private-key.show', ['private_key_uuid' => data_get($key, 'uuid')]) }}">
<div class="group-hover:text-white">
<div>{{ $key->name }}</div>
</div>
@ -12,7 +15,7 @@
@empty
<div>
<div>No private keys found.</div>
<x-use-magic-bar link="/private-key/new" />
<x-use-magic-bar link="/security/private-key/new" />
</div>
@endforelse
</div>

View File

@ -0,0 +1,5 @@
<x-layout>
<h1>Create a new Private Key</h1>
<div class="subtitle ">Private Keys are used to connect to your servers without passwords.</div>
<livewire:private-key.create />
</x-layout>

View File

@ -1,3 +1,4 @@
<x-layout>
<x-security.navbar />
<livewire:private-key.change :private_key="$private_key" />
</x-layout>

View File

@ -30,7 +30,7 @@
@empty
<div>
<div>No sources found.</div>
<x-use-magic-bar />
<x-use-magic-bar link="/source/new" />
</div>
@endforelse
</div>

View File

@ -1,8 +1,8 @@
<x-layout>
<h1>New Source</h1>
<div class="subtitle ">Add source providers for your applications.</div>
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : '' }">
<div class="flex justify-center h-full gap-2 pb-6">
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'github' }">
{{-- <div class="flex justify-center h-full gap-2 pb-6">
<a class="flex items-center justify-center w-1/2 p-2 transition-colors rounded-none min-h-12 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline"
:class="activeTab === 'github' && 'bg-coollabs text-white'"
@click.prevent="activeTab = 'github'; window.location.hash = 'github'" href="#">GitHub
@ -11,12 +11,12 @@
:class="activeTab === 'gitlab' && 'bg-coollabs text-white'"
@click.prevent="activeTab = 'gitlab'; window.location.hash = 'gitlab'" href="#">GitLab
</a>
</div>
</div> --}}
<div x-cloak x-show="activeTab === 'github'" class="h-full">
<livewire:source.github.create />
</div>
<div x-cloak x-show="activeTab === 'gitlab'" class="h-full">
{{-- <div x-cloak x-show="activeTab === 'gitlab'" class="h-full">
WIP
</div>
</div> --}}
</div>
</x-layout>

View File

@ -1,7 +1,5 @@
<x-layout>
<x-team.navbar :team="auth()
->user()
->currentTeam()" />
<x-team.navbar />
<livewire:team.form />
@if (isCloud())
<div class="pb-8">

View File

@ -1,7 +1,5 @@
<x-layout>
<x-team.navbar :team="auth()
->user()
->currentTeam()" />
<x-team.navbar />
<h2>Members</h2>
<div class="pt-4 overflow-hidden">
<table>

View File

@ -1,25 +1,25 @@
<x-layout>
<x-team.navbar :team="auth()
->user()
->currentTeam()" />
<x-team.navbar />
<h2 class="pb-4">Notifications</h2>
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'email' }" class="flex h-full">
<div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'email' && 'text-white'"
@click.prevent="activeTab = 'email'; window.location.hash = 'email'" href="#">Email</a>
<a :class="activeTab === 'Telegram' && 'text-white'"
@click.prevent="activeTab = 'telegram'; window.location.hash = 'telegram'" href="#">Telegram</a>
<a :class="activeTab === 'discord' && 'text-white'"
@click.prevent="activeTab = 'discord'; window.location.hash = 'discord'" href="#">Discord</a>
</div>
<div class="w-full pl-8">
<div x-cloak x-show="activeTab === 'email'" class="h-full">
<livewire:notifications.email-settings :team="auth()
->user()
->currentTeam()" />
<livewire:notifications.email-settings />
</div>
<div x-cloak x-show="activeTab === 'telegram'" class="h-full">
<livewire:notifications.telegram-settings />
</div>
<div x-cloak x-show="activeTab === 'discord'">
<livewire:notifications.discord-settings :model="auth()
->user()
->currentTeam()" />
<livewire:notifications.discord-settings />
</div>
</div>
</x-layout>

View File

@ -9,6 +9,7 @@ use App\Http\Controllers\ServerController;
use App\Http\Livewire\Boarding\Index;
use App\Http\Livewire\Boarding\Server as BoardingServer;
use App\Http\Livewire\Dashboard;
use App\Http\Livewire\Help;
use App\Http\Livewire\Server\All;
use App\Http\Livewire\Server\Show;
use App\Http\Livewire\Waitlist\Index as WaitlistIndex;
@ -48,7 +49,9 @@ Route::post('/forgot-password', function (Request $request) {
return response()->json(['message' => 'Transactional emails are not active'], 400);
})->name('password.forgot');
Route::get('/waitlist', WaitlistIndex::class)->name('waitlist.index');
Route::middleware(['throttle:login'])->group(function() {
Route::get('/auth/link', [Controller::class, 'link'])->name('auth.link');
});
Route::prefix('magic')->middleware(['auth'])->group(function () {
Route::get('/servers', [MagicController::class, 'servers']);
Route::get('/destinations', [MagicController::class, 'destinations']);
@ -103,6 +106,7 @@ Route::middleware(['auth'])->group(function () {
Route::get('/force-password-reset', [Controller::class, 'force_passoword_reset'])->name('auth.force-password-reset');
});
Route::get('/subscription', [Controller::class, 'subscription'])->name('subscription.index');
// Route::get('/help', Help::class)->name('help');
Route::get('/settings', [Controller::class, 'settings'])->name('settings.configuration');
Route::get('/settings/license', [Controller::class, 'license'])->name('settings.license');
Route::get('/profile', fn () => view('profile', ['request' => request()]))->name('profile');
@ -119,13 +123,14 @@ Route::middleware(['auth'])->group(function () {
});
Route::middleware(['auth'])->group(function () {
Route::get('/private-keys', fn () => view('private-key.all', [
Route::get('/security', fn () => view('security.index'))->name('security.index');
Route::get('/security/private-key', fn () => view('security.private-key.index', [
'privateKeys' => PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related'])->where('is_git_related', false)->get()
]))->name('private-key.all');
Route::get('/private-key/new', fn () => view('private-key.new'))->name('private-key.new');
Route::get('/private-key/{private_key_uuid}', fn () => view('private-key.show', [
]))->name('security.private-key.index');
Route::get('/security/private-key/new', fn () => view('security.private-key.new'))->name('security.private-key.new');
Route::get('/security/private-key/{private_key_uuid}', fn () => view('security.private-key.show', [
'private_key' => PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail()
]))->name('private-key.show');
]))->name('security.private-key.show');
});

View File

@ -225,6 +225,7 @@ Route::post('/payments/stripe/events', function () {
]);
$type = data_get($event, 'type');
$data = data_get($event, 'data.object');
ray('Event: '. $type);
switch ($type) {
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
@ -239,12 +240,14 @@ Route::post('/payments/stripe/events', function () {
}
$subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) {
send_internal_notification('Old subscription activated for team: ' . $teamId);
$subscription->update([
'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true,
]);
} else {
send_internal_notification('New subscription for team: ' . $teamId);
Subscription::create([
'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId,
@ -254,43 +257,67 @@ Route::post('/payments/stripe/events', function () {
}
break;
case 'invoice.paid':
$subscriptionId = data_get($data, 'lines.data.0.subscription');
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail();
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$subscription->update([
'stripe_invoice_paid' => true,
]);
break;
case 'invoice.payment_failed':
// case 'invoice.payment_failed':
// $customerId = data_get($data, 'customer');
// $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
// if ($subscription) {
// SubscriptionInvoiceFailedJob::dispatch($subscription->team);
// }
// break;
case 'customer.subscription.updated':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
SubscriptionInvoiceFailedJob::dispatch($subscription->team);
break;
case 'customer.subscription.updated':
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail();
$alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end');
$feedback = data_get($data, 'cancellation_details.feedback');
$comment = data_get($data, 'cancellation_details.comment');
$subscription->update([
'stripe_feedback' => $feedback,
'stripe_comment' => $comment,
'stripe_plan_id' => $planId,
'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
]);
ray($feedback, $comment, $alreadyCancelAtPeriodEnd, $cancelAtPeriodEnd);
if ($feedback) {
$reason = "Cancellation feedback for {$subscription->team->id}: '" . $feedback ."'";
if ($comment) {
$reason .= ' with comment: \'' . $comment ."'";
}
send_internal_notification($reason);
}
ray($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd);
if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) {
if ($cancelAtPeriodEnd) {
send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id);
} else {
send_internal_notification('Subscription resumed for team: ' . $subscription->team->id);
}
}
break;
case 'customer.subscription.deleted':
$subscriptionId = data_get($data, 'items.data.0.subscription');
$subscription = Subscription::where('stripe_subscription_id', $subscriptionId)->firstOrFail();
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$subscription->update([
'stripe_subscription_id' => null,
'stripe_plan_id'=> null,
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
]);
send_internal_notification('Subscription cancelled: ' . $subscription->team->id);
break;
default:
// Unhandled event type
}
} catch (Exception $e) {
ray($e->getMessage());
send_internal_notification('Subscription webhook failed: ' . $e->getMessage());
send_internal_notification("Subscription webhook ($type) failed: " . $e->getMessage());
$webhook->update([
'status' => 'failed',
'failure_reason' => $e->getMessage(),

View File

@ -0,0 +1,2 @@
psql -U postgres -c "create table if not exists test (id SERIAL UNIQUE NOT NULL,article TEXT);"
psql -U postgres -c "insert into test (article) select md5(random()::text) from generate_series(1, 1000000) s(i);"

Some files were not shown because too many files have changed in this diff Show More