mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-18 08:14:35 +00:00
Merge branch 'next' into feat/copy-resource-logs-with-sanitization
This commit is contained in:
85 files changed
+8842
-1086
No files matched your search
@@ -30,7 +30,6 @@ class UpdateCoolify
|
||||
if (! $this->server) {
|
||||
return;
|
||||
}
|
||||
CleanupDocker::dispatch($this->server, false, false);
|
||||
|
||||
// Fetch fresh version from CDN instead of using cache
|
||||
try {
|
||||
@@ -117,17 +116,12 @@ class UpdateCoolify
|
||||
|
||||
private function update()
|
||||
{
|
||||
$helperImage = config('constants.coolify.helper_image');
|
||||
$latest_version = getHelperVersion();
|
||||
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
|
||||
|
||||
$image = config('constants.coolify.registry_url').'/coollabsio/coolify:'.$this->latestVersion;
|
||||
instant_remote_process(["docker pull -q $image"], $this->server, false);
|
||||
|
||||
$latestHelperImageVersion = getHelperVersion();
|
||||
$upgradeScriptUrl = config('constants.coolify.upgrade_script_url');
|
||||
|
||||
remote_process([
|
||||
"curl -fsSL {$upgradeScriptUrl} -o /data/coolify/source/upgrade.sh",
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion",
|
||||
"bash /data/coolify/source/upgrade.sh $this->latestVersion $latestHelperImageVersion",
|
||||
], $this->server);
|
||||
}
|
||||
}
|
||||
@@ -309,7 +309,9 @@ class Github extends Controller
|
||||
if (! $id || ! $branch) {
|
||||
return response('Nothing to do. No id or branch found.');
|
||||
}
|
||||
$applications = Application::where('repository_project_id', $id)->whereRelation('source', 'is_public', false);
|
||||
$applications = Application::where('repository_project_id', $id)
|
||||
->where('source_id', $github_app->id)
|
||||
->whereRelation('source', 'is_public', false);
|
||||
if ($x_github_event === 'push') {
|
||||
$applications = $applications->where('git_branch', $branch)->get();
|
||||
if ($applications->isEmpty()) {
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class NavbarDeleteTeam extends Component
|
||||
@@ -19,12 +17,8 @@ class NavbarDeleteTeam extends Component
|
||||
|
||||
public function delete($password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentTeam = currentTeam();
|
||||
|
||||
@@ -20,6 +20,8 @@ class Show extends Component
|
||||
|
||||
public bool $is_debug_enabled = false;
|
||||
|
||||
public bool $fullscreen = false;
|
||||
|
||||
private bool $deploymentFinishedDispatched = false;
|
||||
|
||||
public function getListeners()
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Exception;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
@@ -154,12 +151,8 @@ class BackupEdit extends Component
|
||||
{
|
||||
$this->authorize('manageBackups', $this->backup->database);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class BackupExecutions extends Component
|
||||
@@ -69,12 +67,8 @@ class BackupExecutions extends Component
|
||||
|
||||
public function deleteBackup($executionId, $password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$execution = $this->backup->executions()->where('id', $executionId)->first();
|
||||
|
||||
@@ -128,6 +128,7 @@ class GithubPrivateRepository extends Component
|
||||
$this->loadBranchByPage();
|
||||
}
|
||||
}
|
||||
$this->branches = sortBranchesByPriority($this->branches);
|
||||
$this->selected_branch_name = data_get($this->branches, '0.name', 'main');
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,9 @@ namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Database extends Component
|
||||
@@ -96,12 +93,8 @@ class Database extends Component
|
||||
try {
|
||||
$this->authorize('delete', $this->database);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->database->delete();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
@@ -16,8 +15,6 @@ use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -62,7 +59,7 @@ class FileStorage extends Component
|
||||
$this->fs_path = $this->fileStorage->fs_path;
|
||||
}
|
||||
|
||||
$this->isReadOnly = $this->fileStorage->isReadOnlyVolume();
|
||||
$this->isReadOnly = $this->fileStorage->shouldBeReadOnlyInUI();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
@@ -104,7 +101,8 @@ class FileStorage extends Component
|
||||
public function loadStorageOnServer()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
// Loading content is a read operation, so we use 'view' permission
|
||||
$this->authorize('view', $this->resource);
|
||||
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
$this->syncData();
|
||||
@@ -140,12 +138,8 @@ class FileStorage extends Component
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
@@ -128,12 +125,8 @@ class ServiceApplicationView extends Component
|
||||
try {
|
||||
$this->authorize('delete', $this->application);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->application->delete();
|
||||
|
||||
@@ -67,7 +67,7 @@ class Storage extends Component
|
||||
public function refreshStorages()
|
||||
{
|
||||
$this->fileStorage = $this->resource->fileStorages()->get();
|
||||
$this->resource->refresh();
|
||||
$this->resource->load('persistentStorages.resource');
|
||||
}
|
||||
|
||||
public function getFilesProperty()
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -93,12 +90,8 @@ class Danger extends Component
|
||||
|
||||
public function delete($password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->resource) {
|
||||
|
||||
@@ -5,12 +5,9 @@ namespace App\Livewire\Project\Shared;
|
||||
use App\Actions\Application\StopApplicationOneServer;
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Events\ApplicationStatusChanged;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -140,12 +137,8 @@ class Destination extends Component
|
||||
public function removeServer(int $network_id, int $server_id, $password)
|
||||
{
|
||||
try {
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared\Storages;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
@@ -67,7 +64,7 @@ class Show extends Component
|
||||
public function mount()
|
||||
{
|
||||
$this->syncData(false);
|
||||
$this->isReadOnly = $this->storage->isReadOnlyVolume();
|
||||
$this->isReadOnly = $this->storage->shouldBeReadOnlyInUI();
|
||||
}
|
||||
|
||||
public function submit()
|
||||
@@ -84,12 +81,8 @@ class Show extends Component
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->storage->delete();
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\DeleteServer;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Delete extends Component
|
||||
@@ -29,12 +26,8 @@ class Delete extends Component
|
||||
|
||||
public function delete($password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->authorize('delete', $this->server);
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
namespace App\Livewire\Server\Security;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -44,13 +41,9 @@ class TerminalAccess extends Component
|
||||
throw new \Exception('Only team administrators and owners can modify terminal access.');
|
||||
}
|
||||
|
||||
// Verify password unless two-step confirmation is disabled
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
// Verify password
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle the terminal setting
|
||||
|
||||
@@ -5,9 +5,12 @@ namespace App\Livewire\Server;
|
||||
use App\Actions\Server\StartSentinel;
|
||||
use App\Actions\Server\StopSentinel;
|
||||
use App\Events\ServerReachabilityChanged;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\Server;
|
||||
use App\Services\HetznerService;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
@@ -73,6 +76,17 @@ class Show extends Component
|
||||
|
||||
public bool $isValidating = false;
|
||||
|
||||
// Hetzner linking properties
|
||||
public Collection $availableHetznerTokens;
|
||||
|
||||
public ?int $selectedHetznerTokenId = null;
|
||||
|
||||
public ?array $matchedHetznerServer = null;
|
||||
|
||||
public ?string $hetznerSearchError = null;
|
||||
|
||||
public bool $hetznerNoMatchFound = false;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = $this->server->team_id ?? auth()->user()->currentTeam()->id;
|
||||
@@ -150,6 +164,9 @@ class Show extends Component
|
||||
$this->hetznerServerStatus = $this->server->hetzner_server_status;
|
||||
$this->isValidating = $this->server->is_validating ?? false;
|
||||
|
||||
// Load Hetzner tokens for linking
|
||||
$this->loadHetznerTokens();
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -465,6 +482,98 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function loadHetznerTokens(): void
|
||||
{
|
||||
$this->availableHetznerTokens = CloudProviderToken::ownedByCurrentTeam()
|
||||
->where('provider', 'hetzner')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function searchHetznerServer(): void
|
||||
{
|
||||
$this->hetznerSearchError = null;
|
||||
$this->hetznerNoMatchFound = false;
|
||||
$this->matchedHetznerServer = null;
|
||||
|
||||
if (! $this->selectedHetznerTokenId) {
|
||||
$this->hetznerSearchError = 'Please select a Hetzner token.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
$token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId);
|
||||
if (! $token) {
|
||||
$this->hetznerSearchError = 'Invalid token selected.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
$matched = $hetznerService->findServerByIp($this->server->ip);
|
||||
|
||||
if ($matched) {
|
||||
$this->matchedHetznerServer = $matched;
|
||||
} else {
|
||||
$this->hetznerNoMatchFound = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->hetznerSearchError = 'Failed to search Hetzner servers: '.$e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function linkToHetzner()
|
||||
{
|
||||
if (! $this->matchedHetznerServer) {
|
||||
$this->dispatch('error', 'No Hetzner server selected.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
|
||||
$token = $this->availableHetznerTokens->firstWhere('id', $this->selectedHetznerTokenId);
|
||||
if (! $token) {
|
||||
$this->dispatch('error', 'Invalid token selected.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the server exists and is accessible with the token
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
$serverData = $hetznerService->getServer($this->matchedHetznerServer['id']);
|
||||
|
||||
if (empty($serverData)) {
|
||||
$this->dispatch('error', 'Could not find Hetzner server with ID: '.$this->matchedHetznerServer['id']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the server with Hetzner details
|
||||
$this->server->update([
|
||||
'cloud_provider_token_id' => $this->selectedHetznerTokenId,
|
||||
'hetzner_server_id' => $this->matchedHetznerServer['id'],
|
||||
'hetzner_server_status' => $serverData['status'] ?? null,
|
||||
]);
|
||||
|
||||
$this->hetznerServerStatus = $serverData['status'] ?? null;
|
||||
|
||||
// Clear the linking state
|
||||
$this->matchedHetznerServer = null;
|
||||
$this->selectedHetznerTokenId = null;
|
||||
$this->hetznerNoMatchFound = false;
|
||||
$this->hetznerSearchError = null;
|
||||
|
||||
$this->dispatch('success', 'Server successfully linked to Hetzner Cloud!');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.show');
|
||||
|
||||
@@ -5,8 +5,6 @@ namespace App\Livewire\Settings;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Rules\ValidIpOrCidr;
|
||||
use Auth;
|
||||
use Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -157,9 +155,7 @@ class Advanced extends Component
|
||||
|
||||
public function toggleTwoStepConfirmation($password): bool
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Team;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class AdminView extends Component
|
||||
@@ -58,12 +55,8 @@ class AdminView extends Component
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! auth()->user()->isInstanceAdmin()) {
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire;
|
||||
|
||||
use App\Actions\Server\UpdateCoolify;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Upgrade extends Component
|
||||
@@ -14,12 +15,20 @@ class Upgrade extends Component
|
||||
|
||||
public string $latestVersion = '';
|
||||
|
||||
public string $currentVersion = '';
|
||||
|
||||
protected $listeners = ['updateAvailable' => 'checkUpdate'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
}
|
||||
|
||||
public function checkUpdate()
|
||||
{
|
||||
try {
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
$this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false);
|
||||
if (isDev()) {
|
||||
$this->isUpgradeAvailable = true;
|
||||
@@ -41,4 +50,71 @@ class Upgrade extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function getUpgradeStatus(): array
|
||||
{
|
||||
// Only root team members can view upgrade status
|
||||
if (auth()->user()?->currentTeam()?->id !== 0) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
$server = Server::find(0);
|
||||
if (! $server) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
$statusFile = '/data/coolify/source/.upgrade-status';
|
||||
|
||||
try {
|
||||
$content = instant_remote_process(
|
||||
["cat {$statusFile} 2>/dev/null || echo ''"],
|
||||
$server,
|
||||
false
|
||||
);
|
||||
$content = trim($content ?? '');
|
||||
} catch (\Throwable $e) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
if (empty($content)) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
$parts = explode('|', $content);
|
||||
if (count($parts) < 3) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
[$step, $message, $timestamp] = $parts;
|
||||
|
||||
// Check if status is stale (older than 10 minutes)
|
||||
try {
|
||||
$statusTime = new \DateTime($timestamp);
|
||||
$now = new \DateTime;
|
||||
$diffMinutes = ($now->getTimestamp() - $statusTime->getTimestamp()) / 60;
|
||||
|
||||
if ($diffMinutes > 10) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return ['status' => 'none'];
|
||||
}
|
||||
|
||||
if ($step === 'error') {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'step' => 0,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
|
||||
$stepInt = (int) $step;
|
||||
$status = $stepInt >= 6 ? 'complete' : 'in_progress';
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'step' => $stepInt,
|
||||
'message' => $message,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -209,6 +209,23 @@ class LocalFileVolume extends BaseModel
|
||||
return $query->get()->where('plain_mount_path', $path);
|
||||
}
|
||||
|
||||
// Check if this volume belongs to a service resource
|
||||
public function isServiceResource(): bool
|
||||
{
|
||||
return in_array($this->resource_type, [
|
||||
'App\Models\ServiceApplication',
|
||||
'App\Models\ServiceDatabase',
|
||||
]);
|
||||
}
|
||||
|
||||
// Determine if this volume should be read-only in the UI
|
||||
// File/directory mounts can be edited even for services
|
||||
public function shouldBeReadOnlyInUI(): bool
|
||||
{
|
||||
// Check for explicit :ro flag in compose (existing logic)
|
||||
return $this->isReadOnlyVolume();
|
||||
}
|
||||
|
||||
// Check if this volume is read-only by parsing the docker-compose content
|
||||
public function isReadOnlyVolume(): bool
|
||||
{
|
||||
@@ -239,22 +256,40 @@ class LocalFileVolume extends BaseModel
|
||||
$volumes = $compose['services'][$serviceName]['volumes'];
|
||||
|
||||
// Check each volume to find a match
|
||||
// Note: We match on mount_path (container path) only, since fs_path gets transformed
|
||||
// from relative (./file) to absolute (/data/coolify/services/uuid/file) during parsing
|
||||
foreach ($volumes as $volume) {
|
||||
// Volume can be string like "host:container:ro" or "host:container"
|
||||
if (is_string($volume)) {
|
||||
$parts = explode(':', $volume);
|
||||
|
||||
// Check if this volume matches our fs_path and mount_path
|
||||
// Check if this volume matches our mount_path
|
||||
if (count($parts) >= 2) {
|
||||
$hostPath = $parts[0];
|
||||
$containerPath = $parts[1];
|
||||
$options = $parts[2] ?? null;
|
||||
|
||||
// Match based on fs_path and mount_path
|
||||
if ($hostPath === $this->fs_path && $containerPath === $this->mount_path) {
|
||||
// Match based on mount_path
|
||||
// Remove leading slash from mount_path if present for comparison
|
||||
$mountPath = str($this->mount_path)->ltrim('/')->toString();
|
||||
$containerPathClean = str($containerPath)->ltrim('/')->toString();
|
||||
|
||||
if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) {
|
||||
return $options === 'ro';
|
||||
}
|
||||
}
|
||||
} elseif (is_array($volume)) {
|
||||
// Long-form syntax: { type: bind, source: ..., target: ..., read_only: true }
|
||||
$containerPath = data_get($volume, 'target');
|
||||
$readOnly = data_get($volume, 'read_only', false);
|
||||
|
||||
// Match based on mount_path
|
||||
// Remove leading slash from mount_path if present for comparison
|
||||
$mountPath = str($this->mount_path)->ltrim('/')->toString();
|
||||
$containerPathClean = str($containerPath)->ltrim('/')->toString();
|
||||
|
||||
if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) {
|
||||
return $readOnly === true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ class LocalPersistentVolume extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function resource()
|
||||
{
|
||||
return $this->morphTo('resource');
|
||||
}
|
||||
|
||||
public function application()
|
||||
{
|
||||
return $this->morphTo('resource');
|
||||
@@ -50,6 +55,54 @@ class LocalPersistentVolume extends Model
|
||||
);
|
||||
}
|
||||
|
||||
// Check if this volume belongs to a service resource
|
||||
public function isServiceResource(): bool
|
||||
{
|
||||
return in_array($this->resource_type, [
|
||||
'App\Models\ServiceApplication',
|
||||
'App\Models\ServiceDatabase',
|
||||
]);
|
||||
}
|
||||
|
||||
// Check if this volume belongs to a dockercompose application
|
||||
public function isDockerComposeResource(): bool
|
||||
{
|
||||
if ($this->resource_type !== 'App\Models\Application') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only access relationship if already eager loaded to avoid N+1
|
||||
if (! $this->relationLoaded('resource')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$application = $this->resource;
|
||||
if (! $application) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return data_get($application, 'build_pack') === 'dockercompose';
|
||||
}
|
||||
|
||||
// Determine if this volume should be read-only in the UI
|
||||
// Service volumes and dockercompose application volumes are read-only
|
||||
// (users should edit compose file directly)
|
||||
public function shouldBeReadOnlyInUI(): bool
|
||||
{
|
||||
// All service volumes should be read-only in UI
|
||||
if ($this->isServiceResource()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// All dockercompose application volumes should be read-only in UI
|
||||
if ($this->isDockerComposeResource()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for explicit :ro flag in compose (existing logic)
|
||||
return $this->isReadOnlyVolume();
|
||||
}
|
||||
|
||||
// Check if this volume is read-only by parsing the docker-compose content
|
||||
public function isReadOnlyVolume(): bool
|
||||
{
|
||||
@@ -85,6 +138,7 @@ class LocalPersistentVolume extends Model
|
||||
$volumes = $compose['services'][$serviceName]['volumes'];
|
||||
|
||||
// Check each volume to find a match
|
||||
// Note: We match on mount_path (container path) only, since host paths get transformed
|
||||
foreach ($volumes as $volume) {
|
||||
// Volume can be string like "host:container:ro" or "host:container"
|
||||
if (is_string($volume)) {
|
||||
@@ -104,6 +158,19 @@ class LocalPersistentVolume extends Model
|
||||
return $options === 'ro';
|
||||
}
|
||||
}
|
||||
} elseif (is_array($volume)) {
|
||||
// Long-form syntax: { type: bind/volume, source: ..., target: ..., read_only: true }
|
||||
$containerPath = data_get($volume, 'target');
|
||||
$readOnly = data_get($volume, 'read_only', false);
|
||||
|
||||
// Match based on mount_path
|
||||
// Remove leading slash from mount_path if present for comparison
|
||||
$mountPath = str($this->mount_path)->ltrim('/')->toString();
|
||||
$containerPathClean = str($containerPath)->ltrim('/')->toString();
|
||||
|
||||
if ($mountPath === $containerPathClean || $this->mount_path === $containerPath) {
|
||||
return $readOnly === true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,28 @@ class S3Storage extends BaseModel
|
||||
'secret' => 'encrypted',
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot the model and register event listeners.
|
||||
*/
|
||||
protected static function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Trim whitespace from credentials before saving to prevent
|
||||
// "Malformed Access Key Id" errors from accidental whitespace in pasted values.
|
||||
// Note: We use the saving event instead of Attribute mutators because key/secret
|
||||
// use Laravel's 'encrypted' cast. Attribute mutators fire before casts, which
|
||||
// would cause issues with the encryption/decryption cycle.
|
||||
static::saving(function (S3Storage $storage) {
|
||||
if ($storage->key !== null) {
|
||||
$storage->key = trim($storage->key);
|
||||
}
|
||||
if ($storage->secret !== null) {
|
||||
$storage->secret = trim($storage->secret);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam(array $select = ['*'])
|
||||
{
|
||||
$selectArray = collect($select)->concat(['id']);
|
||||
@@ -55,6 +77,36 @@ class S3Storage extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim whitespace from endpoint to prevent malformed URLs.
|
||||
*/
|
||||
protected function endpoint(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (?string $value) => $value ? trim($value) : null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim whitespace from bucket name to prevent connection errors.
|
||||
*/
|
||||
protected function bucket(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (?string $value) => $value ? trim($value) : null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim whitespace from region to prevent connection errors.
|
||||
*/
|
||||
protected function region(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (?string $value) => $value ? trim($value) : null,
|
||||
);
|
||||
}
|
||||
|
||||
public function testConnection(bool $shouldSave = false)
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -443,4 +443,13 @@ class User extends Authenticatable implements SendsEmail
|
||||
&& $this->email_change_code_expires_at
|
||||
&& Carbon::now()->lessThan($this->email_change_code_expires_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has a password set.
|
||||
* OAuth users are created without passwords.
|
||||
*/
|
||||
public function hasPassword(): bool
|
||||
{
|
||||
return ! empty($this->password);
|
||||
}
|
||||
}
|
||||
@@ -43,21 +43,26 @@ class EmailChannel
|
||||
throw new Exception('No email recipients found');
|
||||
}
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
// Check if the recipient is part of the team
|
||||
if (! $members->contains('email', $recipient)) {
|
||||
$emailSettings = $notifiable->emailNotificationSettings;
|
||||
data_set($emailSettings, 'smtp_password', '********');
|
||||
data_set($emailSettings, 'resend_api_key', '********');
|
||||
send_internal_notification(sprintf(
|
||||
"Recipient is not part of the team: %s\nTeam: %s\nNotification: %s\nNotifiable: %s\nEmail Settings:\n%s",
|
||||
$recipient,
|
||||
$team,
|
||||
get_class($notification),
|
||||
get_class($notifiable),
|
||||
json_encode($emailSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
|
||||
));
|
||||
throw new Exception('Recipient is not part of the team');
|
||||
// Skip team membership validation for test notifications
|
||||
$isTestNotification = data_get($notification, 'isTestNotification', false);
|
||||
|
||||
if (! $isTestNotification) {
|
||||
foreach ($recipients as $recipient) {
|
||||
// Check if the recipient is part of the team
|
||||
if (! $members->contains('email', $recipient)) {
|
||||
$emailSettings = $notifiable->emailNotificationSettings;
|
||||
data_set($emailSettings, 'smtp_password', '********');
|
||||
data_set($emailSettings, 'resend_api_key', '********');
|
||||
send_internal_notification(sprintf(
|
||||
"Recipient is not part of the team: %s\nTeam: %s\nNotification: %s\nNotifiable: %s\nEmail Settings:\n%s",
|
||||
$recipient,
|
||||
$team,
|
||||
get_class($notification),
|
||||
get_class($notifiable),
|
||||
json_encode($emailSettings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
|
||||
));
|
||||
throw new Exception('Recipient is not part of the team');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ class Test extends Notification implements ShouldQueue
|
||||
|
||||
public $tries = 5;
|
||||
|
||||
public bool $isTestNotification = true;
|
||||
|
||||
public function __construct(public ?string $emails = null, public ?string $channel = null, public ?bool $ping = false)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
|
||||
@@ -8,6 +8,8 @@ use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class Test extends CustomEmailNotification
|
||||
{
|
||||
public bool $isTestNotification = true;
|
||||
|
||||
public function __construct(public string $emails, public bool $isTransactionalEmail = true)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
|
||||
@@ -161,4 +161,30 @@ class HetznerService
|
||||
{
|
||||
$this->request('delete', "/servers/{$serverId}");
|
||||
}
|
||||
|
||||
public function getServers(): array
|
||||
{
|
||||
return $this->requestPaginated('get', '/servers', 'servers');
|
||||
}
|
||||
|
||||
public function findServerByIp(string $ip): ?array
|
||||
{
|
||||
$servers = $this->getServers();
|
||||
|
||||
foreach ($servers as $server) {
|
||||
// Check IPv4
|
||||
$ipv4 = data_get($server, 'public_net.ipv4.ip');
|
||||
if ($ipv4 === $ip) {
|
||||
return $server;
|
||||
}
|
||||
|
||||
// Check IPv6 (Hetzner returns the full /64 block)
|
||||
$ipv6 = data_get($server, 'public_net.ipv6.ip');
|
||||
if ($ipv6 && str_starts_with($ip, rtrim($ipv6, '/'))) {
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user