mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 12:14:07 +00:00
Merge remote-tracking branch 'origin/next' into harden-database-import-files
This commit is contained in:
commit
7d65a4b496
550 files changed
+18078
-5008
No files matched your search
@@ -54,6 +54,9 @@ class Index extends Component
|
||||
|
||||
public function getSubscribers()
|
||||
{
|
||||
if (Auth::id() !== 0 && ! session('impersonating')) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
$this->inactiveSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', false)->count();
|
||||
$this->activeSubscribers = Team::whereRelation('subscription', 'stripe_invoice_paid', true)->count();
|
||||
}
|
||||
|
||||
@@ -9,13 +9,15 @@ use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Services\ConfigurationRepository;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
protected $listeners = [
|
||||
'refreshBoardingIndex' => 'validateServer',
|
||||
'prerequisitesInstalled' => 'handlePrerequisitesInstalled',
|
||||
@@ -174,6 +176,9 @@ class Index extends Component
|
||||
|
||||
public function skipBoarding()
|
||||
{
|
||||
if (auth()->user()?->isMember()) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
Team::find(currentTeam()->id)->update([
|
||||
'show_boarding' => false,
|
||||
]);
|
||||
@@ -276,6 +281,7 @@ class Index extends Component
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->authorize('create', PrivateKey::class);
|
||||
$privateKey = PrivateKey::createAndStore([
|
||||
'name' => $this->privateKeyName,
|
||||
'description' => $this->privateKeyDescription,
|
||||
@@ -294,6 +300,12 @@ class Index extends Component
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
try {
|
||||
$this->authorize('create', Server::class);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$this->privateKey = formatPrivateKey($this->privateKey);
|
||||
$foundServer = Server::whereIp($this->remoteServerHost)->first();
|
||||
if ($foundServer) {
|
||||
@@ -457,7 +469,7 @@ class Index extends Component
|
||||
$this->createdProject = Project::create([
|
||||
'name' => 'My first project',
|
||||
'team_id' => currentTeam()->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
$this->currentState = 'create-resource';
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Docker extends Component
|
||||
{
|
||||
@@ -35,7 +34,7 @@ class Docker extends Component
|
||||
|
||||
public function mount(?string $server_id = null): void
|
||||
{
|
||||
$this->network = (string) new Cuid2;
|
||||
$this->network = new_public_id();
|
||||
$this->servers = Server::isUsable()->get();
|
||||
|
||||
if (filled($server_id)) {
|
||||
@@ -68,7 +67,7 @@ class Docker extends Component
|
||||
|
||||
public function generateName(): void
|
||||
{
|
||||
$name = data_get($this->selectedServer, 'name', new Cuid2);
|
||||
$name = data_get($this->selectedServer, 'name', new_public_id());
|
||||
$this->name = str("{$name}-{$this->network}")->kebab();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Destination;
|
||||
|
||||
use App\Models\StandaloneDocker;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
@@ -31,8 +32,12 @@ class Show extends Component
|
||||
if (! $destination) {
|
||||
return redirect()->route('destination.index');
|
||||
}
|
||||
$this->authorize('view', $destination);
|
||||
|
||||
$this->destination = $destination;
|
||||
$this->syncData();
|
||||
} catch (AuthorizationException) {
|
||||
abort(403);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -1053,6 +1053,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new postgresql)',
|
||||
'type' => 'postgresql',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/postgresql.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1062,6 +1063,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new mysql)',
|
||||
'type' => 'mysql',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/mysql.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1071,6 +1073,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new mariadb)',
|
||||
'type' => 'mariadb',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/mariadb.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1080,6 +1083,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new redis)',
|
||||
'type' => 'redis',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/redis.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1089,6 +1093,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new keydb)',
|
||||
'type' => 'keydb',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/keydb.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1098,6 +1103,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new dragonfly)',
|
||||
'type' => 'dragonfly',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/dragonfly.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1107,6 +1113,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new mongodb)',
|
||||
'type' => 'mongodb',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/mongodb.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1116,6 +1123,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new clickhouse)',
|
||||
'type' => 'clickhouse',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/clickhouse-icon.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Livewire;
|
||||
|
||||
// use Livewire\Component;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class MonacoEditor extends Component
|
||||
{
|
||||
@@ -40,7 +39,7 @@ class MonacoEditor extends Component
|
||||
public function render()
|
||||
{
|
||||
if (is_null($this->id)) {
|
||||
$this->id = new Cuid2;
|
||||
$this->id = new_public_id();
|
||||
}
|
||||
|
||||
if (is_null($this->name)) {
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Component;
|
||||
|
||||
class NavbarDeleteTeam extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public $team;
|
||||
|
||||
public function mount()
|
||||
@@ -17,27 +21,35 @@ class NavbarDeleteTeam extends Component
|
||||
|
||||
public function delete($password, $selectedActions = [])
|
||||
{
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return 'The provided password is incorrect.';
|
||||
try {
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
$currentTeam = currentTeam();
|
||||
$this->authorize('delete', $currentTeam);
|
||||
|
||||
$currentTeam->members->each(function ($user) use ($currentTeam) {
|
||||
if ($user->id === Auth::id()) {
|
||||
return;
|
||||
}
|
||||
$user->teams()->detach($currentTeam);
|
||||
$session = DB::table('sessions')->where('user_id', $user->id)->first();
|
||||
if ($session) {
|
||||
DB::table('sessions')->where('id', $session->id)->delete();
|
||||
}
|
||||
});
|
||||
|
||||
Cache::forget('user:'.Auth::id().':team:'.$currentTeam->id);
|
||||
$currentTeam->delete();
|
||||
|
||||
$newTeam = Auth::user()->teams()->first();
|
||||
refreshSession($newTeam);
|
||||
|
||||
return redirect()->route('team.index');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$currentTeam = currentTeam();
|
||||
$currentTeam->delete();
|
||||
|
||||
$currentTeam->members->each(function ($user) use ($currentTeam) {
|
||||
if ($user->id === Auth::id()) {
|
||||
return;
|
||||
}
|
||||
$user->teams()->detach($currentTeam);
|
||||
$session = DB::table('sessions')->where('user_id', $user->id)->first();
|
||||
if ($session) {
|
||||
DB::table('sessions')->where('id', $session->id)->delete();
|
||||
}
|
||||
});
|
||||
|
||||
refreshSession();
|
||||
|
||||
return redirectRoute($this, 'team.index');
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -110,7 +110,9 @@ class Discord extends Component
|
||||
refreshSession();
|
||||
} else {
|
||||
$this->discordEnabled = $this->settings->discord_enabled;
|
||||
$this->discordWebhookUrl = $this->settings->discord_webhook_url;
|
||||
$this->discordWebhookUrl = auth()->user()->can('update', $this->settings)
|
||||
? $this->settings->discord_webhook_url
|
||||
: null;
|
||||
|
||||
$this->deploymentSuccessDiscordNotifications = $this->settings->deployment_success_discord_notifications;
|
||||
$this->deploymentFailureDiscordNotifications = $this->settings->deployment_failure_discord_notifications;
|
||||
|
||||
@@ -113,8 +113,13 @@ class Pushover extends Component
|
||||
refreshSession();
|
||||
} else {
|
||||
$this->pushoverEnabled = $this->settings->pushover_enabled;
|
||||
$this->pushoverUserKey = $this->settings->pushover_user_key;
|
||||
$this->pushoverApiToken = $this->settings->pushover_api_token;
|
||||
if (auth()->user()->can('update', $this->settings)) {
|
||||
$this->pushoverUserKey = $this->settings->pushover_user_key;
|
||||
$this->pushoverApiToken = $this->settings->pushover_api_token;
|
||||
} else {
|
||||
$this->pushoverUserKey = null;
|
||||
$this->pushoverApiToken = null;
|
||||
}
|
||||
|
||||
$this->deploymentSuccessPushoverNotifications = $this->settings->deployment_success_pushover_notifications;
|
||||
$this->deploymentFailurePushoverNotifications = $this->settings->deployment_failure_pushover_notifications;
|
||||
|
||||
@@ -110,7 +110,9 @@ class Slack extends Component
|
||||
refreshSession();
|
||||
} else {
|
||||
$this->slackEnabled = $this->settings->slack_enabled;
|
||||
$this->slackWebhookUrl = $this->settings->slack_webhook_url;
|
||||
$this->slackWebhookUrl = auth()->user()->can('update', $this->settings)
|
||||
? $this->settings->slack_webhook_url
|
||||
: null;
|
||||
|
||||
$this->deploymentSuccessSlackNotifications = $this->settings->deployment_success_slack_notifications;
|
||||
$this->deploymentFailureSlackNotifications = $this->settings->deployment_failure_slack_notifications;
|
||||
|
||||
@@ -169,8 +169,13 @@ class Telegram extends Component
|
||||
$this->settings->save();
|
||||
} else {
|
||||
$this->telegramEnabled = $this->settings->telegram_enabled;
|
||||
$this->telegramToken = $this->settings->telegram_token;
|
||||
$this->telegramChatId = $this->settings->telegram_chat_id;
|
||||
if (auth()->user()->can('update', $this->settings)) {
|
||||
$this->telegramToken = $this->settings->telegram_token;
|
||||
$this->telegramChatId = $this->settings->telegram_chat_id;
|
||||
} else {
|
||||
$this->telegramToken = null;
|
||||
$this->telegramChatId = null;
|
||||
}
|
||||
|
||||
$this->deploymentSuccessTelegramNotifications = $this->settings->deployment_success_telegram_notifications;
|
||||
$this->deploymentFailureTelegramNotifications = $this->settings->deployment_failure_telegram_notifications;
|
||||
|
||||
@@ -105,7 +105,9 @@ class Webhook extends Component
|
||||
refreshSession();
|
||||
} else {
|
||||
$this->webhookEnabled = $this->settings->webhook_enabled;
|
||||
$this->webhookUrl = $this->settings->webhook_url;
|
||||
$this->webhookUrl = auth()->user()->can('update', $this->settings)
|
||||
? $this->settings->webhook_url
|
||||
: null;
|
||||
|
||||
$this->deploymentSuccessWebhookNotifications = $this->settings->deployment_success_webhook_notifications;
|
||||
$this->deploymentFailureWebhookNotifications = $this->settings->deployment_failure_webhook_notifications;
|
||||
|
||||
@@ -4,11 +4,13 @@ namespace App\Livewire\Project;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class AddEmpty extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public string $name;
|
||||
|
||||
public string $description = '';
|
||||
@@ -29,12 +31,13 @@ class AddEmpty extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('create', Project::class);
|
||||
$this->validate();
|
||||
$project = Project::create([
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'team_id' => currentTeam()->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
|
||||
$productionEnvironment = $project->environments()->where('name', 'production')->first();
|
||||
|
||||
@@ -59,7 +59,9 @@ class Show extends Component
|
||||
$this->application_deployment_queue = $application_deployment_queue;
|
||||
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
|
||||
$this->deployment_uuid = $deploymentUuid;
|
||||
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
|
||||
$this->is_debug_enabled = auth()->user()->isMember()
|
||||
? false
|
||||
: $this->application->settings->is_debug_enabled;
|
||||
$this->isKeepAliveOn();
|
||||
}
|
||||
|
||||
@@ -110,6 +112,8 @@ class Show extends Component
|
||||
|
||||
public function downloadAllLogs(): string
|
||||
{
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
$logs = decode_remote_command_output($this->application_deployment_queue, includeAll: true)
|
||||
->map(function ($line) {
|
||||
$prefix = '';
|
||||
|
||||
@@ -6,11 +6,14 @@ use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Livewire\Component;
|
||||
|
||||
class DeploymentNavbar extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public ApplicationDeploymentQueue $application_deployment_queue;
|
||||
|
||||
public Application $application;
|
||||
@@ -25,7 +28,9 @@ class DeploymentNavbar extends Component
|
||||
{
|
||||
$this->application = Application::ownedByCurrentTeam()->find($this->application_deployment_queue->application_id);
|
||||
$this->server = $this->application->destination->server;
|
||||
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
|
||||
$this->is_debug_enabled = auth()->user()->isMember()
|
||||
? false
|
||||
: $this->application->settings->is_debug_enabled;
|
||||
}
|
||||
|
||||
public function deploymentFinished()
|
||||
@@ -35,15 +40,21 @@ class DeploymentNavbar extends Component
|
||||
|
||||
public function show_debug()
|
||||
{
|
||||
$this->application->settings->is_debug_enabled = ! $this->application->settings->is_debug_enabled;
|
||||
$this->application->settings->save();
|
||||
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
|
||||
$this->dispatch('refreshQueue');
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->application->settings->is_debug_enabled = ! $this->application->settings->is_debug_enabled;
|
||||
$this->application->settings->save();
|
||||
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
|
||||
$this->dispatch('refreshQueue');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function force_start()
|
||||
{
|
||||
try {
|
||||
$this->authorize('deploy', $this->application);
|
||||
force_start_deployment($this->application_deployment_queue);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -58,10 +69,15 @@ class DeploymentNavbar extends Component
|
||||
return '';
|
||||
}
|
||||
|
||||
$isMember = auth()->user()->isMember();
|
||||
|
||||
$markdown = "# Deployment Logs\n\n";
|
||||
$markdown .= "```\n";
|
||||
|
||||
foreach ($logs as $log) {
|
||||
if ($isMember && ! empty($log['hidden'])) {
|
||||
continue;
|
||||
}
|
||||
if (isset($log['output'])) {
|
||||
$markdown .= $log['output']."\n";
|
||||
}
|
||||
@@ -74,6 +90,11 @@ class DeploymentNavbar extends Component
|
||||
|
||||
public function cancel()
|
||||
{
|
||||
try {
|
||||
$this->authorize('deploy', $this->application);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$deployment_uuid = $this->application_deployment_queue->deployment_uuid;
|
||||
$kill_command = "docker rm -f {$deployment_uuid}";
|
||||
$build_server_id = $this->application_deployment_queue->build_server_id ?? $this->application->destination->server_id;
|
||||
|
||||
@@ -13,7 +13,6 @@ use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Livewire\Features\SupportEvents\Event;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class General extends Component
|
||||
{
|
||||
@@ -549,7 +548,7 @@ class General extends Component
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
$uuid = new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$domain = generateUrl(server: $this->application->destination->server, random: $uuid);
|
||||
$sanitizedKey = str($serviceName)->replace('-', '_')->replace('.', '_')->toString();
|
||||
$this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain;
|
||||
|
||||
@@ -7,7 +7,6 @@ use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Models\Application;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Heading extends Component
|
||||
{
|
||||
@@ -65,107 +64,123 @@ class Heading extends Component
|
||||
|
||||
public function force_deploy_without_cache()
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
try {
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
$this->deploy(force_rebuild: true);
|
||||
$this->deploy(force_rebuild: true);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function deploy(bool $force_rebuild = false)
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
try {
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
|
||||
$this->dispatch('error', 'Failed to deploy', 'Please load a Compose file first.');
|
||||
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
|
||||
$this->dispatch('error', 'Failed to deploy', 'Please load a Compose file first.');
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
if ($this->application->destination->server->isSwarm() && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy.', 'To deploy to a Swarm cluster you must set a Docker image name first.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (data_get($this->application, 'settings.is_build_server_enabled') && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy.', 'To use a build server, you must first set a Docker image.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/build-server">documentation</a>');
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->setDeploymentUuid();
|
||||
$result = queue_application_deployment(
|
||||
application: $this->application,
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
force_rebuild: $force_rebuild,
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
$this->dispatch('error', 'Deployment queue full', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
if ($result['status'] === 'skipped') {
|
||||
$this->dispatch('error', 'Deployment skipped', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->redirectRoute('project.application.deployment.show', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'application_uuid' => $this->parameters['application_uuid'],
|
||||
'deployment_uuid' => $this->deploymentUuid,
|
||||
'environment_uuid' => $this->parameters['environment_uuid'],
|
||||
], navigate: false);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
if ($this->application->destination->server->isSwarm() && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy.', 'To deploy to a Swarm cluster you must set a Docker image name first.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (data_get($this->application, 'settings.is_build_server_enabled') && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy.', 'To use a build server, you must first set a Docker image.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/build-server">documentation</a>');
|
||||
|
||||
return;
|
||||
}
|
||||
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->setDeploymentUuid();
|
||||
$result = queue_application_deployment(
|
||||
application: $this->application,
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
force_rebuild: $force_rebuild,
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
$this->dispatch('error', 'Deployment queue full', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
if ($result['status'] === 'skipped') {
|
||||
$this->dispatch('error', 'Deployment skipped', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->redirectRoute('project.application.deployment.show', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'application_uuid' => $this->parameters['application_uuid'],
|
||||
'deployment_uuid' => $this->deploymentUuid,
|
||||
'environment_uuid' => $this->parameters['environment_uuid'],
|
||||
], navigate: false);
|
||||
}
|
||||
|
||||
protected function setDeploymentUuid()
|
||||
{
|
||||
$this->deploymentUuid = new Cuid2;
|
||||
$this->deploymentUuid = new_public_id();
|
||||
$this->parameters['deployment_uuid'] = $this->deploymentUuid;
|
||||
}
|
||||
|
||||
public function stop()
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
try {
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
$this->dispatch('info', 'Gracefully stopping application.<br/>It could take a while depending on the application.');
|
||||
StopApplication::dispatch($this->application, false, $this->docker_cleanup);
|
||||
$this->dispatch('info', 'Gracefully stopping application.<br/>It could take a while depending on the application.');
|
||||
StopApplication::dispatch($this->application, false, $this->docker_cleanup);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function restart()
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
try {
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
|
||||
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setDeploymentUuid();
|
||||
$result = queue_application_deployment(
|
||||
application: $this->application,
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
restart_only: true,
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
$this->dispatch('error', 'Deployment queue full', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
if ($result['status'] === 'skipped') {
|
||||
$this->dispatch('success', 'Deployment skipped', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->redirectRoute('project.application.deployment.show', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'application_uuid' => $this->parameters['application_uuid'],
|
||||
'deployment_uuid' => $this->deploymentUuid,
|
||||
'environment_uuid' => $this->parameters['environment_uuid'],
|
||||
], navigate: false);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$this->setDeploymentUuid();
|
||||
$result = queue_application_deployment(
|
||||
application: $this->application,
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
restart_only: true,
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
$this->dispatch('error', 'Deployment queue full', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
if ($result['status'] === 'skipped') {
|
||||
$this->dispatch('success', 'Deployment skipped', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->redirectRoute('project.application.deployment.show', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'application_uuid' => $this->parameters['application_uuid'],
|
||||
'deployment_uuid' => $this->deploymentUuid,
|
||||
'environment_uuid' => $this->parameters['environment_uuid'],
|
||||
], navigate: false);
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -9,7 +9,6 @@ use App\Models\ApplicationPreview;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Previews extends Component
|
||||
{
|
||||
@@ -234,31 +233,38 @@ class Previews extends Component
|
||||
|
||||
public function force_deploy_without_cache(int $pull_request_id, ?string $pull_request_html_url = null)
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
try {
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
$dockerRegistryImageTag = null;
|
||||
if ($this->application->build_pack === 'dockerimage') {
|
||||
$dockerRegistryImageTag = $this->application->previews()
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->value('docker_registry_image_tag');
|
||||
$dockerRegistryImageTag = null;
|
||||
if ($this->application->build_pack === 'dockerimage') {
|
||||
$dockerRegistryImageTag = $this->application->previews()
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->value('docker_registry_image_tag');
|
||||
}
|
||||
|
||||
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true, docker_registry_image_tag: $dockerRegistryImageTag);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true, docker_registry_image_tag: $dockerRegistryImageTag);
|
||||
}
|
||||
|
||||
public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null, ?string $docker_registry_image_tag = null)
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
try {
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
$this->add($pull_request_id, $pull_request_html_url, $docker_registry_image_tag);
|
||||
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: false, docker_registry_image_tag: $docker_registry_image_tag);
|
||||
$this->add($pull_request_id, $pull_request_html_url, $docker_registry_image_tag);
|
||||
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: false, docker_registry_image_tag: $docker_registry_image_tag);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false, ?string $docker_registry_image_tag = null)
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
try {
|
||||
$this->authorize('deploy', $this->application);
|
||||
$this->setDeploymentUuid();
|
||||
$found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found && (! is_null($pull_request_html_url) || ($this->application->build_pack === 'dockerimage' && str($docker_registry_image_tag)->isNotEmpty()))) {
|
||||
@@ -305,7 +311,7 @@ class Previews extends Component
|
||||
|
||||
protected function setDeploymentUuid()
|
||||
{
|
||||
$this->deployment_uuid = new Cuid2;
|
||||
$this->deployment_uuid = new_public_id();
|
||||
$this->parameters['deployment_uuid'] = $this->deployment_uuid;
|
||||
}
|
||||
|
||||
@@ -350,9 +356,8 @@ class Previews extends Component
|
||||
|
||||
public function stop(int $pull_request_id)
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
try {
|
||||
$this->authorize('deploy', $this->application);
|
||||
$server = $this->application->destination->server;
|
||||
|
||||
if ($this->application->destination->server->isSwarm()) {
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Models\ApplicationPreview;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class PreviewsCompose extends Component
|
||||
{
|
||||
@@ -64,7 +63,7 @@ class PreviewsCompose extends Component
|
||||
if (empty($domain_string)) {
|
||||
$server = $this->preview->application->destination->server;
|
||||
$template = $this->preview->application->preview_url_template;
|
||||
$random = new Cuid2;
|
||||
$random = new_public_id();
|
||||
|
||||
// Generate a unique domain like main app services do
|
||||
$generated_fqdn = generateUrl(server: $server, random: $random);
|
||||
@@ -79,7 +78,7 @@ class PreviewsCompose extends Component
|
||||
$domain_list = explode(',', $domain_string);
|
||||
$preview_fqdns = [];
|
||||
$template = $this->preview->application->preview_url_template;
|
||||
$random = new Cuid2;
|
||||
$random = new_public_id();
|
||||
|
||||
foreach ($domain_list as $single_domain) {
|
||||
$single_domain = trim($single_domain);
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Models\Application;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Rollback extends Component
|
||||
{
|
||||
@@ -52,7 +51,7 @@ class Rollback extends Component
|
||||
|
||||
$commit = validateGitRef($commit, 'rollback commit');
|
||||
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $this->application,
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Models\Application;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class Swarm extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Application $application;
|
||||
|
||||
#[Validate('required')]
|
||||
@@ -51,6 +54,7 @@ class Swarm extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Swarm settings updated.');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -61,6 +65,7 @@ class Swarm extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Swarm settings updated.');
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -11,11 +11,13 @@ use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class CloneMe extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public string $project_uuid;
|
||||
|
||||
public string $environment_uuid;
|
||||
@@ -61,7 +63,7 @@ class CloneMe extends Component
|
||||
->servers()
|
||||
->get()
|
||||
->reject(fn ($server) => $server->isBuildServer());
|
||||
$this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug();
|
||||
$this->newName = str($this->project->name.'-clone-'.new_public_id())->slug();
|
||||
}
|
||||
|
||||
public function toggleVolumeCloning(bool $value)
|
||||
@@ -91,6 +93,7 @@ class CloneMe extends Component
|
||||
public function clone(string $type)
|
||||
{
|
||||
try {
|
||||
$this->authorize('create', Project::class);
|
||||
$this->validate([
|
||||
'selectedDestination' => 'required',
|
||||
'newName' => ValidationPatterns::nameRules(),
|
||||
@@ -108,7 +111,7 @@ class CloneMe extends Component
|
||||
if ($this->environment->name !== 'production') {
|
||||
$project->environments()->create([
|
||||
'name' => $this->environment->name,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
}
|
||||
$environment = $project->environments->where('name', $this->environment->name)->first();
|
||||
@@ -120,7 +123,7 @@ class CloneMe extends Component
|
||||
$project = $this->project;
|
||||
$environment = $this->project->environments()->create([
|
||||
'name' => $this->newName,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
}
|
||||
$applications = $this->environment->applications;
|
||||
@@ -134,7 +137,7 @@ class CloneMe extends Component
|
||||
}
|
||||
|
||||
foreach ($databases as $database) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$newDatabase = $database->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
@@ -225,7 +228,7 @@ class CloneMe extends Component
|
||||
|
||||
$scheduledBackups = $database->scheduledBackups()->get();
|
||||
foreach ($scheduledBackups as $backup) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$newBackup = $backup->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
@@ -254,7 +257,7 @@ class CloneMe extends Component
|
||||
}
|
||||
|
||||
foreach ($services as $service) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$newService = $service->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
@@ -278,7 +281,7 @@ class CloneMe extends Component
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
'service_id' => $newService->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
@@ -409,7 +412,7 @@ class CloneMe extends Component
|
||||
|
||||
$scheduledBackups = $database->scheduledBackups()->get();
|
||||
foreach ($scheduledBackups as $backup) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$newBackup = $backup->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Exception;
|
||||
@@ -190,6 +191,18 @@ class BackupEdit extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function backupNow()
|
||||
{
|
||||
try {
|
||||
$this->authorize('manageBackups', $this->backup->database);
|
||||
|
||||
DatabaseBackupJob::dispatch($this->backup);
|
||||
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class BackupExecutions extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public ?ScheduledDatabaseBackup $backup = null;
|
||||
|
||||
public $database;
|
||||
@@ -44,29 +48,45 @@ class BackupExecutions extends Component
|
||||
|
||||
public function cleanupFailed()
|
||||
{
|
||||
if ($this->backup) {
|
||||
$this->backup->executions()->where('status', 'failed')->delete();
|
||||
$this->refreshBackupExecutions();
|
||||
$this->dispatch('success', 'Failed backups cleaned up.');
|
||||
try {
|
||||
$this->authorize('manageBackups', $this->database);
|
||||
if ($this->backup) {
|
||||
$this->backup->executions()->where('status', 'failed')->delete();
|
||||
$this->refreshBackupExecutions();
|
||||
$this->dispatch('success', 'Failed backups cleaned up.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function cleanupDeleted()
|
||||
{
|
||||
if ($this->backup) {
|
||||
$deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count();
|
||||
if ($deletedCount > 0) {
|
||||
$this->backup->executions()->where('local_storage_deleted', true)->delete();
|
||||
$this->refreshBackupExecutions();
|
||||
$this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage.");
|
||||
} else {
|
||||
$this->dispatch('info', 'No backup entries found that are deleted from local storage.');
|
||||
try {
|
||||
$this->authorize('manageBackups', $this->database);
|
||||
if ($this->backup) {
|
||||
$deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count();
|
||||
if ($deletedCount > 0) {
|
||||
$this->backup->executions()->where('local_storage_deleted', true)->delete();
|
||||
$this->refreshBackupExecutions();
|
||||
$this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage.");
|
||||
} else {
|
||||
$this->dispatch('info', 'No backup entries found that are deleted from local storage.');
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteBackup($executionId, $password, $selectedActions = [])
|
||||
{
|
||||
try {
|
||||
$this->authorize('manageBackups', $this->database);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
@@ -78,7 +98,7 @@ class BackupExecutions extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$server = $execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class
|
||||
$server = $execution->scheduledDatabaseBackup->database->getMorphClass() === ServiceDatabase::class
|
||||
? $execution->scheduledDatabaseBackup->database->service->destination->server
|
||||
: $execution->scheduledDatabaseBackup->database->destination->server;
|
||||
|
||||
@@ -185,7 +205,7 @@ class BackupExecutions extends Component
|
||||
if ($this->database) {
|
||||
$server = null;
|
||||
|
||||
if ($this->database instanceof \App\Models\ServiceDatabase) {
|
||||
if ($this->database instanceof ServiceDatabase) {
|
||||
$server = $this->database->service->destination->server;
|
||||
} elseif ($this->database->destination && $this->database->destination->server) {
|
||||
$server = $this->database->destination->server;
|
||||
|
||||
@@ -14,9 +14,13 @@ class BackupNow extends Component
|
||||
|
||||
public function backupNow()
|
||||
{
|
||||
$this->authorize('manageBackups', $this->backup->database);
|
||||
try {
|
||||
$this->authorize('manageBackups', $this->backup->database);
|
||||
|
||||
DatabaseBackupJob::dispatch($this->backup);
|
||||
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
|
||||
DatabaseBackupJob::dispatch($this->backup);
|
||||
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ class General extends Component
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public bool $isPasswordHiddenForMember = false;
|
||||
|
||||
public function getListeners(): array
|
||||
{
|
||||
$user = Auth::user();
|
||||
@@ -72,6 +74,11 @@ class General extends Component
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
|
||||
if ($this->isPasswordHiddenForMember) {
|
||||
$this->clickhouseAdminPassword = '';
|
||||
}
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
|
||||
@@ -40,6 +40,8 @@ class General extends Component
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public bool $isPasswordHiddenForMember = false;
|
||||
|
||||
public function getListeners(): array
|
||||
{
|
||||
$user = Auth::user();
|
||||
@@ -70,6 +72,11 @@ class General extends Component
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
|
||||
if ($this->isPasswordHiddenForMember) {
|
||||
$this->dragonflyPassword = '';
|
||||
}
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
|
||||
@@ -90,18 +90,28 @@ class Heading extends Component
|
||||
|
||||
public function restart()
|
||||
{
|
||||
$this->authorize('manage', $this->database);
|
||||
try {
|
||||
$this->authorize('manage', $this->database);
|
||||
|
||||
$activity = RestartDatabase::run($this->database);
|
||||
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
|
||||
$activity = RestartDatabase::run($this->database);
|
||||
$this->js("window.dispatchEvent(new CustomEvent('startdatabase'))");
|
||||
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function start()
|
||||
{
|
||||
$this->authorize('manage', $this->database);
|
||||
try {
|
||||
$this->authorize('manage', $this->database);
|
||||
|
||||
$activity = StartDatabase::run($this->database);
|
||||
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
|
||||
$activity = StartDatabase::run($this->database);
|
||||
$this->js("window.dispatchEvent(new CustomEvent('startdatabase'))");
|
||||
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -2,13 +2,20 @@
|
||||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Exception;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class InitScript extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
#[Locked]
|
||||
public StandalonePostgresql $database;
|
||||
|
||||
#[Locked]
|
||||
public array $script;
|
||||
|
||||
@@ -35,6 +42,7 @@ class InitScript extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->validate();
|
||||
$this->script['index'] = $this->index;
|
||||
$this->script['content'] = $this->content;
|
||||
@@ -48,6 +56,7 @@ class InitScript extends Component
|
||||
public function delete()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->dispatch('delete_init_script', $this->script);
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -42,6 +42,8 @@ class General extends Component
|
||||
|
||||
public bool $isLogDrainEnabled = false;
|
||||
|
||||
public bool $isPasswordHiddenForMember = false;
|
||||
|
||||
public function getListeners(): array
|
||||
{
|
||||
$user = Auth::user();
|
||||
@@ -72,6 +74,11 @@ class General extends Component
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
|
||||
if ($this->isPasswordHiddenForMember) {
|
||||
$this->keydbPassword = '';
|
||||
}
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
|
||||
@@ -47,6 +47,8 @@ class General extends Component
|
||||
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
public bool $isPasswordHiddenForMember = false;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
@@ -126,6 +128,12 @@ class General extends Component
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
|
||||
if ($this->isPasswordHiddenForMember) {
|
||||
$this->mariadbRootPassword = '';
|
||||
$this->mariadbPassword = '';
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
|
||||
@@ -45,6 +45,8 @@ class General extends Component
|
||||
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
public bool $isPasswordHiddenForMember = false;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
@@ -119,6 +121,11 @@ class General extends Component
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
|
||||
if ($this->isPasswordHiddenForMember) {
|
||||
$this->mongoInitdbRootPassword = '';
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
|
||||
@@ -47,6 +47,8 @@ class General extends Component
|
||||
|
||||
public ?string $customDockerRunOptions = null;
|
||||
|
||||
public bool $isPasswordHiddenForMember = false;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
@@ -126,6 +128,12 @@ class General extends Component
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
|
||||
if ($this->isPasswordHiddenForMember) {
|
||||
$this->mysqlRootPassword = '';
|
||||
$this->mysqlPassword = '';
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
|
||||
@@ -55,6 +55,8 @@ class General extends Component
|
||||
|
||||
public string $new_content;
|
||||
|
||||
public bool $isPasswordHiddenForMember = false;
|
||||
|
||||
protected $listeners = [
|
||||
'save_init_script',
|
||||
'delete_init_script',
|
||||
@@ -140,6 +142,11 @@ class General extends Component
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
|
||||
if ($this->isPasswordHiddenForMember) {
|
||||
$this->postgresPassword = '';
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
|
||||
@@ -45,6 +45,8 @@ class General extends Component
|
||||
|
||||
public string $redisVersion;
|
||||
|
||||
public bool $isPasswordHiddenForMember = false;
|
||||
|
||||
protected $listeners = [
|
||||
'envsUpdated' => 'refresh',
|
||||
];
|
||||
@@ -118,6 +120,11 @@ class General extends Component
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
|
||||
if ($this->isPasswordHiddenForMember) {
|
||||
$this->redisPassword = '';
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -34,7 +35,7 @@ class ScheduledBackups extends Component
|
||||
$this->setSelectedBackup($this->selectedBackupId, true);
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
|
||||
if ($this->database->getMorphClass() === ServiceDatabase::class) {
|
||||
$this->type = 'service-database';
|
||||
} else {
|
||||
$this->type = 'database';
|
||||
@@ -56,22 +57,30 @@ class ScheduledBackups extends Component
|
||||
|
||||
public function setCustomType()
|
||||
{
|
||||
$this->authorize('update', $this->database);
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
|
||||
$this->database->custom_type = $this->custom_type;
|
||||
$this->database->save();
|
||||
$this->dispatch('success', 'Database type set.');
|
||||
$this->refreshScheduledBackups();
|
||||
$this->database->custom_type = $this->custom_type;
|
||||
$this->database->save();
|
||||
$this->dispatch('success', 'Database type set.');
|
||||
$this->refreshScheduledBackups();
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($scheduled_backup_id): void
|
||||
{
|
||||
$backup = $this->database->scheduledBackups->find($scheduled_backup_id);
|
||||
$this->authorize('manageBackups', $this->database);
|
||||
try {
|
||||
$this->authorize('manageBackups', $this->database);
|
||||
|
||||
$backup->delete();
|
||||
$this->dispatch('success', 'Scheduled backup deleted.');
|
||||
$this->refreshScheduledBackups();
|
||||
$backup = $this->database->scheduledBackups->find($scheduled_backup_id);
|
||||
$backup->delete();
|
||||
$this->dispatch('success', 'Scheduled backup deleted.');
|
||||
$this->refreshScheduledBackups();
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function refreshScheduledBackups(?int $id = null): void
|
||||
|
||||
@@ -28,18 +28,22 @@ class DeleteEnvironment extends Component
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$this->validate([
|
||||
'environment_id' => 'required|int',
|
||||
]);
|
||||
$environment = Environment::ownedByCurrentTeam()->findOrFail($this->environment_id);
|
||||
$this->authorize('delete', $environment);
|
||||
try {
|
||||
$this->validate([
|
||||
'environment_id' => 'required|int',
|
||||
]);
|
||||
$environment = Environment::ownedByCurrentTeam()->findOrFail($this->environment_id);
|
||||
$this->authorize('delete', $environment);
|
||||
|
||||
if ($environment->isEmpty()) {
|
||||
$environment->delete();
|
||||
if ($environment->isEmpty()) {
|
||||
$environment->delete();
|
||||
|
||||
return redirectRoute($this, 'project.show', ['project_uuid' => $this->parameters['project_uuid']]);
|
||||
return redirectRoute($this, 'project.show', ['project_uuid' => $this->parameters['project_uuid']]);
|
||||
}
|
||||
|
||||
return $this->dispatch('error', "<strong>Environment {$environment->name}</strong> has defined resources, please delete them first.");
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
return $this->dispatch('error', "<strong>Environment {$environment->name}</strong> has defined resources, please delete them first.");
|
||||
}
|
||||
}
|
||||
@@ -26,18 +26,22 @@ class DeleteProject extends Component
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$this->validate([
|
||||
'project_id' => 'required|int',
|
||||
]);
|
||||
$project = Project::ownedByCurrentTeam()->findOrFail($this->project_id);
|
||||
$this->authorize('delete', $project);
|
||||
try {
|
||||
$this->validate([
|
||||
'project_id' => 'required|int',
|
||||
]);
|
||||
$project = Project::ownedByCurrentTeam()->findOrFail($this->project_id);
|
||||
$this->authorize('delete', $project);
|
||||
|
||||
if ($project->isEmpty()) {
|
||||
$project->delete();
|
||||
if ($project->isEmpty()) {
|
||||
$project->delete();
|
||||
|
||||
return redirectRoute($this, 'project.index');
|
||||
return redirectRoute($this, 'project.index');
|
||||
}
|
||||
|
||||
return $this->dispatch('error', "<strong>Project {$project->name}</strong> has resources defined, please delete them first.");
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
return $this->dispatch('error', "<strong>Project {$project->name}</strong> has resources defined, please delete them first.");
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,13 @@ namespace App\Livewire\Project;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class Edit extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Project $project;
|
||||
|
||||
public string $name;
|
||||
@@ -54,6 +57,7 @@ class Edit extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->project);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Project updated.');
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -5,11 +5,14 @@ namespace App\Livewire\Project;
|
||||
use App\Models\Application;
|
||||
use App\Models\Project;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
class EnvironmentEdit extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Project $project;
|
||||
|
||||
public Application $application;
|
||||
@@ -62,6 +65,7 @@ class EnvironmentEdit extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->environment);
|
||||
$this->syncData(true);
|
||||
redirectRoute($this, 'project.environment.edit', [
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
|
||||
@@ -7,7 +7,6 @@ use App\Models\Project;
|
||||
use App\Services\DockerImageParser;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class DockerImage extends Component
|
||||
{
|
||||
@@ -130,7 +129,7 @@ class DockerImage extends Component
|
||||
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
|
||||
|
||||
$application = Application::create([
|
||||
'name' => 'docker-image-'.new Cuid2,
|
||||
'name' => 'docker-image-'.new_public_id(),
|
||||
'repository_project_id' => 0,
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
'git_branch' => 'main',
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Livewire\Project\New;
|
||||
|
||||
use App\Models\Project;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class EmptyProject extends Component
|
||||
{
|
||||
@@ -13,7 +12,7 @@ class EmptyProject extends Component
|
||||
$project = Project::create([
|
||||
'name' => generate_random_name(),
|
||||
'team_id' => currentTeam()->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
|
||||
return redirectRoute($this, 'project.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $project->environments->first()->uuid]);
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Component;
|
||||
|
||||
class Select extends Component
|
||||
@@ -107,7 +106,7 @@ class Select extends Component
|
||||
public function loadServices()
|
||||
{
|
||||
$services = get_service_templates();
|
||||
$templateLastUpdatedMap = $this->serviceTemplateLastUpdatedMap($services->keys());
|
||||
$templateLastUpdatedMap = $this->serviceTemplateLastUpdatedMap($services);
|
||||
|
||||
$services = collect($services)->map(function ($service, $key) use ($templateLastUpdatedMap) {
|
||||
$default_logo = 'images/default.webp';
|
||||
@@ -279,19 +278,31 @@ class Select extends Component
|
||||
return $this->formatLastModified($this->serviceTemplatesPath());
|
||||
}
|
||||
|
||||
private function serviceTemplateLastUpdatedMap(Collection $serviceNames): array
|
||||
private function serviceTemplateLastUpdatedMap(Collection $services): array
|
||||
{
|
||||
$bundleMtime = file_exists($this->serviceTemplatesPath()) ? filemtime($this->serviceTemplatesPath()) : 0;
|
||||
return $services
|
||||
->mapWithKeys(fn ($service, $serviceName) => [
|
||||
(string) $serviceName => $this->serviceTemplateLastUpdatedFromPayload($service)
|
||||
?? $this->serviceTemplateLastUpdated((string) $serviceName),
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
return Cache::remember(
|
||||
"service-template-last-updated-map:{$bundleMtime}",
|
||||
now()->addDay(),
|
||||
fn () => $serviceNames
|
||||
->mapWithKeys(fn ($serviceName) => [
|
||||
(string) $serviceName => $this->serviceTemplateLastUpdated((string) $serviceName),
|
||||
])
|
||||
->all()
|
||||
);
|
||||
private function serviceTemplateLastUpdatedFromPayload(mixed $service): ?string
|
||||
{
|
||||
$timestamp = data_get($service, 'template_last_updated_at');
|
||||
|
||||
if (! is_string($timestamp) || $timestamp === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return CarbonImmutable::parse($timestamp)
|
||||
->timezone(config('app.timezone'))
|
||||
->format('M j, Y H:i');
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function serviceTemplateLastUpdated(string $serviceName): ?string
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Models\Application;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\Project;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class SimpleDockerfile extends Component
|
||||
{
|
||||
@@ -48,7 +47,7 @@ CMD ["nginx", "-g", "daemon off;"]
|
||||
$port = 80;
|
||||
}
|
||||
$application = Application::create([
|
||||
'name' => 'dockerfile-'.new Cuid2,
|
||||
'name' => 'dockerfile-'.new_public_id(),
|
||||
'repository_project_id' => 0,
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
'git_branch' => 'main',
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\Service;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class EditCompose extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Service $service;
|
||||
|
||||
public $serviceId;
|
||||
@@ -72,19 +75,29 @@ class EditCompose extends Component
|
||||
|
||||
public function saveEditedCompose()
|
||||
{
|
||||
$this->dispatch('info', 'Saving new docker compose...');
|
||||
$this->dispatch('saveCompose', $this->dockerComposeRaw);
|
||||
$this->dispatch('refreshStorages');
|
||||
try {
|
||||
$this->authorize('update', $this->service);
|
||||
$this->dispatch('info', 'Saving new docker compose...');
|
||||
$this->dispatch('saveCompose', $this->dockerComposeRaw);
|
||||
$this->dispatch('refreshStorages');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
$this->validate([
|
||||
'isContainerLabelEscapeEnabled' => 'required',
|
||||
]);
|
||||
$this->syncData(true);
|
||||
$this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]);
|
||||
$this->dispatch('success', 'Service updated successfully');
|
||||
try {
|
||||
$this->authorize('update', $this->service);
|
||||
$this->validate([
|
||||
'isContainerLabelEscapeEnabled' => 'required',
|
||||
]);
|
||||
$this->syncData(true);
|
||||
$this->service->save(['is_container_label_escape_enabled' => $this->isContainerLabelEscapeEnabled]);
|
||||
$this->dispatch('success', 'Service updated successfully');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -110,8 +110,7 @@ class FileStorage extends Component
|
||||
public function loadStorageOnServer()
|
||||
{
|
||||
try {
|
||||
// Loading content is a read operation, so we use 'view' permission
|
||||
$this->authorize('view', $this->resource);
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
$this->syncData();
|
||||
|
||||
@@ -110,17 +110,20 @@ class Heading extends Component
|
||||
|
||||
public function start()
|
||||
{
|
||||
$this->authorizeService('deploy');
|
||||
|
||||
$activity = StartService::run($this->service, pullLatestImages: true);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
try {
|
||||
$this->authorizeService('deploy');
|
||||
$activity = StartService::run($this->service, pullLatestImages: true);
|
||||
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function forceDeploy()
|
||||
{
|
||||
$this->authorizeService('deploy');
|
||||
|
||||
try {
|
||||
$this->authorizeService('deploy');
|
||||
$activities = Activity::where('properties->type_uuid', $this->service->uuid)
|
||||
->where(function ($q) {
|
||||
$q->where('properties->status', ProcessStatus::IN_PROGRESS->value)
|
||||
@@ -131,49 +134,57 @@ class Heading extends Component
|
||||
$activity->save();
|
||||
}
|
||||
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
|
||||
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function stop()
|
||||
{
|
||||
$this->authorizeService('stop');
|
||||
|
||||
try {
|
||||
$this->authorizeService('stop');
|
||||
StopService::dispatch($this->service, false, $this->docker_cleanup);
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function restart()
|
||||
{
|
||||
$this->authorizeService('deploy');
|
||||
try {
|
||||
$this->authorizeService('deploy');
|
||||
$this->checkDeployments();
|
||||
if ($this->isDeploymentProgress) {
|
||||
$this->dispatch('error', 'There is a deployment in progress.');
|
||||
|
||||
$this->checkDeployments();
|
||||
if ($this->isDeploymentProgress) {
|
||||
$this->dispatch('error', 'There is a deployment in progress.');
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
$activity = StartService::run($this->service, stopBeforeStart: true);
|
||||
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$activity = StartService::run($this->service, stopBeforeStart: true);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function pullAndRestartEvent()
|
||||
{
|
||||
$this->authorizeService('deploy');
|
||||
try {
|
||||
$this->authorizeService('deploy');
|
||||
$this->checkDeployments();
|
||||
if ($this->isDeploymentProgress) {
|
||||
$this->dispatch('error', 'There is a deployment in progress.');
|
||||
|
||||
$this->checkDeployments();
|
||||
if ($this->isDeploymentProgress) {
|
||||
$this->dispatch('error', 'There is a deployment in progress.');
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
|
||||
$this->js("window.dispatchEvent(new CustomEvent('startservice'))");
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
private function authorizeService(string $ability): void
|
||||
|
||||
@@ -4,16 +4,21 @@ namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\Service;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Component;
|
||||
|
||||
class StackForm extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Service $service;
|
||||
|
||||
public Collection $fields;
|
||||
|
||||
public bool $isPasswordHiddenForMember = false;
|
||||
|
||||
protected $listeners = ['saveCompose'];
|
||||
|
||||
// Explicit properties
|
||||
@@ -118,6 +123,17 @@ class StackForm extends Component
|
||||
})->flatMap(function ($group) {
|
||||
return $group;
|
||||
});
|
||||
|
||||
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
|
||||
if ($this->isPasswordHiddenForMember) {
|
||||
$this->fields = $this->fields->map(function ($field) {
|
||||
if (data_get($field, 'isPassword')) {
|
||||
$field['value'] = null;
|
||||
}
|
||||
|
||||
return $field;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function saveCompose($raw)
|
||||
@@ -128,14 +144,20 @@ class StackForm extends Component
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
$this->syncData(true);
|
||||
$this->service->save();
|
||||
$this->dispatch('success', 'Service settings saved.');
|
||||
try {
|
||||
$this->authorize('update', $this->service);
|
||||
$this->syncData(true);
|
||||
$this->service->save();
|
||||
$this->dispatch('success', 'Service settings saved.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit($notify = true)
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->service);
|
||||
$this->validate();
|
||||
$this->syncData(true);
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Danger extends Component
|
||||
{
|
||||
@@ -39,7 +38,7 @@ class Danger extends Component
|
||||
public function mount()
|
||||
{
|
||||
$parameters = get_route_parameters();
|
||||
$this->modalId = new Cuid2;
|
||||
$this->modalId = new_public_id();
|
||||
$this->projectUuid = data_get($parameters, 'project_uuid');
|
||||
$this->environmentUuid = data_get($parameters, 'environment_uuid');
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Events\ApplicationStatusChanged;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Destination extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public $resource;
|
||||
|
||||
public Collection $networks;
|
||||
@@ -59,6 +61,7 @@ class Destination extends Component
|
||||
public function stop($serverId)
|
||||
{
|
||||
try {
|
||||
$this->authorize('deploy', $this->resource);
|
||||
$server = Server::ownedByCurrentTeam()->findOrFail($serverId);
|
||||
StopApplicationOneServer::run($this->resource, $server);
|
||||
$this->refreshServers();
|
||||
@@ -70,12 +73,13 @@ class Destination extends Component
|
||||
public function redeploy(int $network_id, int $server_id)
|
||||
{
|
||||
try {
|
||||
$this->authorize('deploy', $this->resource);
|
||||
if ($this->resource->additional_servers->count() > 0 && str($this->resource->docker_registry_image_name)->isEmpty()) {
|
||||
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
|
||||
|
||||
return;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
|
||||
$destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail();
|
||||
$result = queue_application_deployment(
|
||||
@@ -128,7 +132,7 @@ class Destination extends Component
|
||||
});
|
||||
$this->resource->refresh();
|
||||
$this->refreshServers();
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
@@ -149,7 +153,7 @@ class Destination extends Component
|
||||
|
||||
$this->resource->additional_networks()->attach($network->id, ['server_id' => $server->id]);
|
||||
$this->dispatch('refresh');
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
@@ -157,6 +161,7 @@ class Destination extends Component
|
||||
public function removeServer(int $network_id, int $server_id, $password, $selectedActions = [])
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return 'The provided password is incorrect.';
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ class All extends Component
|
||||
$query->orderBy('order');
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
return $this->nullLockedValues($query->get());
|
||||
}
|
||||
|
||||
private function searchTerm(): string
|
||||
@@ -127,6 +127,20 @@ class All extends Component
|
||||
$this->hardcodedEnvironmentVariablesPreview->isNotEmpty();
|
||||
}
|
||||
|
||||
private function nullLockedValues($envs)
|
||||
{
|
||||
$isMember = auth()->user()?->isMember();
|
||||
|
||||
$envs->each(function ($env) use ($isMember) {
|
||||
if ($env->is_shown_once || $isMember) {
|
||||
$env->value = null;
|
||||
$env->real_value = null;
|
||||
}
|
||||
});
|
||||
|
||||
return $envs;
|
||||
}
|
||||
|
||||
public function getIsSearchActiveProperty(): bool
|
||||
{
|
||||
return $this->searchTerm() !== '';
|
||||
@@ -204,7 +218,12 @@ class All extends Component
|
||||
|
||||
private function formatEnvironmentVariables($variables)
|
||||
{
|
||||
return $variables->map(function ($item) {
|
||||
$isMember = auth()->user()?->isMember();
|
||||
|
||||
return $variables->map(function ($item) use ($isMember) {
|
||||
if ($isMember) {
|
||||
return "$item->key=(Hidden, only admins can view)";
|
||||
}
|
||||
if ($item->is_shown_once) {
|
||||
return "$item->key=(Locked Secret, delete and add again to change)";
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ class Show extends Component
|
||||
|
||||
public bool $is_redis_credential = false;
|
||||
|
||||
public bool $isValueHidden = false;
|
||||
|
||||
public array $problematicVariables = [];
|
||||
|
||||
protected $listeners = [
|
||||
@@ -160,6 +162,13 @@ class Show extends Component
|
||||
$this->is_really_required = $this->env->is_really_required ?? false;
|
||||
$this->is_shared = $this->env->is_shared ?? false;
|
||||
$this->real_value = $this->env->real_value;
|
||||
|
||||
if ($this->env->is_shown_once || auth()->user()?->isMember()) {
|
||||
$this->value = null;
|
||||
$this->real_value = null;
|
||||
}
|
||||
|
||||
$this->isValueHidden = auth()->user()?->isMember() ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,15 @@ use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class ExecuteContainerCommand extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public $selected_container = 'default';
|
||||
|
||||
public Collection $containers;
|
||||
@@ -40,6 +43,7 @@ class ExecuteContainerCommand extends Component
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->type = 'application';
|
||||
$this->resource = Application::ownedByCurrentTeam()->where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->authorize('view', $this->resource);
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->destination->server);
|
||||
}
|
||||
@@ -56,6 +60,7 @@ class ExecuteContainerCommand extends Component
|
||||
abort(404);
|
||||
}
|
||||
$this->resource = $resource;
|
||||
$this->authorize('view', $this->resource);
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->destination->server);
|
||||
}
|
||||
@@ -63,6 +68,7 @@ class ExecuteContainerCommand extends Component
|
||||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->authorize('view', $this->resource);
|
||||
if ($this->resource->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->server);
|
||||
}
|
||||
@@ -70,6 +76,7 @@ class ExecuteContainerCommand extends Component
|
||||
} elseif (data_get($this->parameters, 'server_uuid')) {
|
||||
$this->type = 'server';
|
||||
$this->resource = Server::ownedByCurrentTeam()->where('uuid', $this->parameters['server_uuid'])->firstOrFail();
|
||||
$this->authorize('view', $this->resource);
|
||||
$this->servers = $this->servers->push($this->resource);
|
||||
}
|
||||
$this->servers = $this->servers->sortByDesc(fn ($server) => $server->isTerminalEnabled());
|
||||
@@ -152,7 +159,9 @@ class ExecuteContainerCommand extends Component
|
||||
public function connectToServer()
|
||||
{
|
||||
try {
|
||||
$this->authorize('canAccessTerminal');
|
||||
$server = $this->servers->first();
|
||||
$this->authorize('view', $server);
|
||||
if ($server->isForceDisabled()) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
@@ -181,6 +190,7 @@ class ExecuteContainerCommand extends Component
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->authorize('canAccessTerminal');
|
||||
// Validate container name format
|
||||
if (! ValidationPatterns::isValidContainerName($this->selected_container)) {
|
||||
throw new \InvalidArgumentException('Invalid container name format');
|
||||
@@ -198,6 +208,8 @@ class ExecuteContainerCommand extends Component
|
||||
throw new \RuntimeException('Invalid server configuration.');
|
||||
}
|
||||
|
||||
$this->authorize('view', $server);
|
||||
|
||||
if ($server->isForceDisabled()) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
|
||||
@@ -78,8 +78,12 @@ class HealthChecks extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('view', $this->resource);
|
||||
$this->syncData();
|
||||
try {
|
||||
$this->authorize('view', $this->resource);
|
||||
$this->syncData();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
|
||||
@@ -22,7 +22,6 @@ use App\Models\StandaloneRedis;
|
||||
use App\Models\SwarmDocker;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class ResourceOperations extends Component
|
||||
{
|
||||
@@ -56,224 +55,86 @@ class ResourceOperations extends Component
|
||||
|
||||
public function cloneTo($destination_id)
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$new_destination = StandaloneDocker::ownedByCurrentTeam()->find($destination_id);
|
||||
if (! $new_destination) {
|
||||
$new_destination = SwarmDocker::ownedByCurrentTeam()->find($destination_id);
|
||||
}
|
||||
if (! $new_destination) {
|
||||
return $this->addError('destination_id', 'Destination not found.');
|
||||
}
|
||||
$uuid = (string) new Cuid2;
|
||||
$server = $new_destination->server;
|
||||
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
|
||||
|
||||
$route = route('project.application.configuration', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
'environment_uuid' => $this->environmentUuid,
|
||||
'application_uuid' => $new_resource->uuid,
|
||||
]).'#resource-operations';
|
||||
|
||||
return redirect()->to($route);
|
||||
} elseif (
|
||||
$this->resource->getMorphClass() === StandalonePostgresql::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMongodb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMysql::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMariadb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneRedis::class ||
|
||||
$this->resource->getMorphClass() === StandaloneKeydb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneDragonfly::class ||
|
||||
$this->resource->getMorphClass() === StandaloneClickhouse::class
|
||||
) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$new_resource = $this->resource->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => $uuid,
|
||||
'name' => $this->resource->name.'-clone-'.$uuid,
|
||||
'status' => 'exited',
|
||||
'started_at' => null,
|
||||
'destination_id' => $new_destination->id,
|
||||
]);
|
||||
$new_resource->save();
|
||||
|
||||
$tags = $this->resource->tags;
|
||||
foreach ($tags as $tag) {
|
||||
$new_resource->tags()->attach($tag->id);
|
||||
$new_destination = StandaloneDocker::ownedByCurrentTeam()->find($destination_id);
|
||||
if (! $new_destination) {
|
||||
$new_destination = SwarmDocker::ownedByCurrentTeam()->find($destination_id);
|
||||
}
|
||||
|
||||
$new_resource->persistentStorages()->delete();
|
||||
$persistentVolumes = $this->resource->persistentStorages()->get();
|
||||
foreach ($persistentVolumes as $volume) {
|
||||
$originalName = $volume->name;
|
||||
$newName = '';
|
||||
|
||||
if (str_starts_with($originalName, 'postgres-data-')) {
|
||||
$newName = 'postgres-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'mysql-data-')) {
|
||||
$newName = 'mysql-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'redis-data-')) {
|
||||
$newName = 'redis-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'clickhouse-data-')) {
|
||||
$newName = 'clickhouse-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'mariadb-data-')) {
|
||||
$newName = 'mariadb-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'mongodb-data-')) {
|
||||
$newName = 'mongodb-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'keydb-data-')) {
|
||||
$newName = 'keydb-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'dragonfly-data-')) {
|
||||
$newName = 'dragonfly-data-'.$new_resource->uuid;
|
||||
} else {
|
||||
if (str_starts_with($volume->name, $this->resource->uuid)) {
|
||||
$newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid);
|
||||
} else {
|
||||
$newName = $new_resource->uuid.'-'.$volume->name;
|
||||
}
|
||||
}
|
||||
|
||||
$newPersistentVolume = $volume->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'uuid',
|
||||
])->fill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $new_resource->id,
|
||||
]);
|
||||
$newPersistentVolume->save();
|
||||
|
||||
if ($this->cloneVolumeData) {
|
||||
try {
|
||||
StopDatabase::dispatch($this->resource);
|
||||
$sourceVolume = $volume->name;
|
||||
$targetVolume = $newPersistentVolume->name;
|
||||
$sourceServer = $this->resource->destination->server;
|
||||
$targetServer = $new_resource->destination->server;
|
||||
|
||||
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
|
||||
|
||||
StartDatabase::dispatch($this->resource);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
if (! $new_destination) {
|
||||
return $this->addError('destination_id', 'Destination not found.');
|
||||
}
|
||||
$uuid = new_public_id();
|
||||
$server = $new_destination->server;
|
||||
|
||||
$fileStorages = $this->resource->fileStorages()->get();
|
||||
foreach ($fileStorages as $storage) {
|
||||
$newStorage = $storage->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'resource_id' => $new_resource->id,
|
||||
]);
|
||||
$newStorage->save();
|
||||
}
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
$new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
|
||||
|
||||
$scheduledBackups = $this->resource->scheduledBackups()->get();
|
||||
foreach ($scheduledBackups as $backup) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$newBackup = $backup->replicate([
|
||||
$route = route('project.application.configuration', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
'environment_uuid' => $this->environmentUuid,
|
||||
'application_uuid' => $new_resource->uuid,
|
||||
]).'#resource-operations';
|
||||
|
||||
return redirect()->to($route);
|
||||
} elseif (
|
||||
$this->resource->getMorphClass() === StandalonePostgresql::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMongodb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMysql::class ||
|
||||
$this->resource->getMorphClass() === StandaloneMariadb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneRedis::class ||
|
||||
$this->resource->getMorphClass() === StandaloneKeydb::class ||
|
||||
$this->resource->getMorphClass() === StandaloneDragonfly::class ||
|
||||
$this->resource->getMorphClass() === StandaloneClickhouse::class
|
||||
) {
|
||||
$uuid = new_public_id();
|
||||
$new_resource = $this->resource->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => $uuid,
|
||||
'database_id' => $new_resource->id,
|
||||
'database_type' => $new_resource->getMorphClass(),
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
$newBackup->save();
|
||||
}
|
||||
|
||||
$environmentVaribles = $this->resource->environment_variables()->get();
|
||||
foreach ($environmentVaribles as $environmentVarible) {
|
||||
$payload = [
|
||||
'resourceable_id' => $new_resource->id,
|
||||
'resourceable_type' => $new_resource->getMorphClass(),
|
||||
];
|
||||
$newEnvironmentVariable = $environmentVarible->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill($payload);
|
||||
$newEnvironmentVariable->save();
|
||||
}
|
||||
|
||||
$route = route('project.database.configuration', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
'environment_uuid' => $this->environmentUuid,
|
||||
'database_uuid' => $new_resource->uuid,
|
||||
]).'#resource-operations';
|
||||
|
||||
return redirect()->to($route);
|
||||
} elseif ($this->resource->type() === 'service') {
|
||||
$uuid = (string) new Cuid2;
|
||||
$new_resource = $this->resource->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => $uuid,
|
||||
'name' => $this->resource->name.'-clone-'.$uuid,
|
||||
'destination_id' => $new_destination->id,
|
||||
'destination_type' => $new_destination->getMorphClass(),
|
||||
'server_id' => $new_destination->server_id, // server_id is probably not needed anymore because of the new polymorphic relationships (here it is needed for clone to a different server to work - but maybe we can drop the column)
|
||||
]);
|
||||
|
||||
$new_resource->save();
|
||||
|
||||
$tags = $this->resource->tags;
|
||||
foreach ($tags as $tag) {
|
||||
$new_resource->tags()->attach($tag->id);
|
||||
}
|
||||
|
||||
$scheduledTasks = $this->resource->scheduled_tasks()->get();
|
||||
foreach ($scheduledTasks as $task) {
|
||||
$newTask = $task->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => (string) new Cuid2,
|
||||
'service_id' => $new_resource->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
$newTask->save();
|
||||
}
|
||||
|
||||
$environmentVariables = $this->resource->environment_variables()->get();
|
||||
foreach ($environmentVariables as $environmentVariable) {
|
||||
$newEnvironmentVariable = $environmentVariable->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'resourceable_id' => $new_resource->id,
|
||||
'resourceable_type' => $new_resource->getMorphClass(),
|
||||
]);
|
||||
$newEnvironmentVariable->save();
|
||||
}
|
||||
|
||||
foreach ($new_resource->applications() as $application) {
|
||||
$application->fill([
|
||||
'name' => $this->resource->name.'-clone-'.$uuid,
|
||||
'status' => 'exited',
|
||||
])->save();
|
||||
'started_at' => null,
|
||||
'destination_id' => $new_destination->id,
|
||||
]);
|
||||
$new_resource->save();
|
||||
|
||||
$persistentVolumes = $application->persistentStorages()->get();
|
||||
$tags = $this->resource->tags;
|
||||
foreach ($tags as $tag) {
|
||||
$new_resource->tags()->attach($tag->id);
|
||||
}
|
||||
|
||||
$new_resource->persistentStorages()->delete();
|
||||
$persistentVolumes = $this->resource->persistentStorages()->get();
|
||||
foreach ($persistentVolumes as $volume) {
|
||||
$originalName = $volume->name;
|
||||
$newName = '';
|
||||
if (str_starts_with($volume->name, $volume->resource->uuid)) {
|
||||
$newName = str($volume->name)->replace($volume->resource->uuid, $application->uuid);
|
||||
|
||||
if (str_starts_with($originalName, 'postgres-data-')) {
|
||||
$newName = 'postgres-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'mysql-data-')) {
|
||||
$newName = 'mysql-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'redis-data-')) {
|
||||
$newName = 'redis-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'clickhouse-data-')) {
|
||||
$newName = 'clickhouse-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'mariadb-data-')) {
|
||||
$newName = 'mariadb-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'mongodb-data-')) {
|
||||
$newName = 'mongodb-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'keydb-data-')) {
|
||||
$newName = 'keydb-data-'.$new_resource->uuid;
|
||||
} elseif (str_starts_with($originalName, 'dragonfly-data-')) {
|
||||
$newName = 'dragonfly-data-'.$new_resource->uuid;
|
||||
} else {
|
||||
$newName = $application->uuid.'-'.str($volume->name)->afterLast('-');
|
||||
if (str_starts_with($volume->name, $this->resource->uuid)) {
|
||||
$newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid);
|
||||
} else {
|
||||
$newName = $new_resource->uuid.'-'.$volume->name;
|
||||
}
|
||||
}
|
||||
|
||||
$newPersistentVolume = $volume->replicate([
|
||||
@@ -283,80 +144,222 @@ class ResourceOperations extends Component
|
||||
'uuid',
|
||||
])->fill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $application->id,
|
||||
'resource_id' => $new_resource->id,
|
||||
]);
|
||||
$newPersistentVolume->save();
|
||||
|
||||
if ($this->cloneVolumeData) {
|
||||
try {
|
||||
StopService::dispatch($application);
|
||||
StopDatabase::dispatch($this->resource);
|
||||
$sourceVolume = $volume->name;
|
||||
$targetVolume = $newPersistentVolume->name;
|
||||
$sourceServer = $application->service->destination->server;
|
||||
$sourceServer = $this->resource->destination->server;
|
||||
$targetServer = $new_resource->destination->server;
|
||||
|
||||
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
|
||||
|
||||
StartService::dispatch($application);
|
||||
StartDatabase::dispatch($this->resource);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($new_resource->databases() as $database) {
|
||||
$database->fill([
|
||||
'status' => 'exited',
|
||||
])->save();
|
||||
|
||||
$persistentVolumes = $database->persistentStorages()->get();
|
||||
foreach ($persistentVolumes as $volume) {
|
||||
$newName = '';
|
||||
if (str_starts_with($volume->name, $volume->resource->uuid)) {
|
||||
$newName = str($volume->name)->replace($volume->resource->uuid, $database->uuid);
|
||||
} else {
|
||||
$newName = $database->uuid.'-'.str($volume->name)->afterLast('-');
|
||||
}
|
||||
|
||||
$newPersistentVolume = $volume->replicate([
|
||||
$fileStorages = $this->resource->fileStorages()->get();
|
||||
foreach ($fileStorages as $storage) {
|
||||
$newStorage = $storage->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'uuid',
|
||||
])->fill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $database->id,
|
||||
'resource_id' => $new_resource->id,
|
||||
]);
|
||||
$newPersistentVolume->save();
|
||||
$newStorage->save();
|
||||
}
|
||||
|
||||
if ($this->cloneVolumeData) {
|
||||
try {
|
||||
StopService::dispatch($database->service);
|
||||
$sourceVolume = $volume->name;
|
||||
$targetVolume = $newPersistentVolume->name;
|
||||
$sourceServer = $database->service->destination->server;
|
||||
$targetServer = $new_resource->destination->server;
|
||||
$scheduledBackups = $this->resource->scheduledBackups()->get();
|
||||
foreach ($scheduledBackups as $backup) {
|
||||
$uuid = new_public_id();
|
||||
$newBackup = $backup->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => $uuid,
|
||||
'database_id' => $new_resource->id,
|
||||
'database_type' => $new_resource->getMorphClass(),
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
$newBackup->save();
|
||||
}
|
||||
|
||||
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
|
||||
$environmentVaribles = $this->resource->environment_variables()->get();
|
||||
foreach ($environmentVaribles as $environmentVarible) {
|
||||
$payload = [
|
||||
'resourceable_id' => $new_resource->id,
|
||||
'resourceable_type' => $new_resource->getMorphClass(),
|
||||
];
|
||||
$newEnvironmentVariable = $environmentVarible->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill($payload);
|
||||
$newEnvironmentVariable->save();
|
||||
}
|
||||
|
||||
StartService::dispatch($database->service);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
|
||||
$route = route('project.database.configuration', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
'environment_uuid' => $this->environmentUuid,
|
||||
'database_uuid' => $new_resource->uuid,
|
||||
]).'#resource-operations';
|
||||
|
||||
return redirect()->to($route);
|
||||
} elseif ($this->resource->type() === 'service') {
|
||||
$uuid = new_public_id();
|
||||
$new_resource = $this->resource->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => $uuid,
|
||||
'name' => $this->resource->name.'-clone-'.$uuid,
|
||||
'destination_id' => $new_destination->id,
|
||||
'destination_type' => $new_destination->getMorphClass(),
|
||||
'server_id' => $new_destination->server_id,
|
||||
]);
|
||||
|
||||
$new_resource->save();
|
||||
|
||||
$tags = $this->resource->tags;
|
||||
foreach ($tags as $tag) {
|
||||
$new_resource->tags()->attach($tag->id);
|
||||
}
|
||||
|
||||
$scheduledTasks = $this->resource->scheduled_tasks()->get();
|
||||
foreach ($scheduledTasks as $task) {
|
||||
$newTask = $task->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => new_public_id(),
|
||||
'service_id' => $new_resource->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
$newTask->save();
|
||||
}
|
||||
|
||||
$environmentVariables = $this->resource->environment_variables()->get();
|
||||
foreach ($environmentVariables as $environmentVariable) {
|
||||
$newEnvironmentVariable = $environmentVariable->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'resourceable_id' => $new_resource->id,
|
||||
'resourceable_type' => $new_resource->getMorphClass(),
|
||||
]);
|
||||
$newEnvironmentVariable->save();
|
||||
}
|
||||
|
||||
foreach ($new_resource->applications() as $application) {
|
||||
$application->fill([
|
||||
'status' => 'exited',
|
||||
])->save();
|
||||
|
||||
$persistentVolumes = $application->persistentStorages()->get();
|
||||
foreach ($persistentVolumes as $volume) {
|
||||
$newName = '';
|
||||
if (str_starts_with($volume->name, $volume->resource->uuid)) {
|
||||
$newName = str($volume->name)->replace($volume->resource->uuid, $application->uuid);
|
||||
} else {
|
||||
$newName = $application->uuid.'-'.str($volume->name)->afterLast('-');
|
||||
}
|
||||
|
||||
$newPersistentVolume = $volume->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'uuid',
|
||||
])->fill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $application->id,
|
||||
]);
|
||||
$newPersistentVolume->save();
|
||||
|
||||
if ($this->cloneVolumeData) {
|
||||
try {
|
||||
StopService::dispatch($application);
|
||||
$sourceVolume = $volume->name;
|
||||
$targetVolume = $newPersistentVolume->name;
|
||||
$sourceServer = $application->service->destination->server;
|
||||
$targetServer = $new_resource->destination->server;
|
||||
|
||||
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
|
||||
|
||||
StartService::dispatch($application);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($new_resource->databases() as $database) {
|
||||
$database->fill([
|
||||
'status' => 'exited',
|
||||
])->save();
|
||||
|
||||
$persistentVolumes = $database->persistentStorages()->get();
|
||||
foreach ($persistentVolumes as $volume) {
|
||||
$newName = '';
|
||||
if (str_starts_with($volume->name, $volume->resource->uuid)) {
|
||||
$newName = str($volume->name)->replace($volume->resource->uuid, $database->uuid);
|
||||
} else {
|
||||
$newName = $database->uuid.'-'.str($volume->name)->afterLast('-');
|
||||
}
|
||||
|
||||
$newPersistentVolume = $volume->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'uuid',
|
||||
])->fill([
|
||||
'name' => $newName,
|
||||
'resource_id' => $database->id,
|
||||
]);
|
||||
$newPersistentVolume->save();
|
||||
|
||||
if ($this->cloneVolumeData) {
|
||||
try {
|
||||
StopService::dispatch($database->service);
|
||||
$sourceVolume = $volume->name;
|
||||
$targetVolume = $newPersistentVolume->name;
|
||||
$sourceServer = $database->service->destination->server;
|
||||
$targetServer = $new_resource->destination->server;
|
||||
|
||||
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
|
||||
|
||||
StartService::dispatch($database->service);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$new_resource->parse();
|
||||
|
||||
$route = route('project.service.configuration', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
'environment_uuid' => $this->environmentUuid,
|
||||
'service_uuid' => $new_resource->uuid,
|
||||
]).'#resource-operations';
|
||||
|
||||
return redirect()->to($route);
|
||||
}
|
||||
|
||||
$new_resource->parse();
|
||||
|
||||
$route = route('project.service.configuration', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
'environment_uuid' => $this->environmentUuid,
|
||||
'service_uuid' => $new_resource->uuid,
|
||||
]).'#resource-operations';
|
||||
|
||||
return redirect()->to($route);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ namespace App\Livewire\Project\Shared;
|
||||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Models\Server;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class Terminal extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public bool $hasShell = true;
|
||||
|
||||
public bool $isTerminalConnected = false;
|
||||
@@ -32,7 +35,11 @@ class Terminal extends Component
|
||||
#[On('send-terminal-command')]
|
||||
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
|
||||
{
|
||||
$this->authorize('canAccessTerminal');
|
||||
|
||||
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
|
||||
$this->authorize('view', $server);
|
||||
|
||||
if (! $server->isTerminalEnabled() || $server->isForceDisabled()) {
|
||||
abort(403, 'Terminal access is disabled on this server.');
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Models\Application;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class UploadConfig extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public $config;
|
||||
|
||||
public $applicationId;
|
||||
@@ -29,13 +32,12 @@ class UploadConfig extends Component
|
||||
public function uploadConfig()
|
||||
{
|
||||
try {
|
||||
$application = Application::findOrFail($this->applicationId);
|
||||
$application = Application::ownedByCurrentTeam()->findOrFail($this->applicationId);
|
||||
$this->authorize('update', $application);
|
||||
$application->setConfig($this->config);
|
||||
$this->dispatch('success', 'Application settings updated');
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
|
||||
return;
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,19 +34,24 @@ class Webhooks extends Component
|
||||
{
|
||||
$this->deploywebhook = generateDeployWebhook($this->resource);
|
||||
|
||||
$this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github');
|
||||
if ($this->canViewSecrets()) {
|
||||
$this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github');
|
||||
$this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab');
|
||||
$this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket');
|
||||
$this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea');
|
||||
}
|
||||
|
||||
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
|
||||
|
||||
$this->gitlabManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitlab');
|
||||
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
|
||||
|
||||
$this->bitbucketManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_bitbucket');
|
||||
$this->bitbucketManualWebhook = generateGitManualWebhook($this->resource, 'bitbucket');
|
||||
|
||||
$this->giteaManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_gitea');
|
||||
$this->giteaManualWebhook = generateGitManualWebhook($this->resource, 'gitea');
|
||||
}
|
||||
|
||||
public function canViewSecrets(): bool
|
||||
{
|
||||
return auth()->user()->can('update', $this->resource);
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -5,11 +5,13 @@ namespace App\Livewire\Project;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public Project $project;
|
||||
|
||||
public string $name;
|
||||
@@ -41,11 +43,12 @@ class Show extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('create', Environment::class);
|
||||
$this->validate();
|
||||
$environment = Environment::create([
|
||||
'name' => $this->name,
|
||||
'project_id' => $this->project->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
|
||||
return redirectRoute($this, 'project.resource.index', [
|
||||
|
||||
@@ -36,6 +36,12 @@ class ApiTokens extends Component
|
||||
#[Locked]
|
||||
public bool $canUseWritePermissions = false;
|
||||
|
||||
#[Locked]
|
||||
public bool $canUseDeployPermissions = false;
|
||||
|
||||
#[Locked]
|
||||
public bool $canUseSensitivePermissions = false;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.security.api-tokens');
|
||||
@@ -46,6 +52,8 @@ class ApiTokens extends Component
|
||||
$this->isApiEnabled = InstanceSettings::get()->is_api_enabled;
|
||||
$this->canUseRootPermissions = auth()->user()->can('useRootPermissions', PersonalAccessToken::class);
|
||||
$this->canUseWritePermissions = auth()->user()->can('useWritePermissions', PersonalAccessToken::class);
|
||||
$this->canUseDeployPermissions = auth()->user()->can('useDeployPermissions', PersonalAccessToken::class);
|
||||
$this->canUseSensitivePermissions = auth()->user()->can('useSensitivePermissions', PersonalAccessToken::class);
|
||||
$this->getTokens();
|
||||
}
|
||||
|
||||
@@ -56,10 +64,9 @@ class ApiTokens extends Component
|
||||
|
||||
public function updatedPermissions($permissionToUpdate)
|
||||
{
|
||||
// Check if user is trying to use restricted permissions
|
||||
// Re-evaluate policies fresh — never trust stored snapshot booleans.
|
||||
if ($permissionToUpdate == 'root' && ! auth()->user()->can('useRootPermissions', PersonalAccessToken::class)) {
|
||||
$this->dispatch('error', 'You do not have permission to use root permissions.');
|
||||
// Remove root from permissions if it was somehow added
|
||||
$this->permissions = array_diff($this->permissions, ['root']);
|
||||
|
||||
return;
|
||||
@@ -67,12 +74,25 @@ class ApiTokens extends Component
|
||||
|
||||
if (in_array($permissionToUpdate, ['write', 'write:sensitive'], true) && ! auth()->user()->can('useWritePermissions', PersonalAccessToken::class)) {
|
||||
$this->dispatch('error', 'You do not have permission to use write permissions.');
|
||||
// Remove write permissions if they were somehow added
|
||||
$this->permissions = array_diff($this->permissions, ['write', 'write:sensitive']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($permissionToUpdate == 'deploy' && ! auth()->user()->can('useDeployPermissions', PersonalAccessToken::class)) {
|
||||
$this->dispatch('error', 'You do not have permission to use deploy permissions.');
|
||||
$this->permissions = array_diff($this->permissions, ['deploy']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($permissionToUpdate == 'read:sensitive' && ! auth()->user()->can('useSensitivePermissions', PersonalAccessToken::class)) {
|
||||
$this->dispatch('error', 'You do not have permission to use read:sensitive permissions.');
|
||||
$this->permissions = array_diff($this->permissions, ['read:sensitive']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($permissionToUpdate == 'root') {
|
||||
$this->permissions = ['root'];
|
||||
} elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions, true)) {
|
||||
@@ -92,7 +112,9 @@ class ApiTokens extends Component
|
||||
try {
|
||||
$this->authorize('create', PersonalAccessToken::class);
|
||||
|
||||
// Validate permissions based on user role
|
||||
// Re-evaluate policies fresh against the current authenticated user.
|
||||
// Never trust $this->canUse* booleans — they come from the Livewire
|
||||
// snapshot which can be replayed from another user's session.
|
||||
if (in_array('root', $this->permissions, true) && ! auth()->user()->can('useRootPermissions', PersonalAccessToken::class)) {
|
||||
throw new \Exception('You do not have permission to create tokens with root permissions.');
|
||||
}
|
||||
@@ -101,6 +123,14 @@ class ApiTokens extends Component
|
||||
throw new \Exception('You do not have permission to create tokens with write permissions.');
|
||||
}
|
||||
|
||||
if (in_array('deploy', $this->permissions, true) && ! auth()->user()->can('useDeployPermissions', PersonalAccessToken::class)) {
|
||||
throw new \Exception('You do not have permission to create tokens with deploy permissions.');
|
||||
}
|
||||
|
||||
if (in_array('read:sensitive', $this->permissions, true) && ! auth()->user()->can('useSensitivePermissions', PersonalAccessToken::class)) {
|
||||
throw new \Exception('You do not have permission to create tokens with read:sensitive permissions.');
|
||||
}
|
||||
|
||||
$this->validate([
|
||||
'description' => 'required|min:3|max:255',
|
||||
'expiresInDays' => 'nullable|integer|in:7,30,60,90,365',
|
||||
@@ -108,6 +138,7 @@ class ApiTokens extends Component
|
||||
$expiresAt = $this->expiresInDays ? now()->addDays($this->expiresInDays) : null;
|
||||
$token = auth()->user()->createToken($this->description, array_values($this->permissions), $expiresAt);
|
||||
$this->getTokens();
|
||||
// Do NOT strip the numeric prefix (e.g. "69|...") — Sanctum uses it to index and look up tokens.
|
||||
session()->flash('token', $token->plainTextToken);
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Security;
|
||||
|
||||
use App\Models\CloudInitScript;
|
||||
use App\Rules\ValidCloudInitYaml;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -20,15 +21,19 @@ class CloudInitScriptForm extends Component
|
||||
|
||||
public function mount(?int $scriptId = null)
|
||||
{
|
||||
if ($scriptId) {
|
||||
$this->scriptId = $scriptId;
|
||||
$cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId);
|
||||
$this->authorize('update', $cloudInitScript);
|
||||
try {
|
||||
if ($scriptId) {
|
||||
$this->scriptId = $scriptId;
|
||||
$cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId);
|
||||
$this->authorize('update', $cloudInitScript);
|
||||
|
||||
$this->name = $cloudInitScript->name;
|
||||
$this->script = $cloudInitScript->script;
|
||||
} else {
|
||||
$this->authorize('create', CloudInitScript::class);
|
||||
$this->name = $cloudInitScript->name;
|
||||
$this->script = $cloudInitScript->script;
|
||||
} else {
|
||||
$this->authorize('create', CloudInitScript::class);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +41,7 @@ class CloudInitScriptForm extends Component
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'script' => ['required', 'string', new \App\Rules\ValidCloudInitYaml],
|
||||
'script' => ['required', 'string', new ValidCloudInitYaml],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -64,17 +69,29 @@ class CloudInitScriptForm extends Component
|
||||
'script' => $this->script,
|
||||
]);
|
||||
|
||||
auditLog('ui.cloud_init_script.updated', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_init_script_id' => $cloudInitScript->id,
|
||||
'cloud_init_script_name' => $cloudInitScript->name,
|
||||
]);
|
||||
|
||||
$message = 'Cloud-init script updated successfully.';
|
||||
} else {
|
||||
// Create new script
|
||||
$this->authorize('create', CloudInitScript::class);
|
||||
|
||||
CloudInitScript::create([
|
||||
$cloudInitScript = CloudInitScript::create([
|
||||
'team_id' => currentTeam()->id,
|
||||
'name' => $this->name,
|
||||
'script' => $this->script,
|
||||
]);
|
||||
|
||||
auditLog('ui.cloud_init_script.created', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_init_script_id' => $cloudInitScript->id,
|
||||
'cloud_init_script_name' => $cloudInitScript->name,
|
||||
]);
|
||||
|
||||
$message = 'Cloud-init script created successfully.';
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,16 @@ class CloudInitScripts extends Component
|
||||
$script = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId);
|
||||
$this->authorize('delete', $script);
|
||||
|
||||
$scriptName = $script->name;
|
||||
$script->delete();
|
||||
$this->loadScripts();
|
||||
|
||||
auditLog('ui.cloud_init_script.deleted', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_init_script_id' => $scriptId,
|
||||
'cloud_init_script_name' => $scriptName,
|
||||
]);
|
||||
|
||||
$this->dispatch('success', 'Cloud-init script deleted successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -21,7 +21,11 @@ class CloudProviderTokenForm extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('create', CloudProviderToken::class);
|
||||
try {
|
||||
$this->authorize('create', CloudProviderToken::class);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
@@ -50,7 +54,6 @@ class CloudProviderTokenForm extends Component
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->timeout(10)->get('https://api.hetzner.cloud/v1/servers');
|
||||
ray($response);
|
||||
|
||||
return $response->successful();
|
||||
}
|
||||
@@ -81,6 +84,13 @@ class CloudProviderTokenForm extends Component
|
||||
'name' => $this->name,
|
||||
]);
|
||||
|
||||
auditLog('ui.cloud_token.created', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_token_uuid' => $savedToken->uuid,
|
||||
'cloud_token_name' => $savedToken->name,
|
||||
'provider' => $savedToken->provider,
|
||||
]);
|
||||
|
||||
$this->reset(['token', 'name']);
|
||||
|
||||
// Dispatch event with token ID so parent components can react
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire\Security;
|
||||
|
||||
use App\Models\CloudProviderToken;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Component;
|
||||
|
||||
class CloudProviderTokens extends Component
|
||||
@@ -14,8 +15,12 @@ class CloudProviderTokens extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('viewAny', CloudProviderToken::class);
|
||||
$this->loadTokens();
|
||||
try {
|
||||
$this->authorize('viewAny', CloudProviderToken::class);
|
||||
$this->loadTokens();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function getListeners()
|
||||
@@ -53,6 +58,14 @@ class CloudProviderTokens extends Component
|
||||
} else {
|
||||
$this->dispatch('error', 'Unknown provider.');
|
||||
}
|
||||
|
||||
auditLog('ui.cloud_token.validated', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_token_uuid' => $token->uuid,
|
||||
'cloud_token_name' => $token->name,
|
||||
'provider' => $token->provider,
|
||||
'valid' => $isValid ?? false,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -61,7 +74,7 @@ class CloudProviderTokens extends Component
|
||||
private function validateHetznerToken(string $token): bool
|
||||
{
|
||||
try {
|
||||
$response = \Illuminate\Support\Facades\Http::withToken($token)
|
||||
$response = Http::withToken($token)
|
||||
->timeout(10)
|
||||
->get('https://api.hetzner.cloud/v1/servers?per_page=1');
|
||||
|
||||
@@ -74,7 +87,7 @@ class CloudProviderTokens extends Component
|
||||
private function validateDigitalOceanToken(string $token): bool
|
||||
{
|
||||
try {
|
||||
$response = \Illuminate\Support\Facades\Http::withToken($token)
|
||||
$response = Http::withToken($token)
|
||||
->timeout(10)
|
||||
->get('https://api.digitalocean.com/v2/account');
|
||||
|
||||
@@ -98,9 +111,19 @@ class CloudProviderTokens extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$tokenUuid = $token->uuid;
|
||||
$tokenName = $token->name;
|
||||
$tokenProvider = $token->provider;
|
||||
$token->delete();
|
||||
$this->loadTokens();
|
||||
|
||||
auditLog('ui.cloud_token.deleted', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_token_uuid' => $tokenUuid,
|
||||
'cloud_token_name' => $tokenName,
|
||||
'provider' => $tokenProvider,
|
||||
]);
|
||||
|
||||
$this->dispatch('success', 'Cloud provider token deleted successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -21,8 +21,12 @@ class Index extends Component
|
||||
|
||||
public function cleanupUnusedKeys()
|
||||
{
|
||||
$this->authorize('create', PrivateKey::class);
|
||||
PrivateKey::cleanupUnusedKeys();
|
||||
$this->dispatch('success', 'Unused keys have been cleaned up.');
|
||||
try {
|
||||
$this->authorize('create', PrivateKey::class);
|
||||
PrivateKey::cleanupUnusedKeys();
|
||||
$this->dispatch('success', 'Unused keys have been cleaned up.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire\Server\CloudProviderToken;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
@@ -67,6 +68,16 @@ class Show extends Component
|
||||
|
||||
$this->server->cloudProviderToken()->associate($ownedToken);
|
||||
$this->server->save();
|
||||
|
||||
auditLog('ui.server.cloud_token_assigned', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'server_name' => $this->server->name,
|
||||
'cloud_token_uuid' => $ownedToken->uuid,
|
||||
'cloud_token_name' => $ownedToken->name,
|
||||
'provider' => $ownedToken->provider,
|
||||
]);
|
||||
|
||||
$this->dispatch('success', 'Hetzner token updated successfully.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Exception $e) {
|
||||
@@ -79,7 +90,7 @@ class Show extends Component
|
||||
{
|
||||
try {
|
||||
// First, validate the token itself
|
||||
$response = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->token,
|
||||
])->timeout(10)->get('https://api.hetzner.cloud/v1/servers');
|
||||
|
||||
@@ -92,7 +103,7 @@ class Show extends Component
|
||||
|
||||
// Check if this token can access the specific Hetzner server
|
||||
if ($this->server->hetzner_server_id) {
|
||||
$serverResponse = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
$serverResponse = Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->token,
|
||||
])->timeout(10)->get("https://api.hetzner.cloud/v1/servers/{$this->server->hetzner_server_id}");
|
||||
|
||||
@@ -123,7 +134,7 @@ class Show extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->token,
|
||||
])->timeout(10)->get('https://api.hetzner.cloud/v1/servers');
|
||||
|
||||
@@ -132,6 +143,16 @@ class Show extends Component
|
||||
} else {
|
||||
$this->dispatch('error', 'Hetzner token is invalid or has insufficient permissions.');
|
||||
}
|
||||
|
||||
auditLog('ui.server.cloud_token_validated', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'server_name' => $this->server->name,
|
||||
'cloud_token_uuid' => $token->uuid,
|
||||
'cloud_token_name' => $token->name,
|
||||
'provider' => $token->provider,
|
||||
'valid' => $response->successful(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -72,12 +72,16 @@ class CloudflareTunnel extends Component
|
||||
|
||||
public function manualCloudflareConfig()
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
$this->isCloudflareTunnelsEnabled = true;
|
||||
$this->server->settings->is_cloudflare_tunnel = true;
|
||||
$this->server->settings->save();
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Cloudflare Tunnel enabled.');
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->isCloudflareTunnelsEnabled = true;
|
||||
$this->server->settings->is_cloudflare_tunnel = true;
|
||||
$this->server->settings->save();
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Cloudflare Tunnel enabled.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function automatedCloudflareConfig()
|
||||
|
||||
@@ -69,6 +69,11 @@ class Destinations extends Component
|
||||
|
||||
public function scan()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
if ($this->server->isSwarm()) {
|
||||
$alreadyAddedNetworks = $this->server->swarmDockers;
|
||||
} else {
|
||||
|
||||
@@ -79,14 +79,18 @@ class ByHetzner extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('viewAny', CloudProviderToken::class);
|
||||
$this->loadTokens();
|
||||
$this->loadSavedCloudInitScripts();
|
||||
$this->server_name = generate_random_name();
|
||||
$this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get();
|
||||
try {
|
||||
$this->authorize('viewAny', CloudProviderToken::class);
|
||||
$this->loadTokens();
|
||||
$this->loadSavedCloudInitScripts();
|
||||
$this->server_name = generate_random_name();
|
||||
$this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get();
|
||||
|
||||
if ($this->private_keys->count() > 0) {
|
||||
$this->private_key_id = $this->private_keys->first()->id;
|
||||
if ($this->private_keys->count() > 0) {
|
||||
$this->private_key_id = $this->private_keys->first()->id;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,11 +102,15 @@ class Proxy extends Component
|
||||
|
||||
public function changeProxy()
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
$this->server->proxy = null;
|
||||
$this->server->save();
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->server->proxy = null;
|
||||
$this->server->save();
|
||||
|
||||
$this->dispatch('reloadWindow');
|
||||
$this->dispatch('reloadWindow');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function selectProxy($proxy_type)
|
||||
|
||||
@@ -22,33 +22,37 @@ class DynamicConfigurationNavbar extends Component
|
||||
|
||||
public function delete(string $fileName)
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
$proxy_path = $this->server->proxyPath();
|
||||
$proxy_type = $this->server->proxyType();
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$proxy_path = $this->server->proxyPath();
|
||||
$proxy_type = $this->server->proxyType();
|
||||
|
||||
// Decode filename: pipes are used to encode dots for Livewire property binding
|
||||
// (e.g., 'my|service.yaml' -> 'my.service.yaml')
|
||||
// This must happen BEFORE validation because validateFilenameSafe()
|
||||
// rejects pipe characters through validateShellSafePath().
|
||||
$file = str_replace('|', '.', $fileName);
|
||||
// Decode filename: pipes are used to encode dots for Livewire property binding
|
||||
// (e.g., 'my|service.yaml' -> 'my.service.yaml')
|
||||
// This must happen BEFORE validation because validateFilenameSafe()
|
||||
// rejects pipe characters through validateShellSafePath().
|
||||
$file = str_replace('|', '.', $fileName);
|
||||
|
||||
validateFilenameSafe($file, 'proxy configuration filename');
|
||||
validateFilenameSafe($file, 'proxy configuration filename');
|
||||
|
||||
if ($proxy_type === 'CADDY' && $file === 'Caddyfile') {
|
||||
$this->dispatch('error', 'Cannot delete Caddyfile.');
|
||||
if ($proxy_type === 'CADDY' && $file === 'Caddyfile') {
|
||||
$this->dispatch('error', 'Cannot delete Caddyfile.');
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
$fullPath = "{$proxy_path}/dynamic/{$file}";
|
||||
$escapedPath = escapeshellarg($fullPath);
|
||||
instant_remote_process(["rm -f {$escapedPath}"], $this->server);
|
||||
if ($proxy_type === 'CADDY') {
|
||||
$this->server->reloadCaddy();
|
||||
}
|
||||
$this->dispatch('success', 'File deleted.');
|
||||
$this->dispatch('loadDynamicConfigurations');
|
||||
$this->dispatch('refresh');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
$fullPath = "{$proxy_path}/dynamic/{$file}";
|
||||
$escapedPath = escapeshellarg($fullPath);
|
||||
instant_remote_process(["rm -f {$escapedPath}"], $this->server);
|
||||
if ($proxy_type === 'CADDY') {
|
||||
$this->server->reloadCaddy();
|
||||
}
|
||||
$this->dispatch('success', 'File deleted.');
|
||||
$this->dispatch('loadDynamicConfigurations');
|
||||
$this->dispatch('refresh');
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
namespace App\Livewire\Server\Proxy;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class DynamicConfigurations extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
public $parameters = [];
|
||||
@@ -35,6 +38,11 @@ class DynamicConfigurations extends Component
|
||||
|
||||
public function loadDynamicConfigurations()
|
||||
{
|
||||
try {
|
||||
$this->authorize('view', $this->server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$proxy_path = $this->server->proxyPath();
|
||||
$files = instant_remote_process(["mkdir -p $proxy_path/dynamic && ls -1 {$proxy_path}/dynamic"], $this->server);
|
||||
$files = collect(explode("\n", $files))->filter(fn ($file) => ! empty($file));
|
||||
|
||||
@@ -30,38 +30,53 @@ class Resources extends Component
|
||||
|
||||
public function startUnmanaged($id)
|
||||
{
|
||||
if (! ValidationPatterns::isValidContainerName($id)) {
|
||||
$this->dispatch('error', 'Invalid container identifier.');
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
if (! ValidationPatterns::isValidContainerName($id)) {
|
||||
$this->dispatch('error', 'Invalid container identifier.');
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
$this->server->startUnmanaged($id);
|
||||
$this->dispatch('success', 'Container started.');
|
||||
$this->loadUnmanagedContainers();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$this->server->startUnmanaged($id);
|
||||
$this->dispatch('success', 'Container started.');
|
||||
$this->loadUnmanagedContainers();
|
||||
}
|
||||
|
||||
public function restartUnmanaged($id)
|
||||
{
|
||||
if (! ValidationPatterns::isValidContainerName($id)) {
|
||||
$this->dispatch('error', 'Invalid container identifier.');
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
if (! ValidationPatterns::isValidContainerName($id)) {
|
||||
$this->dispatch('error', 'Invalid container identifier.');
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
$this->server->restartUnmanaged($id);
|
||||
$this->dispatch('success', 'Container restarted.');
|
||||
$this->loadUnmanagedContainers();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$this->server->restartUnmanaged($id);
|
||||
$this->dispatch('success', 'Container restarted.');
|
||||
$this->loadUnmanagedContainers();
|
||||
}
|
||||
|
||||
public function stopUnmanaged($id)
|
||||
{
|
||||
if (! ValidationPatterns::isValidContainerName($id)) {
|
||||
$this->dispatch('error', 'Invalid container identifier.');
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
if (! ValidationPatterns::isValidContainerName($id)) {
|
||||
$this->dispatch('error', 'Invalid container identifier.');
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
$this->server->stopUnmanaged($id);
|
||||
$this->dispatch('success', 'Container stopped.');
|
||||
$this->loadUnmanagedContainers();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$this->server->stopUnmanaged($id);
|
||||
$this->dispatch('success', 'Container stopped.');
|
||||
$this->loadUnmanagedContainers();
|
||||
}
|
||||
|
||||
public function refreshStatus()
|
||||
|
||||
@@ -41,7 +41,11 @@ class Patches extends Component
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid($this->parameters['server_uuid'])->firstOrFail();
|
||||
$this->authorize('viewSecurity', $this->server);
|
||||
try {
|
||||
$this->authorize('viewSecurity', $this->server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkForUpdatesDispatch()
|
||||
@@ -69,14 +73,14 @@ class Patches extends Component
|
||||
|
||||
public function updateAllPackages()
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
if (! $this->packageManager || ! $this->osId) {
|
||||
$this->dispatch('error', message: 'Run "Check for updates" first.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
if (! $this->packageManager || ! $this->osId) {
|
||||
$this->dispatch('error', message: 'Run "Check for updates" first.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$activity = UpdatePackage::run(
|
||||
server: $this->server,
|
||||
packageManager: $this->packageManager,
|
||||
@@ -84,8 +88,8 @@ class Patches extends Component
|
||||
all: true
|
||||
);
|
||||
$this->dispatch('activityMonitor', $activity->id, ServerPackageUpdated::class);
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', message: $e->getMessage());
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
namespace App\Livewire\Server\Sentinel;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class Logs extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
public array $parameters = [];
|
||||
@@ -19,7 +22,11 @@ class Logs extends Component
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->authorize('viewSentinel', $this->server);
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
namespace App\Livewire\Server\Sentinel;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public ?Server $server = null;
|
||||
|
||||
public array $parameters = [];
|
||||
@@ -19,7 +22,11 @@ class Show extends Component
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->authorize('viewSentinel', $this->server);
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
|
||||
@@ -299,18 +299,22 @@ class Show extends Component
|
||||
|
||||
public function checkLocalhostConnection()
|
||||
{
|
||||
$this->syncData(true);
|
||||
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
|
||||
if ($uptime) {
|
||||
$this->dispatch('success', 'Server is reachable.');
|
||||
$this->server->settings->is_reachable = $this->isReachable = true;
|
||||
$this->server->settings->is_usable = $this->isUsable = true;
|
||||
$this->server->settings->save();
|
||||
ServerReachabilityChanged::dispatch($this->server);
|
||||
} else {
|
||||
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);
|
||||
try {
|
||||
$this->syncData(true);
|
||||
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
|
||||
if ($uptime) {
|
||||
$this->dispatch('success', 'Server is reachable.');
|
||||
$this->server->settings->is_reachable = $this->isReachable = true;
|
||||
$this->server->settings->is_usable = $this->isUsable = true;
|
||||
$this->server->settings->save();
|
||||
ServerReachabilityChanged::dispatch($this->server);
|
||||
} else {
|
||||
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,6 +417,7 @@ class Show extends Component
|
||||
public function checkHetznerServerStatus(bool $manual = false)
|
||||
{
|
||||
try {
|
||||
$this->authorize('view', $this->server);
|
||||
if (! $this->server->hetzner_server_id || ! $this->server->cloudProviderToken) {
|
||||
$this->dispatch('error', 'This server is not associated with a Hetzner Cloud server or token.');
|
||||
|
||||
@@ -480,6 +485,7 @@ class Show extends Component
|
||||
public function startHetznerServer()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
if (! $this->server->hetzner_server_id || ! $this->server->cloudProviderToken) {
|
||||
$this->dispatch('error', 'This server is not associated with a Hetzner Cloud server or token.');
|
||||
|
||||
|
||||
@@ -72,32 +72,40 @@ class ValidateAndInstall extends Component
|
||||
|
||||
public function retry()
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
$this->uptime = null;
|
||||
$this->supported_os_type = null;
|
||||
$this->prerequisites_installed = null;
|
||||
$this->docker_installed = null;
|
||||
$this->docker_compose_installed = null;
|
||||
$this->docker_version = null;
|
||||
$this->error = null;
|
||||
$this->number_of_tries = 0;
|
||||
$this->init();
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
$this->uptime = null;
|
||||
$this->supported_os_type = null;
|
||||
$this->prerequisites_installed = null;
|
||||
$this->docker_installed = null;
|
||||
$this->docker_compose_installed = null;
|
||||
$this->docker_version = null;
|
||||
$this->error = null;
|
||||
$this->number_of_tries = 0;
|
||||
$this->init();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function validateConnection()
|
||||
{
|
||||
$this->authorize('update', $this->server);
|
||||
['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection();
|
||||
if (! $this->uptime) {
|
||||
$sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$sanitizedError.'</div>';
|
||||
$this->server->update([
|
||||
'validation_logs' => $this->error,
|
||||
]);
|
||||
try {
|
||||
$this->authorize('update', $this->server);
|
||||
['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection();
|
||||
if (! $this->uptime) {
|
||||
$sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$sanitizedError.'</div>';
|
||||
$this->server->update([
|
||||
'validation_logs' => $this->error,
|
||||
]);
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
$this->dispatch('validateOS');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
$this->dispatch('validateOS');
|
||||
}
|
||||
|
||||
public function validateOS()
|
||||
|
||||
@@ -5,11 +5,14 @@ namespace App\Livewire\Settings;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Rules\ValidDnsServers;
|
||||
use App\Rules\ValidIpOrCidr;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class Advanced extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public InstanceSettings $settings;
|
||||
|
||||
#[Validate('boolean')]
|
||||
@@ -77,6 +80,7 @@ class Advanced extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->validate();
|
||||
|
||||
$this->custom_dns_servers = str($this->custom_dns_servers)->replaceEnd(',', '')->trim();
|
||||
@@ -146,6 +150,7 @@ class Advanced extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->settings->is_registration_enabled = $this->is_registration_enabled;
|
||||
$this->settings->do_not_track = $this->do_not_track;
|
||||
$this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
|
||||
@@ -178,6 +183,7 @@ class Advanced extends Component
|
||||
|
||||
public function toggleTwoStepConfirmation($password): bool
|
||||
{
|
||||
$this->authorize('update', $this->settings);
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@ namespace App\Livewire\Settings;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public InstanceSettings $settings;
|
||||
|
||||
public ?Server $server = null;
|
||||
@@ -87,6 +90,7 @@ class Index extends Component
|
||||
|
||||
public function instantSave($isSave = true)
|
||||
{
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->validate();
|
||||
$this->settings->fqdn = $this->fqdn ? trim($this->fqdn) : $this->fqdn;
|
||||
$this->settings->public_port_min = $this->public_port_min;
|
||||
@@ -104,6 +108,7 @@ class Index extends Component
|
||||
|
||||
public function confirmDomainUsage()
|
||||
{
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->forceSaveDomains = true;
|
||||
$this->showDomainConflictModal = false;
|
||||
$this->submit();
|
||||
@@ -112,6 +117,7 @@ class Index extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$error_show = false;
|
||||
$this->resetErrorBag();
|
||||
|
||||
@@ -173,6 +179,7 @@ class Index extends Component
|
||||
public function buildHelperImage()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
if (! isDev()) {
|
||||
$this->dispatch('error', 'Building helper image is only available in development mode.');
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ namespace App\Livewire\Settings;
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class Updates extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public InstanceSettings $settings;
|
||||
|
||||
public ?Server $server = null;
|
||||
@@ -41,6 +44,7 @@ class Updates extends Component
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
if ($this->settings->is_auto_update_enabled === true) {
|
||||
$this->validate([
|
||||
'auto_update_frequency' => ['required', 'string'],
|
||||
@@ -59,6 +63,7 @@ class Updates extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->resetErrorBag();
|
||||
$this->validate();
|
||||
|
||||
@@ -91,6 +96,7 @@ class Updates extends Component
|
||||
|
||||
public function checkManually()
|
||||
{
|
||||
$this->authorize('update', $this->settings);
|
||||
CheckForUpdatesJob::dispatchSync();
|
||||
$this->dispatch('updateAvailable');
|
||||
$settings = instanceSettings();
|
||||
|
||||
@@ -8,12 +8,15 @@ use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class SettingsBackup extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public InstanceSettings $settings;
|
||||
|
||||
public Server $server;
|
||||
@@ -77,6 +80,7 @@ class SettingsBackup extends Component
|
||||
public function addCoolifyDatabase()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$server = Server::findOrFail(0);
|
||||
$out = instant_remote_process(['docker inspect coolify-db'], $server);
|
||||
$envs = format_docker_envs_to_json($out);
|
||||
@@ -123,14 +127,19 @@ class SettingsBackup extends Component
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->validate();
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->validate();
|
||||
|
||||
$this->database->update([
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'postgres_user' => $this->postgres_user,
|
||||
'postgres_password' => $this->postgres_password,
|
||||
]);
|
||||
$this->dispatch('success', 'Backup updated.');
|
||||
$this->database->update([
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'postgres_user' => $this->postgres_user,
|
||||
'postgres_password' => $this->postgres_password,
|
||||
]);
|
||||
$this->dispatch('success', 'Backup updated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Notifications\TransactionalEmails\Test;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
@@ -12,6 +13,8 @@ use Livewire\Component;
|
||||
|
||||
class SettingsEmail extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public InstanceSettings $settings;
|
||||
|
||||
#[Locked]
|
||||
@@ -103,6 +106,7 @@ class SettingsEmail extends Component
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->resetErrorBag();
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Transactional email settings updated.');
|
||||
@@ -114,6 +118,7 @@ class SettingsEmail extends Component
|
||||
public function instantSave(string $type)
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$currentSmtpEnabled = $this->settings->smtp_enabled;
|
||||
$currentResendEnabled = $this->settings->resend_enabled;
|
||||
$this->resetErrorBag();
|
||||
@@ -141,6 +146,7 @@ class SettingsEmail extends Component
|
||||
public function submitSmtp()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->validate([
|
||||
'smtpEnabled' => 'boolean',
|
||||
'smtpFromAddress' => 'required|email',
|
||||
@@ -184,6 +190,7 @@ class SettingsEmail extends Component
|
||||
public function submitResend()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->validate([
|
||||
'resendEnabled' => 'boolean',
|
||||
'resendApiKey' => 'required|string',
|
||||
@@ -214,6 +221,7 @@ class SettingsEmail extends Component
|
||||
public function sendTestEmail()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->validate([
|
||||
'testEmailAddress' => 'required|email',
|
||||
], [
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\OauthSetting;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class SettingsOauth extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public $oauth_settings_map;
|
||||
|
||||
protected function rules()
|
||||
@@ -131,6 +134,7 @@ class SettingsOauth extends Component
|
||||
public function instantSave(string $provider)
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', instanceSettings());
|
||||
$this->updateOauthSettings($provider);
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
@@ -139,7 +143,12 @@ class SettingsOauth extends Component
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->updateOauthSettings();
|
||||
$this->dispatch('success', 'Instance settings updated successfully!');
|
||||
try {
|
||||
$this->authorize('update', instanceSettings());
|
||||
$this->updateOauthSettings();
|
||||
$this->dispatch('success', 'Instance settings updated successfully!');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,12 @@ class Show extends Component
|
||||
|
||||
private function formatEnvironmentVariables($variables)
|
||||
{
|
||||
return $variables->map(function ($item) {
|
||||
$isMember = auth()->user()?->isMember();
|
||||
|
||||
return $variables->map(function ($item) use ($isMember) {
|
||||
if ($isMember) {
|
||||
return "$item->key=(Hidden, only admins can view)";
|
||||
}
|
||||
if ($item->is_shown_once) {
|
||||
return "$item->key=(Locked Secret, delete and add again to change)";
|
||||
}
|
||||
|
||||
@@ -58,9 +58,13 @@ class Show extends Component
|
||||
|
||||
public function switch()
|
||||
{
|
||||
$this->authorize('view', $this->project);
|
||||
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
|
||||
$this->getDevView();
|
||||
try {
|
||||
$this->authorize('view', $this->project);
|
||||
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
|
||||
$this->getDevView();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function getDevView()
|
||||
@@ -70,7 +74,12 @@ class Show extends Component
|
||||
|
||||
private function formatEnvironmentVariables($variables)
|
||||
{
|
||||
return $variables->map(function ($item) {
|
||||
$isMember = auth()->user()?->isMember();
|
||||
|
||||
return $variables->map(function ($item) use ($isMember) {
|
||||
if ($isMember) {
|
||||
return "$item->key=(Hidden, only admins can view)";
|
||||
}
|
||||
if ($item->is_shown_once) {
|
||||
return "$item->key=(Locked Secret, delete and add again to change)";
|
||||
}
|
||||
|
||||
@@ -52,9 +52,13 @@ class Index extends Component
|
||||
|
||||
public function switch()
|
||||
{
|
||||
$this->authorize('view', $this->team);
|
||||
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
|
||||
$this->getDevView();
|
||||
try {
|
||||
$this->authorize('view', $this->team);
|
||||
$this->view = $this->view === 'normal' ? 'dev' : 'normal';
|
||||
$this->getDevView();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function getDevView()
|
||||
@@ -64,7 +68,12 @@ class Index extends Component
|
||||
|
||||
private function formatEnvironmentVariables($variables)
|
||||
{
|
||||
return $variables->map(function ($item) {
|
||||
$isMember = auth()->user()?->isMember();
|
||||
|
||||
return $variables->map(function ($item) use ($isMember) {
|
||||
if ($isMember) {
|
||||
return "$item->key=(Hidden, only admins can view)";
|
||||
}
|
||||
if ($item->is_shown_once) {
|
||||
return "$item->key=(Locked Secret, delete and add again to change)";
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ class Form extends Component
|
||||
|
||||
public ?bool $isUsable = null;
|
||||
|
||||
public bool $isPasswordHiddenForMember = false;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
@@ -110,6 +112,12 @@ class Form extends Component
|
||||
public function mount()
|
||||
{
|
||||
$this->syncData(false);
|
||||
|
||||
$this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false;
|
||||
if ($this->isPasswordHiddenForMember) {
|
||||
$this->key = '';
|
||||
$this->secret = '';
|
||||
}
|
||||
}
|
||||
|
||||
public function testConnection()
|
||||
|
||||
@@ -4,16 +4,21 @@ namespace App\Livewire\Storage;
|
||||
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class Resources extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public S3Storage $storage;
|
||||
|
||||
public array $selectedStorages = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('view', $this->storage);
|
||||
|
||||
$backups = ScheduledDatabaseBackup::where('s3_storage_id', $this->storage->id)
|
||||
->where('save_s3', true)
|
||||
->get();
|
||||
@@ -25,6 +30,8 @@ class Resources extends Component
|
||||
|
||||
public function disableS3(int $backupId): void
|
||||
{
|
||||
$this->authorize('update', $this->storage);
|
||||
|
||||
$backup = ScheduledDatabaseBackup::where('id', $backupId)
|
||||
->where('s3_storage_id', $this->storage->id)
|
||||
->firstOrFail();
|
||||
@@ -41,6 +48,8 @@ class Resources extends Component
|
||||
|
||||
public function moveBackup(int $backupId): void
|
||||
{
|
||||
$this->authorize('update', $this->storage);
|
||||
|
||||
$backup = ScheduledDatabaseBackup::where('id', $backupId)
|
||||
->where('s3_storage_id', $this->storage->id)
|
||||
->firstOrFail();
|
||||
@@ -62,6 +71,8 @@ class Resources extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$this->authorize('update', $newStorage);
|
||||
|
||||
$backup->update(['s3_storage_id' => $newStorage->id]);
|
||||
|
||||
unset($this->selectedStorages[$backupId]);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire\Storage;
|
||||
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -23,7 +24,11 @@ class Show extends Component
|
||||
if (! $this->storage) {
|
||||
abort(404);
|
||||
}
|
||||
$this->authorize('view', $this->storage);
|
||||
try {
|
||||
$this->authorize('view', $this->storage);
|
||||
} catch (AuthorizationException) {
|
||||
return $this->redirectRoute('storage.index', navigate: true);
|
||||
}
|
||||
$this->currentRoute = request()->route()->getName();
|
||||
$this->backupCount = ScheduledDatabaseBackup::where('s3_storage_id', $this->storage->id)->count();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ class Actions extends Component
|
||||
|
||||
public bool $refundAlreadyUsed = false;
|
||||
|
||||
public bool $refundLatestPayment = false;
|
||||
|
||||
public string $billingInterval = 'monthly';
|
||||
|
||||
public ?string $nextBillingDate = null;
|
||||
@@ -100,7 +102,7 @@ class Actions extends Component
|
||||
return 'Invalid password.';
|
||||
}
|
||||
|
||||
$result = (new RefundSubscription)->execute(currentTeam());
|
||||
$result = app(RefundSubscription::class)->execute(currentTeam());
|
||||
|
||||
if ($result['success']) {
|
||||
$this->dispatch('success', 'Subscription refunded successfully.');
|
||||
@@ -114,12 +116,28 @@ class Actions extends Component
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cancelImmediately(string $password): bool|string
|
||||
public function cancelImmediately(string $password, array $selectedActions = []): bool|string
|
||||
{
|
||||
if (! shouldSkipPasswordConfirmation() && ! Hash::check($password, auth()->user()->password)) {
|
||||
return 'Invalid password.';
|
||||
}
|
||||
|
||||
if (in_array('refundLatestPayment', $selectedActions, true)) {
|
||||
// Eligibility is re-validated server-side inside RefundSubscription::execute()
|
||||
$result = app(RefundSubscription::class)->execute(currentTeam());
|
||||
|
||||
if ($result['success']) {
|
||||
$this->dispatch('success', 'Subscription refunded and cancelled successfully.');
|
||||
$this->redirect(route('subscription.index'), navigate: true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->dispatch('error', 'Something went wrong with the refund. Please <a href="'.config('constants.urls.contact').'" target="_blank" class="underline">contact us</a>.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$team = currentTeam();
|
||||
$subscription = $team->subscription;
|
||||
|
||||
@@ -130,7 +148,7 @@ class Actions extends Component
|
||||
}
|
||||
|
||||
try {
|
||||
$stripe = new StripeClient(config('subscription.stripe_api_key'));
|
||||
$stripe = app(StripeClient::class);
|
||||
$stripe->subscriptions->cancel($subscription->stripe_subscription_id);
|
||||
|
||||
$subscription->update([
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire\Subscription;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Livewire\Component;
|
||||
use Stripe\StripeClient;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
@@ -52,7 +53,7 @@ class Index extends Component
|
||||
{
|
||||
try {
|
||||
$subscription = currentTeam()->subscription;
|
||||
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
|
||||
$stripe = app(StripeClient::class);
|
||||
$customer = $stripe->customers->retrieve(currentTeam()->subscription->stripe_customer_id);
|
||||
if ($customer) {
|
||||
$subscriptions = $stripe->subscriptions->all(['customer' => $customer->id]);
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire\Tags;
|
||||
use App\Http\Controllers\Api\DeployController;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Title;
|
||||
@@ -13,6 +14,8 @@ use Livewire\Component;
|
||||
#[Title('Tags | Coolify')]
|
||||
class Show extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
#[Locked]
|
||||
public ?string $tagName = null;
|
||||
|
||||
@@ -73,6 +76,12 @@ class Show extends Component
|
||||
public function redeployAll()
|
||||
{
|
||||
try {
|
||||
$this->applications->each(function ($resource) {
|
||||
$this->authorize('deploy', $resource);
|
||||
});
|
||||
$this->services->each(function ($resource) {
|
||||
$this->authorize('deploy', $resource);
|
||||
});
|
||||
$message = collect([]);
|
||||
$this->applications->each(function ($resource) use ($message) {
|
||||
$deploy = new DeployController;
|
||||
|
||||
@@ -25,6 +25,9 @@ class AdminView extends Component
|
||||
|
||||
public function submitSearch()
|
||||
{
|
||||
if (! isInstanceAdmin()) {
|
||||
return;
|
||||
}
|
||||
if ($this->search !== '') {
|
||||
$this->users = User::where(function ($query) {
|
||||
$query->where('name', 'like', "%{$this->search}%")
|
||||
@@ -39,6 +42,9 @@ class AdminView extends Component
|
||||
|
||||
public function getUsers()
|
||||
{
|
||||
if (! isInstanceAdmin()) {
|
||||
return;
|
||||
}
|
||||
$users = User::where('id', '!=', auth()->id())->get();
|
||||
if ($users->count() > $this->number_of_users_to_show) {
|
||||
$this->lots_of_users = true;
|
||||
|
||||
+24
-15
@@ -7,6 +7,7 @@ use App\Models\TeamInvitation;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -95,23 +96,31 @@ class Index extends Component
|
||||
|
||||
public function delete()
|
||||
{
|
||||
$currentTeam = currentTeam();
|
||||
$this->authorize('delete', $currentTeam);
|
||||
$currentTeam->delete();
|
||||
try {
|
||||
$currentTeam = currentTeam();
|
||||
$this->authorize('delete', $currentTeam);
|
||||
$currentTeam->members->each(function ($user) use ($currentTeam) {
|
||||
if ($user->id === Auth::id()) {
|
||||
return;
|
||||
}
|
||||
$user->teams()->detach($currentTeam);
|
||||
$session = DB::table('sessions')->where('user_id', $user->id)->first();
|
||||
if ($session) {
|
||||
DB::table('sessions')->where('id', $session->id)->delete();
|
||||
}
|
||||
});
|
||||
|
||||
$currentTeam->members->each(function ($user) use ($currentTeam) {
|
||||
if ($user->id === Auth::id()) {
|
||||
return;
|
||||
}
|
||||
$user->teams()->detach($currentTeam);
|
||||
$session = DB::table('sessions')->where('user_id', $user->id)->first();
|
||||
if ($session) {
|
||||
DB::table('sessions')->where('id', $session->id)->delete();
|
||||
}
|
||||
});
|
||||
// Clear stale cache before deleting so refreshSession doesn't resolve the deleted team
|
||||
Cache::forget('user:'.Auth::id().':team:'.$currentTeam->id);
|
||||
$currentTeam->delete();
|
||||
|
||||
refreshSession();
|
||||
// Switch to the user's next available team
|
||||
$newTeam = Auth::user()->teams()->first();
|
||||
refreshSession($newTeam);
|
||||
|
||||
return redirect()->route('team.index');
|
||||
return redirect()->route('team.index');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class InviteLink extends Component
|
||||
{
|
||||
@@ -61,7 +60,7 @@ class InviteLink extends Component
|
||||
if ($member_emails->contains($this->email)) {
|
||||
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.');
|
||||
}
|
||||
$uuid = (string) new Cuid2(32);
|
||||
$uuid = new_public_id(32);
|
||||
$link = url('/').config('constants.invitation.link.base_url').$uuid;
|
||||
$user = User::whereEmail($this->email)->first();
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@ class Upgrade extends Component
|
||||
public function upgrade()
|
||||
{
|
||||
try {
|
||||
if (! isInstanceAdmin()) {
|
||||
abort(403);
|
||||
}
|
||||
if ($this->updateInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user