diff --git a/CLAUDE.md b/CLAUDE.md
index 38f9a2542..39b614885 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -38,6 +38,43 @@ npm run dev # vite dev server
npm run build # production build
```
+## Browser Tests (Pest Browser Plugin)
+
+Uses `pestphp/pest-plugin-browser` with Laravel Dusk 8. New browser tests go in `tests/v4/Browser/`.
+
+```bash
+# Run all browser tests
+php artisan test --compact tests/v4/Browser/
+
+# Run a specific browser test file
+php artisan test --compact tests/v4/Browser/LoginTest.php
+
+# Run a specific test by name
+php artisan test --compact --filter='can login with valid credentials'
+```
+
+### Writing Browser Tests
+
+- Place new tests in `tests/v4/Browser/` — legacy Dusk tests in `tests/Browser/` should not be used as reference.
+- Use `RefreshDatabase` and seed required data (at minimum `InstanceSettings::create(['id' => 0])`) in `beforeEach`.
+- Key API: `visit()`, `fill(field, value)`, `click(text)`, `assertSee()`, `assertDontSee()`, `assertPathIs()`, `screenshot()`.
+- Always call `screenshot()` at the end of each test for debugging.
+- For authenticated tests, create a helper function that logs in via the UI:
+
+```php
+function loginAsRoot(): mixed
+{
+ return visit('/login')
+ ->fill('email', 'test@example.com')
+ ->fill('password', 'password')
+ ->click('Login');
+}
+```
+
+- See `tests/v4/Browser/LoginTest.php`, `tests/v4/Browser/DashboardTest.php`, and `tests/v4/Browser/RegistrationTest.php` for conventions.
+- Chrome driver runs on `localhost:4444`, app on `localhost:8000` (configured in `tests/DuskTestCase.php`).
+- Legacy Dusk macros in `app/Providers/DuskServiceProvider.php` use the old `type()`/`press()` API — do not mix with Pest Browser Plugin's `fill()`/`click()` API.
+
## Architecture
### Backend Structure (app/)
diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php
index 43ba06804..02be98fc9 100644
--- a/app/Console/Commands/Emails.php
+++ b/app/Console/Commands/Emails.php
@@ -18,6 +18,7 @@ use Exception;
use Illuminate\Console\Command;
use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage;
+use Illuminate\Support\Str;
use Mail;
use function Laravel\Prompts\confirm;
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index f3193e9f2..8c8006973 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -30,7 +30,6 @@ use Illuminate\Validation\Rule;
use OpenApi\Attributes as OA;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
-use Visus\Cuid2\Cuid2;
class ApplicationsController extends Controller
{
@@ -64,6 +63,10 @@ class ApplicationsController extends Controller
$this->hideNestedServerSecrets($application);
}
+ if ($application->is_shown_once ?? false) {
+ $application->makeHidden(['value', 'real_value']);
+ }
+
return serializeApiResponse($application);
}
@@ -1251,7 +1254,7 @@ class ApplicationsController extends Controller
$application->isConfigurationChanged(true);
if ($instantDeploy) {
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -1490,7 +1493,7 @@ class ApplicationsController extends Controller
$application->isConfigurationChanged(true);
if ($instantDeploy) {
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -1699,7 +1702,7 @@ class ApplicationsController extends Controller
$application->isConfigurationChanged(true);
if ($instantDeploy) {
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -1745,7 +1748,7 @@ class ApplicationsController extends Controller
], 422);
}
if (! $request->has('name')) {
- $request->offsetSet('name', 'dockerfile-'.new Cuid2);
+ $request->offsetSet('name', 'dockerfile-'.new_public_id());
}
$return = $this->validateDataApplications($request, $server);
@@ -1819,7 +1822,7 @@ class ApplicationsController extends Controller
$application->isConfigurationChanged(true);
if ($instantDeploy) {
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -1863,7 +1866,7 @@ class ApplicationsController extends Controller
], 422);
}
if (! $request->has('name')) {
- $request->offsetSet('name', 'docker-image-'.new Cuid2);
+ $request->offsetSet('name', 'docker-image-'.new_public_id());
}
$return = $this->validateDataApplications($request, $server);
if ($return instanceof JsonResponse) {
@@ -1938,7 +1941,7 @@ class ApplicationsController extends Controller
$application->isConfigurationChanged(true);
if ($instantDeploy) {
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -2736,7 +2739,7 @@ class ApplicationsController extends Controller
]);
if ($instantDeploy) {
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -3643,7 +3646,7 @@ class ApplicationsController extends Controller
$this->authorize('deploy', $application);
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
@@ -3841,7 +3844,7 @@ class ApplicationsController extends Controller
$this->authorize('deploy', $application);
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
diff --git a/app/Http/Controllers/Api/CloudProviderTokensController.php b/app/Http/Controllers/Api/CloudProviderTokensController.php
index 2d8589662..a6f196f13 100644
--- a/app/Http/Controllers/Api/CloudProviderTokensController.php
+++ b/app/Http/Controllers/Api/CloudProviderTokensController.php
@@ -182,6 +182,7 @@ class CloudProviderTokensController extends Controller
if (is_null($token)) {
return response()->json(['message' => 'Cloud provider token not found.'], 404);
}
+ $this->authorize('view', $token);
return response()->json($this->removeSensitiveData($token));
}
@@ -248,6 +249,7 @@ class CloudProviderTokensController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [CloudProviderToken::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
@@ -399,6 +401,7 @@ class CloudProviderTokensController extends Controller
if (! $token) {
return response()->json(['message' => 'Cloud provider token not found.'], 404);
}
+ $this->authorize('update', $token);
$token->update(array_intersect_key($body, array_flip($allowedFields)));
@@ -480,6 +483,7 @@ class CloudProviderTokensController extends Controller
if (! $token) {
return response()->json(['message' => 'Cloud provider token not found.'], 404);
}
+ $this->authorize('delete', $token);
if ($token->hasServers()) {
return response()->json(['message' => 'Cannot delete token that is used by servers.'], 400);
@@ -550,9 +554,18 @@ class CloudProviderTokensController extends Controller
if (! $cloudToken) {
return response()->json(['message' => 'Cloud provider token not found.'], 404);
}
+ $this->authorize('view', $cloudToken);
$validation = $this->validateProviderToken($cloudToken->provider, $cloudToken->token);
+ auditLog('api.cloud_token.validated', [
+ 'team_id' => $teamId,
+ 'cloud_token_uuid' => $cloudToken->uuid,
+ 'cloud_token_name' => $cloudToken->name,
+ 'provider' => $cloudToken->provider,
+ 'valid' => $validation['valid'],
+ ]);
+
return response()->json([
'valid' => $validation['valid'],
'message' => $validation['valid'] ? 'Token is valid.' : $validation['error'],
diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php
index 7a049ae2b..da95556e8 100644
--- a/app/Http/Controllers/Api/DeployController.php
+++ b/app/Http/Controllers/Api/DeployController.php
@@ -15,7 +15,6 @@ use App\Models\Tag;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
-use Visus\Cuid2\Cuid2;
class DeployController extends Controller
{
@@ -515,7 +514,7 @@ class DeployController extends Controller
if ($dockerTag !== null && $resource->build_pack !== 'dockerimage') {
return ['message' => 'docker_tag can only be used with Docker Image applications.', 'deployment_uuid' => null];
}
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $resource,
deployment_uuid: $deployment_uuid,
diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php
index 651969b97..150743f99 100644
--- a/app/Http/Controllers/Api/GithubController.php
+++ b/app/Http/Controllers/Api/GithubController.php
@@ -183,6 +183,7 @@ class GithubController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [GithubApp::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
return $return;
@@ -564,6 +565,7 @@ class GithubController extends Controller
$githubApp = GithubApp::where('id', $github_app_id)
->where('team_id', $teamId)
->firstOrFail();
+ $this->authorize('update', $githubApp);
// Define allowed fields for update
$allowedFields = [
@@ -737,6 +739,7 @@ class GithubController extends Controller
$githubApp = GithubApp::where('id', $github_app_id)
->where('team_id', $teamId)
->firstOrFail();
+ $this->authorize('delete', $githubApp);
// Check if the GitHub app is being used by any applications
if ($githubApp->applications->isNotEmpty()) {
diff --git a/app/Http/Controllers/Api/HetznerController.php b/app/Http/Controllers/Api/HetznerController.php
index 2f35ba576..1c9d6f9ef 100644
--- a/app/Http/Controllers/Api/HetznerController.php
+++ b/app/Http/Controllers/Api/HetznerController.php
@@ -116,6 +116,7 @@ class HetznerController extends Controller
if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}
+ $this->authorize('view', $token);
try {
$hetznerService = new HetznerService($token->token);
@@ -237,6 +238,7 @@ class HetznerController extends Controller
if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}
+ $this->authorize('view', $token);
try {
$hetznerService = new HetznerService($token->token);
@@ -336,6 +338,7 @@ class HetznerController extends Controller
if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}
+ $this->authorize('view', $token);
try {
$hetznerService = new HetznerService($token->token);
@@ -445,6 +448,7 @@ class HetznerController extends Controller
if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}
+ $this->authorize('view', $token);
try {
$hetznerService = new HetznerService($token->token);
@@ -550,6 +554,7 @@ class HetznerController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [Server::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
@@ -620,6 +625,7 @@ class HetznerController extends Controller
if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}
+ $this->authorize('view', $token);
// Validate private key
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php
index 0e5f6e93b..92f19c7ae 100644
--- a/app/Http/Controllers/Api/ProjectController.php
+++ b/app/Http/Controllers/Api/ProjectController.php
@@ -97,6 +97,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
+ $this->authorize('view', $project);
$project->load(['environments']);
@@ -233,6 +234,7 @@ class ProjectController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [Project::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
@@ -385,6 +387,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
+ $this->authorize('update', $project);
$project->update($request->only($allowedFields));
@@ -469,6 +472,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
+ $this->authorize('delete', $project);
if (! $project->isEmpty()) {
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
}
@@ -652,6 +656,7 @@ class ProjectController extends Controller
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
+ $this->authorize('update', $project);
$existingEnvironment = $project->environments()->where('name', $request->name)->first();
if ($existingEnvironment) {
@@ -746,6 +751,7 @@ class ProjectController extends Controller
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
+ $this->authorize('delete', $environment);
if (! $environment->isEmpty()) {
return response()->json(['message' => 'Environment has resources, so it cannot be deleted.'], 400);
diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index 759af5134..25430631b 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -114,6 +114,7 @@ class SecurityController extends Controller
'message' => 'Private Key not found.',
], 404);
}
+ $this->authorize('view', $key);
return response()->json($this->removeSensitiveData($key));
}
@@ -180,6 +181,7 @@ class SecurityController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [PrivateKey::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
return $return;
@@ -342,6 +344,7 @@ class SecurityController extends Controller
'message' => 'Private Key not found.',
], 404);
}
+ $this->authorize('update', $foundKey);
$foundKey->update($request->only($allowedFields));
auditLog('api.private_key.updated', [
@@ -425,6 +428,7 @@ class SecurityController extends Controller
if (is_null($key)) {
return response()->json(['message' => 'Private Key not found.'], 404);
}
+ $this->authorize('delete', $key);
if ($key->isInUse()) {
return response()->json([
diff --git a/app/Http/Controllers/Api/SentinelController.php b/app/Http/Controllers/Api/SentinelController.php
index df5c60d40..3af05f4fa 100644
--- a/app/Http/Controllers/Api/SentinelController.php
+++ b/app/Http/Controllers/Api/SentinelController.php
@@ -97,12 +97,12 @@ class SentinelController extends Controller
if ($this->shouldDispatchUpdate($server, $data)) {
PushServerUpdateJob::dispatch($server, $data);
- }
- auditLog('sentinel.metrics_pushed', [
- 'server_uuid' => $server->uuid,
- 'team_id' => $server->team_id,
- ]);
+ auditLog('sentinel.metrics_pushed', [
+ 'server_uuid' => $server->uuid,
+ 'team_id' => $server->team_id,
+ ]);
+ }
return response()->json(['message' => 'ok'], 200);
}
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index 467b4c06a..a5c74d971 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -156,6 +156,7 @@ class ServersController extends Controller
if (is_null($server)) {
return response()->json(['message' => 'Server not found.'], 404);
}
+ $this->authorize('view', $server);
if ($with_resources) {
$server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [
@@ -485,6 +486,7 @@ class ServersController extends Controller
if (is_null($teamId)) {
return invalidTokenResponse();
}
+ $this->authorize('create', [ModelsServer::class]);
$return = validateIncomingRequest($request);
if ($return instanceof JsonResponse) {
@@ -709,6 +711,7 @@ class ServersController extends Controller
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
+ $this->authorize('update', $server);
if ($request->proxy_type) {
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
return str($proxyType->value)->lower();
@@ -833,6 +836,7 @@ class ServersController extends Controller
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
+ $this->authorize('delete', $server);
$force = filter_var($request->query('force', false), FILTER_VALIDATE_BOOLEAN);
@@ -932,6 +936,7 @@ class ServersController extends Controller
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
+ $this->authorize('update', $server);
ValidateServer::dispatch($server);
auditLog('api.server.validated', [
diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php
index 773fadd78..fb03c9e1a 100644
--- a/app/Http/Controllers/Api/ServicesController.php
+++ b/app/Http/Controllers/Api/ServicesController.php
@@ -42,6 +42,10 @@ class ServicesController extends Controller
$this->exposeNestedServerSecrets($service);
}
+ if ($service->is_shown_once ?? false) {
+ $service->makeHidden(['value', 'real_value']);
+ }
+
return serializeApiResponse($service);
}
diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php
index 03b36e4e0..d47399533 100644
--- a/app/Http/Controllers/Api/TeamController.php
+++ b/app/Http/Controllers/Api/TeamController.php
@@ -110,6 +110,7 @@ class TeamController extends Controller
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
+ $this->authorize('view', $team);
$team = $this->removeSensitiveData($team);
return response()->json(
@@ -168,6 +169,7 @@ class TeamController extends Controller
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
+ $this->authorize('view', $team);
$members = $team->members;
$members->makeHidden([
'pivot',
diff --git a/app/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php
index 6c3dda402..26a76ab3e 100644
--- a/app/Http/Controllers/UploadController.php
+++ b/app/Http/Controllers/UploadController.php
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Routing\Controller as BaseController;
@@ -11,6 +12,8 @@ use Pion\Laravel\ChunkUpload\Receiver\FileReceiver;
class UploadController extends BaseController
{
+ use AuthorizesRequests;
+
private const MAX_BYTES = 10 * 1024 * 1024 * 1024; // 10 GiB
private const ALLOWED_EXTENSIONS = [
@@ -40,6 +43,8 @@ class UploadController extends BaseController
return response()->json(['error' => 'You do not have permission for this database'], 500);
}
+ $this->authorize('uploadBackup', $resource);
+
$chunk = $request->file('file');
$originalName = $chunk instanceof UploadedFile ? $chunk->getClientOriginalName() : null;
if (blank($originalName) || ! self::hasAllowedExtension($originalName)) {
diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php
index d37ba7cee..435f5efab 100644
--- a/app/Http/Controllers/Webhook/Bitbucket.php
+++ b/app/Http/Controllers/Webhook/Bitbucket.php
@@ -10,7 +10,6 @@ use App\Models\Application;
use App\Models\ApplicationPreview;
use Exception;
use Illuminate\Http\Request;
-use Visus\Cuid2\Cuid2;
class Bitbucket extends Controller
{
@@ -141,7 +140,7 @@ class Bitbucket extends Controller
continue;
}
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -192,7 +191,7 @@ class Bitbucket extends Controller
continue;
}
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php
index be064e380..82a8cc8af 100644
--- a/app/Http/Controllers/Webhook/Gitea.php
+++ b/app/Http/Controllers/Webhook/Gitea.php
@@ -11,7 +11,6 @@ use App\Models\ApplicationPreview;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
-use Visus\Cuid2\Cuid2;
class Gitea extends Controller
{
@@ -127,7 +126,7 @@ class Gitea extends Controller
continue;
}
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -194,7 +193,7 @@ class Gitea extends Controller
continue;
}
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php
index 40c5cbdf0..c9b0116fb 100644
--- a/app/Http/Controllers/Webhook/Github.php
+++ b/app/Http/Controllers/Webhook/Github.php
@@ -17,7 +17,6 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
-use Visus\Cuid2\Cuid2;
class Github extends Controller
{
@@ -144,7 +143,7 @@ class Github extends Controller
continue;
}
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -362,7 +361,7 @@ class Github extends Controller
continue;
}
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php
index 231a0b6e5..c90f4ad40 100644
--- a/app/Http/Controllers/Webhook/Gitlab.php
+++ b/app/Http/Controllers/Webhook/Gitlab.php
@@ -11,7 +11,6 @@ use App\Models\ApplicationPreview;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
-use Visus\Cuid2\Cuid2;
class Gitlab extends Controller
{
@@ -168,7 +167,7 @@ class Gitlab extends Controller
continue;
}
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$result = queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -236,7 +235,7 @@ class Gitlab extends Controller
continue;
}
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
diff --git a/app/Http/Middleware/ApiAbility.php b/app/Http/Middleware/ApiAbility.php
index f81c7d184..8951ad50d 100644
--- a/app/Http/Middleware/ApiAbility.php
+++ b/app/Http/Middleware/ApiAbility.php
@@ -7,9 +7,34 @@ use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
class ApiAbility extends CheckForAnyAbility
{
+ /**
+ * Permissions that only admins/owners may use.
+ */
+ private const MEMBER_DISALLOWED_ABILITIES = [
+ 'root',
+ 'write',
+ 'write:sensitive',
+ 'deploy',
+ 'read:sensitive',
+ ];
+
public function handle($request, $next, ...$abilities)
{
try {
+ $token = $request->user()->currentAccessToken();
+ $teamId = data_get($token, 'team_id');
+
+ if ($teamId !== null && ! $request->user()->isAdminOfTeam((int) $teamId)) {
+ $tokenAbilities = $token->abilities ?? [];
+ $disallowed = array_intersect($tokenAbilities, self::MEMBER_DISALLOWED_ABILITIES);
+
+ if (! empty($disallowed)) {
+ return response()->json([
+ 'message' => 'This API token has permissions ('.implode(', ', $disallowed).') that exceed your current role as a team member. Members are restricted to read-only API access. Please revoke this token and create a new one with only read permissions.',
+ ], 403);
+ }
+ }
+
if ($request->user()->tokenCan('root')) {
return $next($request);
}
diff --git a/app/Http/Middleware/ApiSensitiveData.php b/app/Http/Middleware/ApiSensitiveData.php
index 49584ddb3..8d7c51d11 100644
--- a/app/Http/Middleware/ApiSensitiveData.php
+++ b/app/Http/Middleware/ApiSensitiveData.php
@@ -10,10 +10,13 @@ class ApiSensitiveData
public function handle(Request $request, Closure $next)
{
$token = $request->user()->currentAccessToken();
+ $hasTokenPermission = $token->can('root') || $token->can('read:sensitive');
+ $teamId = (int) data_get($token, 'team_id');
+ $isAdmin = $teamId ? $request->user()->isAdminOfTeam($teamId) : false;
- // Allow access to sensitive data if token has root or read:sensitive permission
+ // Allow access to sensitive data only if token has permission AND user is admin/owner
$request->attributes->add([
- 'can_read_sensitive' => $token->can('root') || $token->can('read:sensitive'),
+ 'can_read_sensitive' => $hasTokenPermission && $isAdmin,
]);
return $next($request);
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 1b8ef3fc4..20eae036b 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -37,7 +37,6 @@ use JsonException;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Throwable;
-use Visus\Cuid2\Cuid2;
class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -2207,7 +2206,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
continue;
}
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->application,
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 64e900b49..79bf929be 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -27,7 +27,6 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;
-use Visus\Cuid2\Cuid2;
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -309,7 +308,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
// Generate unique UUID for each database backup execution
$attempts = 0;
do {
- $this->backup_log_uuid = (string) new Cuid2;
+ $this->backup_log_uuid = new_public_id();
$exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists();
$attempts++;
if ($attempts >= 3 && $exists) {
diff --git a/app/Jobs/ProcessGithubPullRequestWebhook.php b/app/Jobs/ProcessGithubPullRequestWebhook.php
index d35ecf35a..61fc3d4ee 100644
--- a/app/Jobs/ProcessGithubPullRequestWebhook.php
+++ b/app/Jobs/ProcessGithubPullRequestWebhook.php
@@ -15,7 +15,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;
-use Visus\Cuid2\Cuid2;
class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue
{
@@ -166,7 +165,7 @@ class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue
}
// Queue the deployment
- $deployment_uuid = new Cuid2;
+ $deployment_uuid = new_public_id();
queue_application_deployment(
application: $application,
pull_request_id: $this->pullRequestId,
diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php
index 4d22047cc..226d2e332 100644
--- a/app/Livewire/Admin/Index.php
+++ b/app/Livewire/Admin/Index.php
@@ -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();
}
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index 2d0ae939d..5582efbda 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -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';
}
diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php
index 254823163..61e8bba34 100644
--- a/app/Livewire/Destination/New/Docker.php
+++ b/app/Livewire/Destination/New/Docker.php
@@ -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();
}
diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php
index 9d55d7462..1b344c905 100644
--- a/app/Livewire/Destination/Show.php
+++ b/app/Livewire/Destination/Show.php
@@ -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);
}
diff --git a/app/Livewire/MonacoEditor.php b/app/Livewire/MonacoEditor.php
index f660f9c13..cf476eb75 100644
--- a/app/Livewire/MonacoEditor.php
+++ b/app/Livewire/MonacoEditor.php
@@ -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)) {
diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php
index 8e5478b5e..52e4460ad 100644
--- a/app/Livewire/NavbarDeleteTeam.php
+++ b/app/Livewire/NavbarDeleteTeam.php
@@ -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()
diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php
index ab3884320..59350a3e1 100644
--- a/app/Livewire/Notifications/Discord.php
+++ b/app/Livewire/Notifications/Discord.php
@@ -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;
diff --git a/app/Livewire/Notifications/Pushover.php b/app/Livewire/Notifications/Pushover.php
index d79eea87b..f894c5005 100644
--- a/app/Livewire/Notifications/Pushover.php
+++ b/app/Livewire/Notifications/Pushover.php
@@ -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;
diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php
index f870b3986..58cab5494 100644
--- a/app/Livewire/Notifications/Slack.php
+++ b/app/Livewire/Notifications/Slack.php
@@ -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;
diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php
index fc3966cf6..78eb7ef9f 100644
--- a/app/Livewire/Notifications/Telegram.php
+++ b/app/Livewire/Notifications/Telegram.php
@@ -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;
diff --git a/app/Livewire/Notifications/Webhook.php b/app/Livewire/Notifications/Webhook.php
index 630d422a9..4a67180ff 100644
--- a/app/Livewire/Notifications/Webhook.php
+++ b/app/Livewire/Notifications/Webhook.php
@@ -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;
diff --git a/app/Livewire/Project/AddEmpty.php b/app/Livewire/Project/AddEmpty.php
index 974f0608a..e004ac69e 100644
--- a/app/Livewire/Project/AddEmpty.php
+++ b/app/Livewire/Project/AddEmpty.php
@@ -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();
diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php
index c9f818e2c..9ed0ab807 100644
--- a/app/Livewire/Project/Application/Deployment/Show.php
+++ b/app/Livewire/Project/Application/Deployment/Show.php
@@ -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 = '';
diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php
index ebdc014ae..b60f543ba 100644
--- a/app/Livewire/Project/Application/DeploymentNavbar.php
+++ b/app/Livewire/Project/Application/DeploymentNavbar.php
@@ -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;
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 89b1b4217..7af0a275d 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -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;
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index a46b2f19c..b7750e087 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -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.
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()
{
- $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.
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 59b52f557..74b2ebce8 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -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()) {
diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php
index 85ba2328e..e8da3b45c 100644
--- a/app/Livewire/Project/Application/PreviewsCompose.php
+++ b/app/Livewire/Project/Application/PreviewsCompose.php
@@ -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);
diff --git a/app/Livewire/Project/Application/Rollback.php b/app/Livewire/Project/Application/Rollback.php
index 3edd77833..b070ae1cc 100644
--- a/app/Livewire/Project/Application/Rollback.php
+++ b/app/Livewire/Project/Application/Rollback.php
@@ -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,
diff --git a/app/Livewire/Project/Application/Swarm.php b/app/Livewire/Project/Application/Swarm.php
index 197dc41ed..94d627e67 100644
--- a/app/Livewire/Project/Application/Swarm.php
+++ b/app/Livewire/Project/Application/Swarm.php
@@ -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) {
diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php
index 644753c83..0a6e3d8ec 100644
--- a/app/Livewire/Project/CloneMe.php
+++ b/app/Livewire/Project/CloneMe.php
@@ -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',
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index ef106a65f..99426c120 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -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 {
diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php
index 1dd93781d..41fb1681b 100644
--- a/app/Livewire/Project/Database/BackupExecutions.php
+++ b/app/Livewire/Project/Database/BackupExecutions.php
@@ -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;
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/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php
index 694674326..ad5e45b3f 100644
--- a/app/Livewire/Project/Database/Clickhouse/General.php
+++ b/app/Livewire/Project/Database/Clickhouse/General.php
@@ -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
diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php
index f196b9dfb..2f5b84484 100644
--- a/app/Livewire/Project/Database/Dragonfly/General.php
+++ b/app/Livewire/Project/Database/Dragonfly/General.php
@@ -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
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index c6c9a3c48..943f22702 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -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()
diff --git a/app/Livewire/Project/Database/InitScript.php b/app/Livewire/Project/Database/InitScript.php
index e3baa1c8e..7074c235d 100644
--- a/app/Livewire/Project/Database/InitScript.php
+++ b/app/Livewire/Project/Database/InitScript.php
@@ -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);
diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php
index 974803e8d..b2d9bce91 100644
--- a/app/Livewire/Project/Database/Keydb/General.php
+++ b/app/Livewire/Project/Database/Keydb/General.php
@@ -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
diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php
index 41266f152..61280a34b 100644
--- a/app/Livewire/Project/Database/Mariadb/General.php
+++ b/app/Livewire/Project/Database/Mariadb/General.php
@@ -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)
diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php
index 7d8249ec4..f68ba82c7 100644
--- a/app/Livewire/Project/Database/Mongodb/General.php
+++ b/app/Livewire/Project/Database/Mongodb/General.php
@@ -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)
diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php
index 6b88d735d..1adfe2ea7 100644
--- a/app/Livewire/Project/Database/Mysql/General.php
+++ b/app/Livewire/Project/Database/Mysql/General.php
@@ -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)
diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php
index 4e89e8b62..8993cc251 100644
--- a/app/Livewire/Project/Database/Postgresql/General.php
+++ b/app/Livewire/Project/Database/Postgresql/General.php
@@ -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)
diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php
index aff7b7afa..d431b1506 100644
--- a/app/Livewire/Project/Database/Redis/General.php
+++ b/app/Livewire/Project/Database/Redis/General.php
@@ -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)
diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php
index 1cf5e53f6..9d1f212e4 100644
--- a/app/Livewire/Project/Database/ScheduledBackups.php
+++ b/app/Livewire/Project/Database/ScheduledBackups.php
@@ -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
diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php
index 4d28c7676..3d7767699 100644
--- a/app/Livewire/Project/DeleteEnvironment.php
+++ b/app/Livewire/Project/DeleteEnvironment.php
@@ -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', "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 d95041c2d..598e1f61b 100644
--- a/app/Livewire/Project/DeleteProject.php
+++ b/app/Livewire/Project/DeleteProject.php
@@ -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', "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/Edit.php b/app/Livewire/Project/Edit.php
index a2d73eb5f..1314c9e4b 100644
--- a/app/Livewire/Project/Edit.php
+++ b/app/Livewire/Project/Edit.php
@@ -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) {
diff --git a/app/Livewire/Project/EnvironmentEdit.php b/app/Livewire/Project/EnvironmentEdit.php
index 529b9d7b1..9b9a3670d 100644
--- a/app/Livewire/Project/EnvironmentEdit.php
+++ b/app/Livewire/Project/EnvironmentEdit.php
@@ -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,
diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php
index 737806cb8..de86bea4a 100644
--- a/app/Livewire/Project/New/DockerImage.php
+++ b/app/Livewire/Project/New/DockerImage.php
@@ -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',
diff --git a/app/Livewire/Project/New/EmptyProject.php b/app/Livewire/Project/New/EmptyProject.php
index 0360365a9..7c92ce96b 100644
--- a/app/Livewire/Project/New/EmptyProject.php
+++ b/app/Livewire/Project/New/EmptyProject.php
@@ -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]);
diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php
index f07948dba..5a84343fd 100644
--- a/app/Livewire/Project/New/SimpleDockerfile.php
+++ b/app/Livewire/Project/New/SimpleDockerfile.php
@@ -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',
diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php
index 32cf72067..0f5c739b1 100644
--- a/app/Livewire/Project/Service/EditCompose.php
+++ b/app/Livewire/Project/Service/EditCompose.php
@@ -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()
diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php
index 2f1a229b4..877142d8b 100644
--- a/app/Livewire/Project/Service/FileStorage.php
+++ b/app/Livewire/Project/Service/FileStorage.php
@@ -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();
diff --git a/app/Livewire/Project/Service/Heading.php b/app/Livewire/Project/Service/Heading.php
index 60273ab23..34bb46ff1 100644
--- a/app/Livewire/Project/Service/Heading.php
+++ b/app/Livewire/Project/Service/Heading.php
@@ -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
diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php
index 64a7d8d8b..86d5a57c1 100644
--- a/app/Livewire/Project/Service/StackForm.php
+++ b/app/Livewire/Project/Service/StackForm.php
@@ -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);
diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php
index caaabc494..7f0d3b173 100644
--- a/app/Livewire/Project/Shared/Danger.php
+++ b/app/Livewire/Project/Shared/Danger.php
@@ -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');
diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php
index 715ce82a7..4f3e659da 100644
--- a/app/Livewire/Project/Shared/Destination.php
+++ b/app/Livewire/Project/Shared/Destination.php
@@ -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.
More information here: documentation');
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.';
}
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index a19837e16..3a5145023 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -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)";
}
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
index 26369852e..094320bdd 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
@@ -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;
}
}
diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
index 4ea5e12db..3fa063298 100644
--- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php
+++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
@@ -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.');
}
diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php
index 195e7fd92..5fa62b04e 100644
--- a/app/Livewire/Project/Shared/HealthChecks.php
+++ b/app/Livewire/Project/Shared/HealthChecks.php
@@ -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
diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php
index 2a8747c33..02171af8d 100644
--- a/app/Livewire/Project/Shared/ResourceOperations.php
+++ b/app/Livewire/Project/Shared/ResourceOperations.php
@@ -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);
}
}
diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php
index db65cdaad..46c75e352 100644
--- a/app/Livewire/Project/Shared/Terminal.php
+++ b/app/Livewire/Project/Shared/Terminal.php
@@ -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.');
}
diff --git a/app/Livewire/Project/Shared/UploadConfig.php b/app/Livewire/Project/Shared/UploadConfig.php
index 1b10f588b..0f0894687 100644
--- a/app/Livewire/Project/Shared/UploadConfig.php
+++ b/app/Livewire/Project/Shared/UploadConfig.php
@@ -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);
}
}
diff --git a/app/Livewire/Project/Shared/Webhooks.php b/app/Livewire/Project/Shared/Webhooks.php
index eafc653d5..eb90262e9 100644
--- a/app/Livewire/Project/Shared/Webhooks.php
+++ b/app/Livewire/Project/Shared/Webhooks.php
@@ -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 {
diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php
index e884abb4e..fc84e4fbd 100644
--- a/app/Livewire/Project/Show.php
+++ b/app/Livewire/Project/Show.php
@@ -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', [
diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php
index c275ec097..bf201e257 100644
--- a/app/Livewire/Security/ApiTokens.php
+++ b/app/Livewire/Security/ApiTokens.php
@@ -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);
diff --git a/app/Livewire/Security/CloudInitScriptForm.php b/app/Livewire/Security/CloudInitScriptForm.php
index 33beff334..c7f933d39 100644
--- a/app/Livewire/Security/CloudInitScriptForm.php
+++ b/app/Livewire/Security/CloudInitScriptForm.php
@@ -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.';
}
diff --git a/app/Livewire/Security/CloudInitScripts.php b/app/Livewire/Security/CloudInitScripts.php
index 13bcf2caa..57b7324d3 100644
--- a/app/Livewire/Security/CloudInitScripts.php
+++ b/app/Livewire/Security/CloudInitScripts.php
@@ -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);
diff --git a/app/Livewire/Security/CloudProviderTokenForm.php b/app/Livewire/Security/CloudProviderTokenForm.php
index 7affb1531..6d0efa15f 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
@@ -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
diff --git a/app/Livewire/Security/CloudProviderTokens.php b/app/Livewire/Security/CloudProviderTokens.php
index cfef30772..dabb199ed 100644
--- a/app/Livewire/Security/CloudProviderTokens.php
+++ b/app/Livewire/Security/CloudProviderTokens.php
@@ -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);
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 @@ 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);
+ }
}
}
diff --git a/app/Livewire/Server/CloudProviderToken/Show.php b/app/Livewire/Server/CloudProviderToken/Show.php
index 6b22fddc6..e3232d3f3 100644
--- a/app/Livewire/Server/CloudProviderToken/Show.php
+++ b/app/Livewire/Server/CloudProviderToken/Show.php
@@ -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);
}
diff --git a/app/Livewire/Server/CloudflareTunnel.php b/app/Livewire/Server/CloudflareTunnel.php
index 24f8e022e..2ab829854 100644
--- a/app/Livewire/Server/CloudflareTunnel.php
+++ b/app/Livewire/Server/CloudflareTunnel.php
@@ -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()
diff --git a/app/Livewire/Server/Destinations.php b/app/Livewire/Server/Destinations.php
index f3f142646..7b5d6f9da 100644
--- a/app/Livewire/Server/Destinations.php
+++ b/app/Livewire/Server/Destinations.php
@@ -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 {
diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php
index 4c6f31b0c..9ae065d83 100644
--- a/app/Livewire/Server/New/ByHetzner.php
+++ b/app/Livewire/Server/New/ByHetzner.php
@@ -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);
}
}
diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php
index c2d8205ef..8cd4e9640 100644
--- a/app/Livewire/Server/Proxy.php
+++ b/app/Livewire/Server/Proxy.php
@@ -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)
diff --git a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
index 20d14ddc7..4ea21e4ae 100644
--- a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
+++ b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
@@ -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()
diff --git a/app/Livewire/Server/Proxy/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php
index 6ea9e7c3d..f824645aa 100644
--- a/app/Livewire/Server/Proxy/DynamicConfigurations.php
+++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php
@@ -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));
diff --git a/app/Livewire/Server/Resources.php b/app/Livewire/Server/Resources.php
index 3710064dc..9ea87161d 100644
--- a/app/Livewire/Server/Resources.php
+++ b/app/Livewire/Server/Resources.php
@@ -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()
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 @@ 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);
}
}
diff --git a/app/Livewire/Server/Sentinel/Logs.php b/app/Livewire/Server/Sentinel/Logs.php
index 6619e101e..1190cd59a 100644
--- a/app/Livewire/Server/Sentinel/Logs.php
+++ b/app/Livewire/Server/Sentinel/Logs.php
@@ -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
diff --git a/app/Livewire/Server/Sentinel/Show.php b/app/Livewire/Server/Sentinel/Show.php
index 7070a09ce..fc30994d6 100644
--- a/app/Livewire/Server/Sentinel/Show.php
+++ b/app/Livewire/Server/Sentinel/Show.php
@@ -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
diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php
index 4a6e2335e..d14046ed1 100644
--- a/app/Livewire/Server/Show.php
+++ b/app/Livewire/Server/Show.php
@@ -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.
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);
}
}
@@ -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.');
diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php
index 59ca4cd36..afcc918a6 100644
--- a/app/Livewire/Server/ValidateAndInstall.php
+++ b/app/Livewire/Server/ValidateAndInstall.php
@@ -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.
Check this documentation for further help.
diff --git a/resources/views/livewire/notifications/telegram.blade.php b/resources/views/livewire/notifications/telegram.blade.php
index f87a13c37..98ec128d5 100644
--- a/resources/views/livewire/notifications/telegram.blade.php
+++ b/resources/views/livewire/notifications/telegram.blade.php
@@ -24,12 +24,21 @@