From 5848b07fc24499c611584b58328965b5e856c2bc Mon Sep 17 00:00:00 2001
From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com>
Date: Tue, 29 Jul 2025 21:42:47 -0400
Subject: [PATCH 001/151] feat(api): add endpoint to retrieve database logs by
UUID
---
.../Controllers/Api/DatabasesController.php | 101 ++++++++++++++++++
bootstrap/helpers/docker.php | 13 +++
routes/api.php | 1 +
3 files changed, 115 insertions(+)
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index 504665f6a..fc2b7b6d0 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -1535,6 +1535,107 @@ public function create_database(Request $request, NewDatabaseTypes $type)
return response()->json(['message' => 'Invalid database type requested.'], 400);
}
+ #[OA\Get(
+ summary: 'Get database logs.',
+ description: 'Get database logs by UUID.',
+ path: '/databases/{uuid}/logs',
+ operationId: 'get-database-logs-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Databases'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the database.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ new OA\Parameter(
+ name: 'lines',
+ in: 'query',
+ description: 'Number of lines to show from the end of the logs.',
+ required: false,
+ schema: new OA\Schema(
+ type: 'integer',
+ format: 'int32',
+ default: 100,
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get database logs by UUID.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'logs' => ['type' => 'string'],
+ ]
+ )
+ ),
+ ]
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ new OA\Response(
+ response: 404,
+ ref: '#/components/responses/404',
+ ),
+ ]
+ )]
+ public function logs_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $database = queryDatabaseByUuidWithinTeam($uuid, $teamId);
+ if (! $database) {
+ return response()->json(['message' => 'Database not found.'], 404);
+ }
+
+ $containers = getCurrentDatabaseContainerStatus($database->destination->server, $database->id);
+
+ if ($containers->count() == 0) {
+ return response()->json([
+ 'message' => 'Database is not running.',
+ ], 400);
+ }
+
+ $container = $containers->first();
+
+ $status = getContainerStatus($database->destination->server, $container['Names']);
+ if ($status !== 'running') {
+ return response()->json([
+ 'message' => 'Database is not running.',
+ ], 400);
+ }
+
+ $lines = $request->query->get('lines', 100) ?: 100;
+ $logs = getContainerLogs($database->destination->server, $container['ID'], $lines);
+
+ return response()->json([
+ 'logs' => $logs,
+ ]);
+ }
#[OA\Delete(
summary: 'Delete',
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 944c51e3c..cac8ffb6c 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -53,6 +53,19 @@ function getCurrentServiceContainerStatus(Server $server, int $id): Collection
return $containers;
}
+function getCurrentDatabaseContainerStatus(Server $server, int $id): Collection
+{
+ $containers = collect([]);
+ if (! $server->isSwarm()) {
+ $containers = instant_remote_process(["docker ps -a --filter='label=coolify.databaseId={$id}' --format '{{json .}}' "], $server);
+ $containers = format_docker_command_output_to_json($containers);
+
+ return $containers->filter();
+ }
+
+ return $containers;
+}
+
function format_docker_command_output_to_json($rawOutput): Collection
{
$outputLines = explode(PHP_EOL, $rawOutput);
diff --git a/routes/api.php b/routes/api.php
index d63e3ee0e..958c88fda 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -112,6 +112,7 @@
Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']);
Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']);
+ Route::get('/databases/{uuid}/logs', [DatabasesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:write']);
Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:write']);
From bc9bfaefc78cb0436795f59ab3a6f98c5d1e7039 Mon Sep 17 00:00:00 2001
From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com>
Date: Tue, 29 Jul 2025 22:40:02 -0400
Subject: [PATCH 002/151] feat(api): add endpoints to retrieve service logs by
UUID for each container
---
.../Controllers/Api/ServicesController.php | 115 ++++++++++++++++++
routes/api.php | 1 +
2 files changed, 116 insertions(+)
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 542be83de..61ff80a60 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -448,6 +448,121 @@ public function service_by_uuid(Request $request)
return response()->json($this->removeSensitiveData($service));
}
+ #[OA\Get(
+ summary: 'Get service logs.',
+ description: 'Get service logs by UUID.',
+ path: '/services/{uuid}/containers/{container_id}/logs',
+ operationId: 'get-service-logs-by-uuid',
+ security: [
+ ['bearerAuth' => []],
+ ],
+ tags: ['Services'],
+ parameters: [
+ new OA\Parameter(
+ name: 'uuid',
+ in: 'path',
+ description: 'UUID of the service.',
+ required: true,
+ schema: new OA\Schema(
+ type: 'string',
+ format: 'uuid',
+ )
+ ),
+ new OA\Parameter(
+ name: 'container_id',
+ in: 'path',
+ description: 'Container ID.',
+ required: true,
+ schema: new OA\Schema(type: 'string'),
+ ),
+ new OA\Parameter(
+ name: 'lines',
+ in: 'query',
+ description: 'Number of lines to show from the end of the logs.',
+ required: false,
+ schema: new OA\Schema(
+ type: 'integer',
+ format: 'int32',
+ default: 100,
+ )
+ ),
+ ],
+ responses: [
+ new OA\Response(
+ response: 200,
+ description: 'Get service logs by UUID.',
+ content: [
+ new OA\MediaType(
+ mediaType: 'application/json',
+ schema: new OA\Schema(
+ type: 'object',
+ properties: [
+ 'logs' => ['type' => 'string'],
+ ]
+ )
+ ),
+ ]
+ ),
+ new OA\Response(
+ response: 401,
+ ref: '#/components/responses/401',
+ ),
+ new OA\Response(
+ response: 400,
+ ref: '#/components/responses/400',
+ ),
+ new OA\Response(
+ response: 404,
+ ref: '#/components/responses/404',
+ ),
+ ]
+ )]
+ public function logs_by_uuid(Request $request)
+ {
+ $teamId = getTeamIdFromToken();
+ if (is_null($teamId)) {
+ return invalidTokenResponse();
+ }
+ $uuid = $request->route('uuid');
+ if (! $uuid) {
+ return response()->json(['message' => 'UUID is required.'], 400);
+ }
+ $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
+ if (! $service) {
+ return response()->json(['message' => 'Service not found.'], 404);
+ }
+
+ $containers = getCurrentServiceContainerStatus($service->destination->server, $service->id);
+
+ if ($containers->count() == 0) {
+ return response()->json([
+ 'message' => 'Service is not running.',
+ ], 400);
+ }
+
+ $container = $containers->first(function ($container) use ($request) {
+ return $container['ID'] === $request->container_id;
+ });
+
+ if (! $container) {
+ return response()->json(['message' => 'Container not found.'], 404);
+ }
+
+ $status = getContainerStatus($service->destination->server, $container['Names']);
+ if ($status !== 'running') {
+ return response()->json([
+ 'message' => 'Container is not running.',
+ ], 400);
+ }
+
+ $lines = $request->query->get('lines', 100) ?: 100;
+ $logs = getContainerLogs($service->destination->server, $container['ID'], $lines);
+
+ return response()->json([
+ 'logs' => $logs,
+ ]);
+ }
+
#[OA\Delete(
summary: 'Delete',
description: 'Delete service by UUID.',
diff --git a/routes/api.php b/routes/api.php
index 958c88fda..c790627b8 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -130,6 +130,7 @@
Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware(['api.ability:write']);
Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
+ Route::get('/services/{uuid}/containers/{container_id}/logs', [ServicesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:write']);
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:write']);
From 28e20473da9abd641dfc6dd6306f01808c36beac Mon Sep 17 00:00:00 2001
From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com>
Date: Wed, 30 Jul 2025 11:29:27 -0400
Subject: [PATCH 003/151] refactor(api): update service logs endpoint to use
sub service name
---
.../Controllers/Api/ServicesController.php | 18 ++++++++++--------
bootstrap/helpers/docker.php | 13 +++++++++++++
routes/api.php | 2 +-
3 files changed, 24 insertions(+), 9 deletions(-)
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 61ff80a60..299af54e0 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -451,7 +451,7 @@ public function service_by_uuid(Request $request)
#[OA\Get(
summary: 'Get service logs.',
description: 'Get service logs by UUID.',
- path: '/services/{uuid}/containers/{container_id}/logs',
+ path: '/services/{uuid}/logs',
operationId: 'get-service-logs-by-uuid',
security: [
['bearerAuth' => []],
@@ -469,9 +469,9 @@ public function service_by_uuid(Request $request)
)
),
new OA\Parameter(
- name: 'container_id',
- in: 'path',
- description: 'Container ID.',
+ name: 'sub_service_name',
+ in: 'query',
+ description: 'Sub service name.',
required: true,
schema: new OA\Schema(type: 'string'),
),
@@ -527,12 +527,16 @@ public function logs_by_uuid(Request $request)
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
+ $subServiceName = $request->query->get('sub_service_name');
+ if (! $subServiceName) {
+ return response()->json(['message' => 'Sub service name is required.'], 400);
+ }
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
- $containers = getCurrentServiceContainerStatus($service->destination->server, $service->id);
+ $containers = getCurrentServiceSubContainerStatus($service->destination->server, $service->id, $subServiceName);
if ($containers->count() == 0) {
return response()->json([
@@ -540,9 +544,7 @@ public function logs_by_uuid(Request $request)
], 400);
}
- $container = $containers->first(function ($container) use ($request) {
- return $container['ID'] === $request->container_id;
- });
+ $container = $containers->first();
if (! $container) {
return response()->json(['message' => 'Container not found.'], 404);
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index cac8ffb6c..26704060c 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -66,6 +66,19 @@ function getCurrentDatabaseContainerStatus(Server $server, int $id): Collection
return $containers;
}
+function getCurrentServiceSubContainerStatus(Server $server, int $id, string $subName): Collection
+{
+ $containers = collect([]);
+ if (! $server->isSwarm()) {
+ $containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --filter='label=coolify.service.subName={$subName}' --format '{{json .}}' "], $server);
+ $containers = format_docker_command_output_to_json($containers);
+
+ return $containers->filter();
+ }
+
+ return $containers;
+}
+
function format_docker_command_output_to_json($rawOutput): Collection
{
$outputLines = explode(PHP_EOL, $rawOutput);
diff --git a/routes/api.php b/routes/api.php
index c790627b8..8fec3e3a5 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -130,7 +130,7 @@
Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware(['api.ability:write']);
Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
- Route::get('/services/{uuid}/containers/{container_id}/logs', [ServicesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
+ Route::get('/services/{uuid}/logs', [ServicesController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:write']);
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:write']);
From c239b8bbba07c0f9b6f9f5507581f2d07853a11a Mon Sep 17 00:00:00 2001
From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com>
Date: Wed, 30 Jul 2025 13:41:17 -0400
Subject: [PATCH 004/151] refactor(api): modify service sub container retrieval
filter to use coolify.name
---
app/Http/Controllers/Api/ServicesController.php | 10 ++--------
bootstrap/helpers/docker.php | 4 ++--
2 files changed, 4 insertions(+), 10 deletions(-)
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 299af54e0..edec32db5 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -536,14 +536,8 @@ public function logs_by_uuid(Request $request)
return response()->json(['message' => 'Service not found.'], 404);
}
- $containers = getCurrentServiceSubContainerStatus($service->destination->server, $service->id, $subServiceName);
-
- if ($containers->count() == 0) {
- return response()->json([
- 'message' => 'Service is not running.',
- ], 400);
- }
-
+ $name = "{$subServiceName}-{$service->uuid}";
+ $containers = getCurrentServiceSubContainerStatus($service->destination->server, $service->id, $name);
$container = $containers->first();
if (! $container) {
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 26704060c..87dc47336 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -66,11 +66,11 @@ function getCurrentDatabaseContainerStatus(Server $server, int $id): Collection
return $containers;
}
-function getCurrentServiceSubContainerStatus(Server $server, int $id, string $subName): Collection
+function getCurrentServiceSubContainerStatus(Server $server, int $id, string $name): Collection
{
$containers = collect([]);
if (! $server->isSwarm()) {
- $containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --filter='label=coolify.service.subName={$subName}' --format '{{json .}}' "], $server);
+ $containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --filter='label=coolify.name={$name}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers);
return $containers->filter();
From 0eb2ea86e85693199e89606afeb960acbba2b5cf Mon Sep 17 00:00:00 2001
From: Yanluis Fermin <32645451+Jacxk@users.noreply.github.com>
Date: Wed, 30 Jul 2025 21:32:11 -0400
Subject: [PATCH 005/151] feat(api): add 'show_timestamps' parameter to logs
endpoints
---
.../Controllers/Api/ApplicationsController.php | 12 ++++++++++--
app/Http/Controllers/Api/DatabasesController.php | 12 ++++++++++--
app/Http/Controllers/Api/ServicesController.php | 12 ++++++++++--
bootstrap/helpers/docker.php | 16 ++++++++--------
4 files changed, 38 insertions(+), 14 deletions(-)
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index 0860c7133..f2015de67 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -1553,6 +1553,13 @@ public function application_by_uuid(Request $request)
default: 100,
)
),
+ new OA\Parameter(
+ name: 'show_timestamps',
+ in: 'query',
+ description: 'Show timestamps in the logs.',
+ required: false,
+ schema: new OA\Schema(type: 'boolean', default: false),
+ ),
],
responses: [
new OA\Response(
@@ -1616,8 +1623,9 @@ public function logs_by_uuid(Request $request)
], 400);
}
- $lines = $request->query->get('lines', 100) ?: 100;
- $logs = getContainerLogs($application->destination->server, $container['ID'], $lines);
+ $lines = $request->query->get('lines', 100);
+ $showTimestamps = $request->query->get('show_timestamps', false);
+ $logs = getContainerLogs($application->destination->server, $container['ID'], $lines, $showTimestamps);
return response()->json([
'logs' => $logs,
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index fc2b7b6d0..dd4165c90 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -1566,6 +1566,13 @@ public function create_database(Request $request, NewDatabaseTypes $type)
default: 100,
)
),
+ new OA\Parameter(
+ name: 'show_timestamps',
+ in: 'query',
+ description: 'Show timestamps in the logs.',
+ required: false,
+ schema: new OA\Schema(type: 'boolean', default: false),
+ ),
],
responses: [
new OA\Response(
@@ -1629,8 +1636,9 @@ public function logs_by_uuid(Request $request)
], 400);
}
- $lines = $request->query->get('lines', 100) ?: 100;
- $logs = getContainerLogs($database->destination->server, $container['ID'], $lines);
+ $lines = $request->query->get('lines', 100);
+ $showTimestamps = $request->query->get('show_timestamps', false);
+ $logs = getContainerLogs($database->destination->server, $container['ID'], $lines, $showTimestamps);
return response()->json([
'logs' => $logs,
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index edec32db5..1fc4bb765 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -486,6 +486,13 @@ public function service_by_uuid(Request $request)
default: 100,
)
),
+ new OA\Parameter(
+ name: 'show_timestamps',
+ in: 'query',
+ description: 'Show timestamps in the logs.',
+ required: false,
+ schema: new OA\Schema(type: 'boolean', default: false),
+ ),
],
responses: [
new OA\Response(
@@ -551,8 +558,9 @@ public function logs_by_uuid(Request $request)
], 400);
}
- $lines = $request->query->get('lines', 100) ?: 100;
- $logs = getContainerLogs($service->destination->server, $container['ID'], $lines);
+ $lines = $request->query->get('lines', 100);
+ $showTimestamps = $request->query->get('show_timestamps', false);
+ $logs = getContainerLogs($service->destination->server, $container['ID'], $lines, $showTimestamps);
return response()->json([
'logs' => $logs,
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 87dc47336..771937ce0 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -1115,18 +1115,18 @@ function validateComposeFile(string $compose, int $server_id): string|Throwable
}
}
-function getContainerLogs(Server $server, string $container_id, int $lines = 100): string
+function getContainerLogs(Server $server, string $container_id, int $lines = 100, bool $showTimestamps = false): string
{
+ $command = "docker logs -n {$lines} {$container_id}";
if ($server->isSwarm()) {
- $output = instant_remote_process([
- "docker service logs -n {$lines} {$container_id}",
- ], $server);
- } else {
- $output = instant_remote_process([
- "docker logs -n {$lines} {$container_id}",
- ], $server);
+ $command = "docker service logs -n {$lines} {$container_id}";
}
+ if ($showTimestamps) {
+ $command .= ' --timestamps';
+ }
+
+ $output = instant_remote_process([$command], $server);
$output .= removeAnsiColors($output);
return $output;
From 555b46a7d84d22a0f9ff0f30f22d3797ff8b3171 Mon Sep 17 00:00:00 2001
From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
Date: Tue, 24 Feb 2026 00:45:33 +0530
Subject: [PATCH 006/151] chore(repo): improve contributor guidelines
---
CONTRIBUTING.md | 463 +++++++++++++++++++++---------------------------
1 file changed, 205 insertions(+), 258 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9aec08420..af8c7503c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,298 +1,245 @@
# Contributing to Coolify
+We’re happy that you’re interested in contributing to Coolify!
-> "First, thanks for considering contributing to my project. It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
+There are many ways to help:
+- Answer questions in GitHub Discussions or Discord
+- Report reproducible bugs
+- Submit pull requests to fix issues
+- Add new one-click services
+- Improve documentation
-You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel.
+Coolify is a PaaS used by 400,000+ people worldwide and maintained by two active maintainers. Contributions are welcome — but **alignment matters more than quantity**.
-To understand the tech stack, please refer to the [Tech Stack](TECH_STACK.md) document.
-
-## Table of Contents
-
-1. [Setup Development Environment](#1-setup-development-environment)
-2. [Verify Installation](#2-verify-installation-optional)
-3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
-4. [Set up Environment Variables](#4-set-up-environment-variables)
-5. [Start Coolify](#5-start-coolify)
-6. [Start Development](#6-start-development)
-7. [Create a Pull Request](#7-create-a-pull-request)
-8. [Development Notes](#development-notes)
-9. [Resetting Development Environment](#resetting-development-environment)
-10. [Additional Contribution Guidelines](#additional-contribution-guidelines)
-
-## 1. Setup Development Environment
-
-Follow the steps below for your operating system:
-
-Windows
-
-1. Install `docker-ce`, Docker Desktop (or similar):
- - Docker CE (recommended):
- - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify)
- - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify)
- - Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide
- - Install Docker Desktop (easier):
- - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify)
- - Ensure WSL2 backend is enabled in Docker Desktop settings
-
-2. Install Spin:
- - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2?ref=coolify)
-
-MacOS
-
-1. Install Orbstack, Docker Desktop (or similar):
- - Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
- - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify)
- - Docker Desktop:
- - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify)
-
-2. Install Spin:
- - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin?ref=coolify)
-
-Linux
-
-1. Install Docker Engine, Docker Desktop (or similar):
- - Docker Engine (recommended, as there is no VM overhead):
- - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution
- - Docker Desktop:
- - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify)
-
-2. Install Spin:
- - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions?ref=coolify)
-
-Windows
+
+1. Install `docker-ce`, Docker Desktop (or similar):
+ - Docker CE (recommended):
+ - Install Windows Subsystem for Linux v2 (WSL2) by following this guide: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install?ref=coolify)
+ - After installing WSL2, install Docker CE for your Linux distribution by following this guide: [Install Docker Engine](https://docs.docker.com/engine/install/?ref=coolify)
+ - Make sure to choose the appropriate Linux distribution (e.g., Ubuntu) when following the Docker installation guide
+ - Install Docker Desktop (easier):
+ - Download and install [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/?ref=coolify)
+ - Ensure WSL2 backend is enabled in Docker Desktop settings
+
+2. Install Spin:
+ - Follow the instructions to install Spin on Windows from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-windows#download-and-install-spin-into-wsl2?ref=coolify)
+
+MacOS
+
+1. Install Orbstack, Docker Desktop (or similar):
+ - Orbstack (recommended, as it is a faster and lighter alternative to Docker Desktop):
+ - Download and install [Orbstack](https://docs.orbstack.dev/quick-start#installation?ref=coolify)
+ - Docker Desktop:
+ - Download and install [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/?ref=coolify)
+
+2. Install Spin:
+ - Follow the instructions to install Spin on MacOS from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-macos/#download-and-install-spin?ref=coolify)
+
+Linux
+
+1. Install Docker Engine, Docker Desktop (or similar):
+ - Docker Engine (recommended, as there is no VM overhead):
+ - Follow the official [Docker Engine installation guide](https://docs.docker.com/engine/install/?ref=coolify) for your Linux distribution
+ - Docker Desktop:
+ - If you want a GUI, you can use [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/?ref=coolify)
+
+2. Install Spin:
+ - Follow the instructions to install Spin on Linux from the [Spin documentation](https://serversideup.net/open-source/spin/docs/installation/install-linux#configure-docker-permissions?ref=coolify)
+
+
More information here: documentation');
+
+ 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.
More information here: documentation');
+
+ 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.
More information here: documentation');
-
- 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.
More information here: documentation');
-
- 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()
@@ -127,45 +135,53 @@ protected function setDeploymentUuid()
public function stop()
{
- $this->authorize('deploy', $this->application);
+ try {
+ $this->authorize('deploy', $this->application);
- $this->dispatch('info', 'Gracefully stopping application.
It could take a while depending on the application.');
- StopApplication::dispatch($this->application, false, $this->docker_cleanup);
+ $this->dispatch('info', 'Gracefully stopping application.
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.
More information here: documentation');
+ 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.
More information here: documentation');
- 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()
diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php
index 41f352c14..50acf76b2 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -215,24 +215,31 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null)
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);
- $this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true);
+ $this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null)
{
- $this->authorize('deploy', $this->application);
+ try {
+ $this->authorize('deploy', $this->application);
- $this->add($pull_request_id, $pull_request_html_url);
- $this->deploy($pull_request_id, $pull_request_html_url);
+ $this->add($pull_request_id, $pull_request_html_url);
+ $this->deploy($pull_request_id, $pull_request_html_url);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false)
{
- $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)) {
@@ -291,9 +298,8 @@ private function stopContainers(array $containers, $server)
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()) {
diff --git a/app/Livewire/Project/Database/BackupNow.php b/app/Livewire/Project/Database/BackupNow.php
index decd59a4c..e4ed2a366 100644
--- a/app/Livewire/Project/Database/BackupNow.php
+++ b/app/Livewire/Project/Database/BackupNow.php
@@ -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);
+ }
}
}
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index 8d3d8e294..ef2163f14 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -86,18 +86,26 @@ public function stop()
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->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->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function render()
diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php
index 1cf5e53f6..06473c56a 100644
--- a/app/Livewire/Project/Database/ScheduledBackups.php
+++ b/app/Livewire/Project/Database/ScheduledBackups.php
@@ -56,22 +56,30 @@ public function setSelectedBackup($backupId, $force = false)
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
diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php
index aa6e95975..28027ce5a 100644
--- a/app/Livewire/Project/DeleteEnvironment.php
+++ b/app/Livewire/Project/DeleteEnvironment.php
@@ -30,18 +30,22 @@ public function mount()
public function delete()
{
- $this->validate([
- 'environment_id' => 'required|int',
- ]);
- $environment = Environment::findOrFail($this->environment_id);
- $this->authorize('delete', $environment);
+ try {
+ $this->validate([
+ 'environment_id' => 'required|int',
+ ]);
+ $environment = Environment::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', "Environment {$environment->name} has defined resources, please delete them first.");
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
-
- return $this->dispatch('error', "Environment {$environment->name} has defined resources, please delete them first.");
}
}
diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php
index a018046fd..0b99f57a4 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -26,18 +26,22 @@ public function mount()
public function delete()
{
- $this->validate([
- 'project_id' => 'required|int',
- ]);
- $project = Project::findOrFail($this->project_id);
- $this->authorize('delete', $project);
+ try {
+ $this->validate([
+ 'project_id' => 'required|int',
+ ]);
+ $project = Project::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', "Project {$project->name} has resources defined, please delete them first.");
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
-
- return $this->dispatch('error', "Project {$project->name} has resources defined, please delete them first.");
}
}
diff --git a/app/Livewire/Project/Service/Heading.php b/app/Livewire/Project/Service/Heading.php
index c8a08d8f9..adc2b151b 100644
--- a/app/Livewire/Project/Service/Heading.php
+++ b/app/Livewire/Project/Service/Heading.php
@@ -7,12 +7,15 @@
use App\Actions\Service\StopService;
use App\Enums\ProcessStatus;
use App\Models\Service;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
class Heading extends Component
{
+ use AuthorizesRequests;
+
public Service $service;
public array $parameters;
@@ -99,13 +102,19 @@ public function checkDeployments()
public function start()
{
- $activity = StartService::run($this->service, pullLatestImages: true);
- $this->dispatch('activityMonitor', $activity->id);
+ try {
+ $this->authorize('deploy', $this->service);
+ $activity = StartService::run($this->service, pullLatestImages: true);
+ $this->dispatch('activityMonitor', $activity->id);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function forceDeploy()
{
try {
+ $this->authorize('deploy', $this->service);
$activities = Activity::where('properties->type_uuid', $this->service->uuid)
->where(function ($q) {
$q->where('properties->status', ProcessStatus::IN_PROGRESS->value)
@@ -117,42 +126,53 @@ public function forceDeploy()
}
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
$this->dispatch('activityMonitor', $activity->id);
- } catch (\Exception $e) {
- $this->dispatch('error', $e->getMessage());
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
public function stop()
{
try {
+ $this->authorize('stop', $this->service);
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->checkDeployments();
- if ($this->isDeploymentProgress) {
- $this->dispatch('error', 'There is a deployment in progress.');
+ try {
+ $this->authorize('deploy', $this->service);
+ $this->checkDeployments();
+ if ($this->isDeploymentProgress) {
+ $this->dispatch('error', 'There is a deployment in progress.');
- return;
+ return;
+ }
+ $activity = StartService::run($this->service, stopBeforeStart: true);
+ $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->checkDeployments();
- if ($this->isDeploymentProgress) {
- $this->dispatch('error', 'There is a deployment in progress.');
+ try {
+ $this->authorize('deploy', $this->service);
+ $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->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);
}
public function render()
diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php
index 7ab81b7d1..1eb1dc580 100644
--- a/app/Livewire/Project/Shared/Destination.php
+++ b/app/Livewire/Project/Shared/Destination.php
@@ -7,12 +7,15 @@
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 +62,7 @@ public function loadData()
public function stop($serverId)
{
try {
+ $this->authorize('deploy', $this->resource);
$server = Server::ownedByCurrentTeam()->findOrFail($serverId);
StopApplicationOneServer::run($this->resource, $server);
$this->refreshServers();
@@ -70,6 +74,7 @@ public function stop($serverId)
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.
More information here: documentation');
@@ -110,15 +115,20 @@ public function redeploy(int $network_id, int $server_id)
public function promote(int $network_id, int $server_id)
{
- $main_destination = $this->resource->destination;
- $this->resource->update([
- 'destination_id' => $network_id,
- 'destination_type' => StandaloneDocker::class,
- ]);
- $this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
- $this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]);
- $this->refreshServers();
- $this->resource->refresh();
+ try {
+ $this->authorize('update', $this->resource);
+ $main_destination = $this->resource->destination;
+ $this->resource->update([
+ 'destination_id' => $network_id,
+ 'destination_type' => StandaloneDocker::class,
+ ]);
+ $this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
+ $this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]);
+ $this->refreshServers();
+ $this->resource->refresh();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function refreshServers()
@@ -130,13 +140,19 @@ public function refreshServers()
public function addServer(int $network_id, int $server_id)
{
- $this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]);
- $this->dispatch('refresh');
+ try {
+ $this->authorize('update', $this->resource);
+ $this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]);
+ $this->dispatch('refresh');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function removeServer(int $network_id, int $server_id, $password)
{
try {
+ $this->authorize('update', $this->resource);
if (! verifyPasswordConfirmation($password, $this)) {
return;
}
diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php
index df2de5142..0a47034fd 100644
--- a/app/Livewire/Project/Shared/HealthChecks.php
+++ b/app/Livewire/Project/Shared/HealthChecks.php
@@ -70,8 +70,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
diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php
index e769e4bcb..25545c4b0 100644
--- a/app/Livewire/Project/Shared/ResourceOperations.php
+++ b/app/Livewire/Project/Shared/ResourceOperations.php
@@ -47,224 +47,87 @@ public function toggleVolumeCloning(bool $value)
public function cloneTo($destination_id)
{
- $this->authorize('update', $this->resource);
+ try {
+ $this->authorize('update', $this->resource);
- $teamScope = fn ($q) => $q->where('team_id', currentTeam()->id);
- $new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id);
- if (! $new_destination) {
- $new_destination = SwarmDocker::whereHas('server', $teamScope)->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() === \App\Models\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() === \App\Models\StandalonePostgresql::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneMysql::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
- $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
- ) {
+ $teamScope = fn ($q) => $q->where('team_id', currentTeam()->id);
+ $new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id);
+ if (! $new_destination) {
+ $new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id);
+ }
+ if (! $new_destination) {
+ return $this->addError('destination_id', 'Destination not found.');
+ }
$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();
+ $server = $new_destination->server;
- $tags = $this->resource->tags;
- foreach ($tags as $tag) {
- $new_resource->tags()->attach($tag->id);
- }
+ if ($this->resource->getMorphClass() === \App\Models\Application::class) {
+ $new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData);
- $new_resource->persistentStorages()->delete();
- $persistentVolumes = $this->resource->persistentStorages()->get();
- foreach ($persistentVolumes as $volume) {
- $originalName = $volume->name;
- $newName = '';
+ $route = route('project.application.configuration', [
+ 'project_uuid' => $this->projectUuid,
+ 'environment_uuid' => $this->environmentUuid,
+ 'application_uuid' => $new_resource->uuid,
+ ]).'#resource-operations';
- 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',
- ])->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());
- }
- }
- }
-
- $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();
- }
-
- $scheduledBackups = $this->resource->scheduledBackups()->get();
- foreach ($scheduledBackups as $backup) {
+ return redirect()->to($route);
+ } elseif (
+ $this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneMysql::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class ||
+ $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class
+ ) {
$uuid = (string) new Cuid2;
- $newBackup = $backup->replicate([
+ $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->update([
+ 'name' => $this->resource->name.'-clone-'.$uuid,
'status' => 'exited',
+ '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([
@@ -273,79 +136,220 @@ public function cloneTo($destination_id)
'updated_at',
])->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->update([
- 'status' => 'exited',
- ]);
-
- $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',
])->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 = (string) new Cuid2;
+ $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 = (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->update([
+ 'status' => 'exited',
+ ]);
+
+ $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',
+ ])->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->update([
+ 'status' => 'exited',
+ ]);
+
+ $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',
+ ])->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);
}
}
diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php
index a263acedf..d22d5d9fc 100644
--- a/app/Livewire/Security/ApiTokens.php
+++ b/app/Livewire/Security/ApiTokens.php
@@ -23,6 +23,8 @@ class ApiTokens extends Component
public bool $canUseWritePermissions = false;
+ public bool $canUseDeployPermissions = false;
+
public function render()
{
return view('livewire.security.api-tokens');
@@ -33,6 +35,7 @@ public function mount()
$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->getTokens();
}
@@ -60,6 +63,13 @@ public function updatedPermissions($permissionToUpdate)
return;
}
+ if ($permissionToUpdate == 'deploy' && ! $this->canUseDeployPermissions) {
+ $this->dispatch('error', 'You do not have permission to use deploy permissions.');
+ $this->permissions = array_diff($this->permissions, ['deploy']);
+
+ return;
+ }
+
if ($permissionToUpdate == 'root') {
$this->permissions = ['root'];
} elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions)) {
@@ -88,6 +98,10 @@ public function addNewToken()
throw new \Exception('You do not have permission to create tokens with write permissions.');
}
+ if (in_array('deploy', $this->permissions) && ! $this->canUseDeployPermissions) {
+ throw new \Exception('You do not have permission to create tokens with deploy permissions.');
+ }
+
$this->validate([
'description' => 'required|min:3|max:255',
]);
diff --git a/app/Livewire/Security/CloudInitScriptForm.php b/app/Livewire/Security/CloudInitScriptForm.php
index 33beff334..5e4ca9853 100644
--- a/app/Livewire/Security/CloudInitScriptForm.php
+++ b/app/Livewire/Security/CloudInitScriptForm.php
@@ -20,15 +20,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);
}
}
diff --git a/app/Livewire/Security/CloudProviderTokenForm.php b/app/Livewire/Security/CloudProviderTokenForm.php
index 7affb1531..ec4513ff3 100644
--- a/app/Livewire/Security/CloudProviderTokenForm.php
+++ b/app/Livewire/Security/CloudProviderTokenForm.php
@@ -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
diff --git a/app/Livewire/Security/CloudProviderTokens.php b/app/Livewire/Security/CloudProviderTokens.php
index cfef30772..b7f389534 100644
--- a/app/Livewire/Security/CloudProviderTokens.php
+++ b/app/Livewire/Security/CloudProviderTokens.php
@@ -14,8 +14,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()
diff --git a/app/Livewire/Security/PrivateKey/Index.php b/app/Livewire/Security/PrivateKey/Index.php
index 1eb66ae3e..0362b65fa 100644
--- a/app/Livewire/Security/PrivateKey/Index.php
+++ b/app/Livewire/Security/PrivateKey/Index.php
@@ -21,8 +21,12 @@ public function render()
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);
+ }
}
}
diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php
index f1ffa60f2..e8df99d65 100644
--- a/app/Livewire/Server/New/ByHetzner.php
+++ b/app/Livewire/Server/New/ByHetzner.php
@@ -78,14 +78,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);
}
}
diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php
index 1a14baf89..6c163a112 100644
--- a/app/Livewire/Server/Proxy.php
+++ b/app/Livewire/Server/Proxy.php
@@ -96,11 +96,15 @@ public function getConfigurationFilePathProperty(): string
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)
diff --git a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
index c67591cf5..f7db1257c 100644
--- a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
+++ b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
@@ -22,34 +22,38 @@ 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 validateShellSafePath() correctly
- // rejects pipe characters as dangerous shell metacharacters
- $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 validateShellSafePath() correctly
+ // rejects pipe characters as dangerous shell metacharacters
+ $file = str_replace('|', '.', $fileName);
- // Validate filename to prevent command injection
- validateShellSafePath($file, 'proxy configuration filename');
+ // Validate filename to prevent command injection
+ validateShellSafePath($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()
diff --git a/app/Livewire/Server/Resources.php b/app/Livewire/Server/Resources.php
index a21b0372b..31e57b301 100644
--- a/app/Livewire/Server/Resources.php
+++ b/app/Livewire/Server/Resources.php
@@ -29,23 +29,38 @@ public function getListeners()
public function startUnmanaged($id)
{
- $this->server->startUnmanaged($id);
- $this->dispatch('success', 'Container started.');
- $this->loadUnmanagedContainers();
+ try {
+ $this->authorize('update', $this->server);
+ $this->server->startUnmanaged($id);
+ $this->dispatch('success', 'Container started.');
+ $this->loadUnmanagedContainers();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function restartUnmanaged($id)
{
- $this->server->restartUnmanaged($id);
- $this->dispatch('success', 'Container restarted.');
- $this->loadUnmanagedContainers();
+ try {
+ $this->authorize('update', $this->server);
+ $this->server->restartUnmanaged($id);
+ $this->dispatch('success', 'Container restarted.');
+ $this->loadUnmanagedContainers();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function stopUnmanaged($id)
{
- $this->server->stopUnmanaged($id);
- $this->dispatch('success', 'Container stopped.');
- $this->loadUnmanagedContainers();
+ try {
+ $this->authorize('update', $this->server);
+ $this->server->stopUnmanaged($id);
+ $this->dispatch('success', 'Container stopped.');
+ $this->loadUnmanagedContainers();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function refreshStatus()
diff --git a/app/Livewire/Server/Security/Patches.php b/app/Livewire/Server/Security/Patches.php
index b4d151424..087836da3 100644
--- a/app/Livewire/Server/Security/Patches.php
+++ b/app/Livewire/Server/Security/Patches.php
@@ -41,7 +41,11 @@ public function mount()
{
$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 @@ public function checkForUpdates()
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 @@ public function updateAllPackages()
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);
}
}
diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php
index 83c63a81c..38053386e 100644
--- a/app/Livewire/Server/Show.php
+++ b/app/Livewire/Server/Show.php
@@ -286,18 +286,22 @@ public function validateServer($install = true)
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.
Check this documentation for further help.
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.
Check this documentation for further help.
Error: '.$error);
- return;
+ return;
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
}
}
diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php
index 1a5bd381b..9b0f02573 100644
--- a/app/Livewire/Server/ValidateAndInstall.php
+++ b/app/Livewire/Server/ValidateAndInstall.php
@@ -72,31 +72,39 @@ public function startValidatingAfterAsking()
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) {
- $this->error = 'Server is not reachable. Please validate your configuration and connection.
Check this documentation for further help.