commit
38f9761b67
@ -6,6 +6,7 @@
|
||||
USERID=
|
||||
GROUPID=
|
||||
############################################################################################################
|
||||
APP_NAME=Coolify-localhost
|
||||
APP_ID=development
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
|
84
.github/workflows/coolify-helper-next.yml
vendored
Normal file
84
.github/workflows/coolify-helper-next.yml
vendored
Normal 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 }}
|
8
.github/workflows/coolify-helper.yml
vendored
8
.github/workflows/coolify-helper.yml
vendored
@ -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 }}
|
||||
|
5
.github/workflows/development-build.yml
vendored
5
.github/workflows/development-build.yml
vendored
@ -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 }}
|
||||
|
@ -65,7 +65,7 @@ class StartPostgresql
|
||||
],
|
||||
'networks' => [
|
||||
$this->database->destination->network => [
|
||||
'external' => false,
|
||||
'external' => true,
|
||||
'name' => $this->database->destination->network,
|
||||
'attachable' => true,
|
||||
]
|
||||
|
@ -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...",
|
||||
|
@ -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);
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ namespace App\Enums;
|
||||
|
||||
enum ProxyTypes: string
|
||||
{
|
||||
case NONE = 'NONE';
|
||||
case TRAEFIK_V2 = 'TRAEFIK_V2';
|
||||
case NGINX = 'NGINX';
|
||||
case CADDY = 'CADDY';
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
54
app/Http/Livewire/Help.php
Normal file
54
app/Http/Livewire/Help.php
Normal 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');
|
||||
}
|
||||
}
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
62
app/Http/Livewire/Notifications/TelegramSettings.php
Normal file
62
app/Http/Livewire/Notifications/TelegramSettings.php
Normal 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.');
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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', [
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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]]);
|
||||
}
|
||||
|
@ -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]),
|
||||
|
@ -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') {
|
||||
|
@ -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],
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
|
72
app/Jobs/SendMessageToTelegramJob.php
Normal file
72
app/Jobs/SendMessageToTelegramJob.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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'));
|
||||
}
|
||||
|
9
app/Notifications/Channels/SendsTelegram.php
Normal file
9
app/Notifications/Channels/SendsTelegram.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications\Channels;
|
||||
|
||||
interface SendsTelegram
|
||||
{
|
||||
public function routeNotificationForTelegram();
|
||||
|
||||
}
|
25
app/Notifications/Channels/TelegramChannel.php
Normal file
25
app/Notifications/Channels/TelegramChannel.php
Normal 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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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.'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -47,6 +47,9 @@ function getEndDate()
|
||||
|
||||
function isSubscriptionActive()
|
||||
{
|
||||
if (!isCloud()) {
|
||||
return false;
|
||||
}
|
||||
$team = currentTeam();
|
||||
if (!$team) {
|
||||
return false;
|
||||
|
@ -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'),
|
||||
];
|
||||
|
@ -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'),
|
||||
|
@ -1,3 +1,3 @@
|
||||
<?php
|
||||
|
||||
return '4.0.0-beta.23';
|
||||
return '4.0.0-beta.24';
|
||||
|
32
database/migrations/2023_08_22_071054_add_stripe_reasons.php
Normal file
32
database/migrations/2023_08_22_071054_add_stripe_reasons.php
Normal 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');
|
||||
});
|
||||
}
|
||||
};
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
@ -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}");
|
||||
|
@ -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:
|
||||
|
@ -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"]
|
||||
|
||||
|
@ -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"]
|
||||
|
||||
|
@ -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`
|
||||
|
@ -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>
|
||||
|
@ -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)') }}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -43,10 +43,6 @@
|
||||
@endisset
|
||||
@if ($modalSubmit)
|
||||
{{ $modalSubmit }}
|
||||
@else
|
||||
<x-forms.button onclick="{{ $modalId }}.close()" type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
@endif
|
||||
|
||||
</form>
|
||||
|
@ -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
|
||||
|
17
resources/views/components/security/navbar.blade.php
Normal file
17
resources/views/components/security/navbar.blade.php
Normal 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>
|
5
resources/views/emails/help.blade.php
Normal file
5
resources/views/emails/help.blade.php
Normal file
@ -0,0 +1,5 @@
|
||||
{{ $description }}
|
||||
|
||||
{{ Illuminate\Mail\Markdown::parse('---') }}
|
||||
|
||||
{{ Illuminate\Mail\Markdown::parse($debug) }}
|
@ -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.
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 couldn’t find the page you’re 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 couldn’t find the page you’re 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>
|
||||
|
@ -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 couldn’t find the page you’re 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">→</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 couldn’t find the page you’re 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">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-layout>
|
||||
</div>
|
||||
|
@ -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">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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">→</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -35,5 +35,5 @@
|
||||
<div class="stat-value">{{ $s3s }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
|
||||
</div>
|
||||
|
10
resources/views/livewire/help.blade.php
Normal file
10
resources/views/livewire/help.blade.php
Normal 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>
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
9
resources/views/security/index.blade.php
Normal file
9
resources/views/security/index.blade.php
Normal 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>
|
@ -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>
|
5
resources/views/security/private-key/new.blade.php
Normal file
5
resources/views/security/private-key/new.blade.php
Normal 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>
|
@ -1,3 +1,4 @@
|
||||
<x-layout>
|
||||
<x-security.navbar />
|
||||
<livewire:private-key.change :private_key="$private_key" />
|
||||
</x-layout>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
||||
|
@ -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(),
|
||||
|
2
scripts/generate_pg_data.sh
Normal file
2
scripts/generate_pg_data.sh
Normal 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
Loading…
x
Reference in New Issue
Block a user