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.

Error: '.$sanitizedError.'
'; - $this->server->update([ - 'validation_logs' => $this->error, - ]); + try { + $this->authorize('update', $this->server); + ['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection(); + if (! $this->uptime) { + $sanitizedError = htmlspecialchars($error ?? '', ENT_QUOTES, 'UTF-8'); + $this->error = 'Server is not reachable. Please validate your configuration and connection.
Check this documentation for further help.

Error: '.$sanitizedError.'
'; + $this->server->update([ + 'validation_logs' => $this->error, + ]); - return; + return; + } + $this->dispatch('validateOS'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->dispatch('validateOS'); } public function validateOS() diff --git a/app/Livewire/Settings/Advanced.php b/app/Livewire/Settings/Advanced.php index 3a6237183..5be066c7f 100644 --- a/app/Livewire/Settings/Advanced.php +++ b/app/Livewire/Settings/Advanced.php @@ -5,11 +5,14 @@ namespace App\Livewire\Settings; use App\Models\InstanceSettings; use App\Rules\ValidDnsServers; use App\Rules\ValidIpOrCidr; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Validate; use Livewire\Component; class Advanced extends Component { + use AuthorizesRequests; + public InstanceSettings $settings; #[Validate('boolean')] @@ -77,6 +80,7 @@ class Advanced extends Component public function submit() { try { + $this->authorize('update', $this->settings); $this->validate(); $this->custom_dns_servers = str($this->custom_dns_servers)->replaceEnd(',', '')->trim(); @@ -146,6 +150,7 @@ class Advanced extends Component public function instantSave() { try { + $this->authorize('update', $this->settings); $this->settings->is_registration_enabled = $this->is_registration_enabled; $this->settings->do_not_track = $this->do_not_track; $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; @@ -178,6 +183,7 @@ class Advanced extends Component public function toggleTwoStepConfirmation($password): bool { + $this->authorize('update', $this->settings); if (! verifyPasswordConfirmation($password, $this)) { return false; } diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index c2789aa91..9cfc4f6c8 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -4,12 +4,15 @@ namespace App\Livewire\Settings; use App\Models\InstanceSettings; use App\Models\Server; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Computed; use Livewire\Attributes\Validate; use Livewire\Component; class Index extends Component { + use AuthorizesRequests; + public InstanceSettings $settings; public ?Server $server = null; @@ -87,6 +90,7 @@ class Index extends Component public function instantSave($isSave = true) { + $this->authorize('update', $this->settings); $this->validate(); $this->settings->fqdn = $this->fqdn ? trim($this->fqdn) : $this->fqdn; $this->settings->public_port_min = $this->public_port_min; @@ -104,6 +108,7 @@ class Index extends Component public function confirmDomainUsage() { + $this->authorize('update', $this->settings); $this->forceSaveDomains = true; $this->showDomainConflictModal = false; $this->submit(); @@ -112,6 +117,7 @@ class Index extends Component public function submit() { try { + $this->authorize('update', $this->settings); $error_show = false; $this->resetErrorBag(); @@ -173,6 +179,7 @@ class Index extends Component public function buildHelperImage() { try { + $this->authorize('update', $this->settings); if (! isDev()) { $this->dispatch('error', 'Building helper image is only available in development mode.'); diff --git a/app/Livewire/Settings/Updates.php b/app/Livewire/Settings/Updates.php index a200ef689..88a8945d0 100644 --- a/app/Livewire/Settings/Updates.php +++ b/app/Livewire/Settings/Updates.php @@ -5,11 +5,14 @@ namespace App\Livewire\Settings; use App\Jobs\CheckForUpdatesJob; use App\Models\InstanceSettings; use App\Models\Server; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Validate; use Livewire\Component; class Updates extends Component { + use AuthorizesRequests; + public InstanceSettings $settings; public ?Server $server = null; @@ -41,6 +44,7 @@ class Updates extends Component public function instantSave() { try { + $this->authorize('update', $this->settings); if ($this->settings->is_auto_update_enabled === true) { $this->validate([ 'auto_update_frequency' => ['required', 'string'], @@ -59,6 +63,7 @@ class Updates extends Component public function submit() { try { + $this->authorize('update', $this->settings); $this->resetErrorBag(); $this->validate(); @@ -91,6 +96,7 @@ class Updates extends Component public function checkManually() { + $this->authorize('update', $this->settings); CheckForUpdatesJob::dispatchSync(); $this->dispatch('updateAvailable'); $settings = instanceSettings(); diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index 5336c0c9a..d5d80acbd 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -8,12 +8,15 @@ use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\StandalonePostgresql; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; use Livewire\Component; class SettingsBackup extends Component { + use AuthorizesRequests; + public InstanceSettings $settings; public Server $server; @@ -77,6 +80,7 @@ class SettingsBackup extends Component public function addCoolifyDatabase() { try { + $this->authorize('update', $this->settings); $server = Server::findOrFail(0); $out = instant_remote_process(['docker inspect coolify-db'], $server); $envs = format_docker_envs_to_json($out); @@ -123,14 +127,19 @@ class SettingsBackup extends Component public function submit() { - $this->validate(); + try { + $this->authorize('update', $this->settings); + $this->validate(); - $this->database->update([ - 'name' => $this->name, - 'description' => $this->description, - 'postgres_user' => $this->postgres_user, - 'postgres_password' => $this->postgres_password, - ]); - $this->dispatch('success', 'Backup updated.'); + $this->database->update([ + 'name' => $this->name, + 'description' => $this->description, + 'postgres_user' => $this->postgres_user, + 'postgres_password' => $this->postgres_password, + ]); + $this->dispatch('success', 'Backup updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } } diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 8c0e24400..1bb9b2360 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -5,6 +5,7 @@ namespace App\Livewire; use App\Models\InstanceSettings; use App\Models\Team; use App\Notifications\TransactionalEmails\Test; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\RateLimiter; use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; @@ -12,6 +13,8 @@ use Livewire\Component; class SettingsEmail extends Component { + use AuthorizesRequests; + public InstanceSettings $settings; #[Locked] @@ -103,6 +106,7 @@ class SettingsEmail extends Component public function submit() { try { + $this->authorize('update', $this->settings); $this->resetErrorBag(); $this->syncData(true); $this->dispatch('success', 'Transactional email settings updated.'); @@ -114,6 +118,7 @@ class SettingsEmail extends Component public function instantSave(string $type) { try { + $this->authorize('update', $this->settings); $currentSmtpEnabled = $this->settings->smtp_enabled; $currentResendEnabled = $this->settings->resend_enabled; $this->resetErrorBag(); @@ -141,6 +146,7 @@ class SettingsEmail extends Component public function submitSmtp() { try { + $this->authorize('update', $this->settings); $this->validate([ 'smtpEnabled' => 'boolean', 'smtpFromAddress' => 'required|email', @@ -184,6 +190,7 @@ class SettingsEmail extends Component public function submitResend() { try { + $this->authorize('update', $this->settings); $this->validate([ 'resendEnabled' => 'boolean', 'resendApiKey' => 'required|string', @@ -214,6 +221,7 @@ class SettingsEmail extends Component public function sendTestEmail() { try { + $this->authorize('update', $this->settings); $this->validate([ 'testEmailAddress' => 'required|email', ], [ diff --git a/app/Livewire/SettingsOauth.php b/app/Livewire/SettingsOauth.php index 6f949b716..44ea4f611 100644 --- a/app/Livewire/SettingsOauth.php +++ b/app/Livewire/SettingsOauth.php @@ -3,10 +3,13 @@ namespace App\Livewire; use App\Models\OauthSetting; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class SettingsOauth extends Component { + use AuthorizesRequests; + public $oauth_settings_map; protected function rules() @@ -131,6 +134,7 @@ class SettingsOauth extends Component public function instantSave(string $provider) { try { + $this->authorize('update', instanceSettings()); $this->updateOauthSettings($provider); } catch (\Exception $e) { return handleError($e, $this); @@ -139,7 +143,12 @@ class SettingsOauth extends Component public function submit() { - $this->updateOauthSettings(); - $this->dispatch('success', 'Instance settings updated successfully!'); + try { + $this->authorize('update', instanceSettings()); + $this->updateOauthSettings(); + $this->dispatch('success', 'Instance settings updated successfully!'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } } diff --git a/app/Livewire/SharedVariables/Environment/Show.php b/app/Livewire/SharedVariables/Environment/Show.php index bfbdf9212..4bc467959 100644 --- a/app/Livewire/SharedVariables/Environment/Show.php +++ b/app/Livewire/SharedVariables/Environment/Show.php @@ -76,7 +76,12 @@ class Show extends Component private function formatEnvironmentVariables($variables) { - return $variables->map(function ($item) { + $isMember = auth()->user()?->isMember(); + + return $variables->map(function ($item) use ($isMember) { + if ($isMember) { + return "$item->key=(Hidden, only admins can view)"; + } if ($item->is_shown_once) { return "$item->key=(Locked Secret, delete and add again to change)"; } diff --git a/app/Livewire/SharedVariables/Project/Show.php b/app/Livewire/SharedVariables/Project/Show.php index c9f0dcd8e..6b7f26c26 100644 --- a/app/Livewire/SharedVariables/Project/Show.php +++ b/app/Livewire/SharedVariables/Project/Show.php @@ -58,9 +58,13 @@ class Show extends Component public function switch() { - $this->authorize('view', $this->project); - $this->view = $this->view === 'normal' ? 'dev' : 'normal'; - $this->getDevView(); + try { + $this->authorize('view', $this->project); + $this->view = $this->view === 'normal' ? 'dev' : 'normal'; + $this->getDevView(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function getDevView() @@ -70,7 +74,12 @@ class Show extends Component private function formatEnvironmentVariables($variables) { - return $variables->map(function ($item) { + $isMember = auth()->user()?->isMember(); + + return $variables->map(function ($item) use ($isMember) { + if ($isMember) { + return "$item->key=(Hidden, only admins can view)"; + } if ($item->is_shown_once) { return "$item->key=(Locked Secret, delete and add again to change)"; } diff --git a/app/Livewire/SharedVariables/Team/Index.php b/app/Livewire/SharedVariables/Team/Index.php index 29e21a1b7..32da83672 100644 --- a/app/Livewire/SharedVariables/Team/Index.php +++ b/app/Livewire/SharedVariables/Team/Index.php @@ -52,9 +52,13 @@ class Index extends Component public function switch() { - $this->authorize('view', $this->team); - $this->view = $this->view === 'normal' ? 'dev' : 'normal'; - $this->getDevView(); + try { + $this->authorize('view', $this->team); + $this->view = $this->view === 'normal' ? 'dev' : 'normal'; + $this->getDevView(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function getDevView() @@ -64,7 +68,12 @@ class Index extends Component private function formatEnvironmentVariables($variables) { - return $variables->map(function ($item) { + $isMember = auth()->user()?->isMember(); + + return $variables->map(function ($item) use ($isMember) { + if ($isMember) { + return "$item->key=(Hidden, only admins can view)"; + } if ($item->is_shown_once) { return "$item->key=(Locked Secret, delete and add again to change)"; } diff --git a/app/Livewire/Storage/Form.php b/app/Livewire/Storage/Form.php index 342d629cb..b83b41e9f 100644 --- a/app/Livewire/Storage/Form.php +++ b/app/Livewire/Storage/Form.php @@ -33,6 +33,8 @@ class Form extends Component public ?bool $isUsable = null; + public bool $isPasswordHiddenForMember = false; + protected function rules(): array { return [ @@ -110,6 +112,12 @@ class Form extends Component public function mount() { $this->syncData(false); + + $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false; + if ($this->isPasswordHiddenForMember) { + $this->key = ''; + $this->secret = ''; + } } public function testConnection() diff --git a/app/Livewire/Storage/Resources.php b/app/Livewire/Storage/Resources.php index 0dad2d548..4f39943e4 100644 --- a/app/Livewire/Storage/Resources.php +++ b/app/Livewire/Storage/Resources.php @@ -4,16 +4,21 @@ namespace App\Livewire\Storage; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; class Resources extends Component { + use AuthorizesRequests; + public S3Storage $storage; public array $selectedStorages = []; public function mount(): void { + $this->authorize('view', $this->storage); + $backups = ScheduledDatabaseBackup::where('s3_storage_id', $this->storage->id) ->where('save_s3', true) ->get(); @@ -25,6 +30,8 @@ class Resources extends Component public function disableS3(int $backupId): void { + $this->authorize('update', $this->storage); + $backup = ScheduledDatabaseBackup::where('id', $backupId) ->where('s3_storage_id', $this->storage->id) ->firstOrFail(); @@ -41,6 +48,8 @@ class Resources extends Component public function moveBackup(int $backupId): void { + $this->authorize('update', $this->storage); + $backup = ScheduledDatabaseBackup::where('id', $backupId) ->where('s3_storage_id', $this->storage->id) ->firstOrFail(); @@ -62,6 +71,8 @@ class Resources extends Component return; } + $this->authorize('update', $newStorage); + $backup->update(['s3_storage_id' => $newStorage->id]); unset($this->selectedStorages[$backupId]); diff --git a/app/Livewire/Storage/Show.php b/app/Livewire/Storage/Show.php index dc5121e94..dd6640c23 100644 --- a/app/Livewire/Storage/Show.php +++ b/app/Livewire/Storage/Show.php @@ -4,6 +4,7 @@ namespace App\Livewire\Storage; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Component; @@ -23,7 +24,11 @@ class Show extends Component if (! $this->storage) { abort(404); } - $this->authorize('view', $this->storage); + try { + $this->authorize('view', $this->storage); + } catch (AuthorizationException) { + return $this->redirectRoute('storage.index', navigate: true); + } $this->currentRoute = request()->route()->getName(); $this->backupCount = ScheduledDatabaseBackup::where('s3_storage_id', $this->storage->id)->count(); } diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php index fc5b13374..28d6440a9 100644 --- a/app/Livewire/Tags/Show.php +++ b/app/Livewire/Tags/Show.php @@ -5,6 +5,7 @@ namespace App\Livewire\Tags; use App\Http\Controllers\Api\DeployController; use App\Models\ApplicationDeploymentQueue; use App\Models\Tag; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; use Livewire\Attributes\Locked; use Livewire\Attributes\Title; @@ -13,6 +14,8 @@ use Livewire\Component; #[Title('Tags | Coolify')] class Show extends Component { + use AuthorizesRequests; + #[Locked] public ?string $tagName = null; @@ -73,6 +76,12 @@ class Show extends Component public function redeployAll() { try { + $this->applications->each(function ($resource) { + $this->authorize('deploy', $resource); + }); + $this->services->each(function ($resource) { + $this->authorize('deploy', $resource); + }); $message = collect([]); $this->applications->each(function ($resource) use ($message) { $deploy = new DeployController; diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php index 09878f27b..b9cb3a43b 100644 --- a/app/Livewire/Team/AdminView.php +++ b/app/Livewire/Team/AdminView.php @@ -25,6 +25,9 @@ class AdminView extends Component public function submitSearch() { + if (! isInstanceAdmin()) { + return; + } if ($this->search !== '') { $this->users = User::where(function ($query) { $query->where('name', 'like', "%{$this->search}%") @@ -39,6 +42,9 @@ class AdminView extends Component public function getUsers() { + if (! isInstanceAdmin()) { + return; + } $users = User::where('id', '!=', auth()->id())->get(); if ($users->count() > $this->number_of_users_to_show) { $this->lots_of_users = true; diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php index 8a943e6b6..140d9f5cc 100644 --- a/app/Livewire/Team/Index.php +++ b/app/Livewire/Team/Index.php @@ -7,6 +7,7 @@ use App\Models\TeamInvitation; use App\Support\ValidationPatterns; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Livewire\Component; @@ -95,23 +96,31 @@ class Index extends Component public function delete() { - $currentTeam = currentTeam(); - $this->authorize('delete', $currentTeam); - $currentTeam->delete(); + try { + $currentTeam = currentTeam(); + $this->authorize('delete', $currentTeam); + $currentTeam->members->each(function ($user) use ($currentTeam) { + if ($user->id === Auth::id()) { + return; + } + $user->teams()->detach($currentTeam); + $session = DB::table('sessions')->where('user_id', $user->id)->first(); + if ($session) { + DB::table('sessions')->where('id', $session->id)->delete(); + } + }); - $currentTeam->members->each(function ($user) use ($currentTeam) { - if ($user->id === Auth::id()) { - return; - } - $user->teams()->detach($currentTeam); - $session = DB::table('sessions')->where('user_id', $user->id)->first(); - if ($session) { - DB::table('sessions')->where('id', $session->id)->delete(); - } - }); + // Clear stale cache before deleting so refreshSession doesn't resolve the deleted team + Cache::forget('user:'.Auth::id().':team:'.$currentTeam->id); + $currentTeam->delete(); - refreshSession(); + // Switch to the user's next available team + $newTeam = Auth::user()->teams()->first(); + refreshSession($newTeam); - return redirect()->route('team.index'); + return redirect()->route('team.index'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } } diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php index fb30961e9..5b040db71 100644 --- a/app/Livewire/Team/InviteLink.php +++ b/app/Livewire/Team/InviteLink.php @@ -10,7 +10,6 @@ use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Livewire\Component; -use Visus\Cuid2\Cuid2; class InviteLink extends Component { @@ -61,7 +60,7 @@ class InviteLink extends Component if ($member_emails->contains($this->email)) { return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.'); } - $uuid = (string) new Cuid2(32); + $uuid = new_public_id(32); $link = url('/').config('constants.invitation.link.base_url').$uuid; $user = User::whereEmail($this->email)->first(); diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php index 1b8701d94..0ccb06a08 100644 --- a/app/Livewire/Upgrade.php +++ b/app/Livewire/Upgrade.php @@ -62,6 +62,9 @@ class Upgrade extends Component public function upgrade() { try { + if (! isInstanceAdmin()) { + abort(403); + } if ($this->updateInProgress) { return; } diff --git a/app/Mcp/Concerns/ResolvesTeam.php b/app/Mcp/Concerns/ResolvesTeam.php index f6d82453a..8e0ae0467 100644 --- a/app/Mcp/Concerns/ResolvesTeam.php +++ b/app/Mcp/Concerns/ResolvesTeam.php @@ -7,15 +7,19 @@ use Laravel\Mcp\Response; trait ResolvesTeam { - protected function ensureAbility(Request $request, string $ability = 'read'): ?Response + protected function ensureAbility(Request $request, string $ability = 'read', ?string $tool = null): ?Response { $user = $request->user(); if (! $user) { + $this->auditMcpTool($request, $tool, 'denied', ['reason' => 'unauthenticated']); + return Response::error('Unauthenticated.'); } $token = $user->currentAccessToken(); if (! $token) { + $this->auditMcpTool($request, $tool, 'denied', ['reason' => 'invalid_token']); + return Response::error('Invalid token.'); } @@ -23,6 +27,11 @@ trait ResolvesTeam return null; } + $this->auditMcpTool($request, $tool, 'denied', [ + 'reason' => 'missing_ability', + 'required_ability' => $ability, + ]); + return Response::error("Missing required permissions: {$ability}"); } @@ -38,4 +47,28 @@ trait ResolvesTeam return (int) $teamId; } + + protected function mcpSuccess(Request $request, Response $response, array $context = []): Response + { + $this->auditMcpTool($request, $this->name ?? null, 'success', $context); + + return $response; + } + + protected function mcpError(Request $request, string $message, array $context = []): Response + { + $this->auditMcpTool($request, $this->name ?? null, 'error', $context + ['reason' => $message]); + + return Response::error($message); + } + + protected function auditMcpTool(Request $request, ?string $tool, string $outcome, array $context = []): void + { + auditLog('mcp.tool.called', [ + 'tool' => $tool ?: 'unknown', + 'team_id' => $this->resolveTeamId($request), + 'outcome' => $outcome, + ...$context, + ]); + } } diff --git a/app/Mcp/Servers/CoolifyServer.php b/app/Mcp/Servers/CoolifyServer.php index aff7e3f76..2b2d33d60 100644 --- a/app/Mcp/Servers/CoolifyServer.php +++ b/app/Mcp/Servers/CoolifyServer.php @@ -13,13 +13,14 @@ use App\Mcp\Tools\ListProjects; use App\Mcp\Tools\ListServers; use App\Mcp\Tools\ListServices; use Laravel\Mcp\Server; -use Laravel\Mcp\Server\Attributes\Instructions; -use Laravel\Mcp\Server\Attributes\Name; -use Laravel\Mcp\Server\Attributes\Version; -#[Name('Coolify')] -#[Version('0.1.0')] -#[Instructions(<<<'MD' +class CoolifyServer extends Server +{ + protected string $name = 'Coolify'; + + protected string $version = '0.1.0'; + + protected string $instructions = <<<'MD' Read-only MCP server for Coolify, scoped to the authenticated team token. Recommended workflow: @@ -28,9 +29,8 @@ Recommended workflow: 3. get_server / get_application / get_database / get_service — full details for a single UUID. Every response is `{ data, _actions?, _pagination? }`. `_actions` suggests the next tool + args; `_pagination.next` is the args to call again for the next page. -MD)] -class CoolifyServer extends Server -{ +MD; + protected array $tools = [ GetInfrastructureOverview::class, ListServers::class, diff --git a/app/Mcp/Tools/GetApplication.php b/app/Mcp/Tools/GetApplication.php index f7ac8db77..1d2f9f014 100644 --- a/app/Mcp/Tools/GetApplication.php +++ b/app/Mcp/Tools/GetApplication.php @@ -8,36 +8,36 @@ use App\Models\Application; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Mcp\Request; use Laravel\Mcp\Response; -use Laravel\Mcp\Server\Attributes\Description; -use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Tool; -#[Name('get_application')] -#[Description('Get full details for a single application by UUID.')] class GetApplication extends Tool { + protected string $name = 'get_application'; + + protected string $description = 'Get full details for a single application by UUID.'; + use BuildsResponse; use ResolvesTeam; public function handle(Request $request): Response { - if ($error = $this->ensureAbility($request, 'read')) { + if ($error = $this->ensureAbility($request, 'read', $this->name)) { return $error; } $teamId = $this->resolveTeamId($request); if (is_null($teamId)) { - return Response::error('Invalid token.'); + return $this->mcpError($request, 'Invalid token.'); } $uuid = $request->get('uuid'); if (! is_string($uuid) || $uuid === '') { - return Response::error('uuid argument is required.'); + return $this->mcpError($request, 'uuid argument is required.'); } $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $uuid)->first(); if (! $application) { - return Response::error("Application [{$uuid}] not found."); + return $this->mcpError($request, "Application [{$uuid}] not found.", ['resource_uuid' => $uuid]); } // Drop relations that the server_status accessor lazy-loads — they @@ -45,10 +45,10 @@ class GetApplication extends Tool $application->setRelations([]); $application->makeHidden(['destination', 'source', 'additional_servers', 'environment', 'tags', 'environmentVariables']); - return $this->respond( + return $this->mcpSuccess($request, $this->respond( $this->scrubSensitive($application->toArray()), $this->actionsForApplication($uuid, $application->status), - ); + ), ['resource_uuid' => $uuid]); } public function schema(JsonSchema $schema): array diff --git a/app/Mcp/Tools/GetDatabase.php b/app/Mcp/Tools/GetDatabase.php index 4eee9c961..c5d62e3a0 100644 --- a/app/Mcp/Tools/GetDatabase.php +++ b/app/Mcp/Tools/GetDatabase.php @@ -7,46 +7,46 @@ use App\Mcp\Concerns\ResolvesTeam; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Mcp\Request; use Laravel\Mcp\Response; -use Laravel\Mcp\Server\Attributes\Description; -use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Tool; -#[Name('get_database')] -#[Description('Get full details for a standalone database by UUID. Detects type across postgresql, mysql, mariadb, mongodb, redis, keydb, dragonfly, clickhouse.')] class GetDatabase extends Tool { + protected string $name = 'get_database'; + + protected string $description = 'Get full details for a standalone database by UUID. Detects type across postgresql, mysql, mariadb, mongodb, redis, keydb, dragonfly, clickhouse.'; + use BuildsResponse; use ResolvesTeam; public function handle(Request $request): Response { - if ($error = $this->ensureAbility($request, 'read')) { + if ($error = $this->ensureAbility($request, 'read', $this->name)) { return $error; } $teamId = $this->resolveTeamId($request); if (is_null($teamId)) { - return Response::error('Invalid token.'); + return $this->mcpError($request, 'Invalid token.'); } $uuid = $request->get('uuid'); if (! is_string($uuid) || $uuid === '') { - return Response::error('uuid argument is required.'); + return $this->mcpError($request, 'uuid argument is required.'); } $database = queryDatabaseByUuidWithinTeam($uuid, (string) $teamId); if (! $database) { - return Response::error("Database [{$uuid}] not found."); + return $this->mcpError($request, "Database [{$uuid}] not found.", ['resource_uuid' => $uuid]); } // Drop relations so deep server/destination data doesn't leak. $database->setRelations([]); $database->makeHidden(['destination', 'source', 'environment', 'environment_variables', 'environment_variables_preview']); - return $this->respond( + return $this->mcpSuccess($request, $this->respond( $this->scrubSensitive($database->toArray()), $this->actionsForDatabase($uuid, $database->status ?? null), - ); + ), ['resource_uuid' => $uuid]); } public function schema(JsonSchema $schema): array diff --git a/app/Mcp/Tools/GetInfrastructureOverview.php b/app/Mcp/Tools/GetInfrastructureOverview.php index 06e91ff57..6fcafa316 100644 --- a/app/Mcp/Tools/GetInfrastructureOverview.php +++ b/app/Mcp/Tools/GetInfrastructureOverview.php @@ -9,26 +9,26 @@ use App\Models\Server; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Mcp\Request; use Laravel\Mcp\Response; -use Laravel\Mcp\Server\Attributes\Description; -use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Tool; -#[Name('get_infrastructure_overview')] -#[Description('High-level overview of the authenticated team: Coolify version, all servers, projects with resource counts, and aggregate counts. Start here to understand the setup.')] class GetInfrastructureOverview extends Tool { + protected string $name = 'get_infrastructure_overview'; + + protected string $description = 'High-level overview of the authenticated team: Coolify version, all servers, projects with resource counts, and aggregate counts. Start here to understand the setup.'; + use BuildsResponse; use ResolvesTeam; public function handle(Request $request): Response { - if ($error = $this->ensureAbility($request, 'read')) { + if ($error = $this->ensureAbility($request, 'read', $this->name)) { return $error; } $teamId = $this->resolveTeamId($request); if (is_null($teamId)) { - return Response::error('Invalid token.'); + return $this->mcpError($request, 'Invalid token.'); } $servers = Server::whereTeamId($teamId) @@ -72,7 +72,7 @@ class GetInfrastructureOverview extends Tool ]; } - return $this->respond([ + return $this->mcpSuccess($request, $this->respond([ 'coolify_version' => config('constants.coolify.version'), 'servers' => $servers, 'projects' => $projectSummaries, @@ -83,7 +83,7 @@ class GetInfrastructureOverview extends Tool 'services' => $serviceCount, 'databases' => $databaseCount, ], - ]); + ])); } public function schema(JsonSchema $schema): array diff --git a/app/Mcp/Tools/GetServer.php b/app/Mcp/Tools/GetServer.php index fc3e72f14..771aa7d36 100644 --- a/app/Mcp/Tools/GetServer.php +++ b/app/Mcp/Tools/GetServer.php @@ -8,36 +8,36 @@ use App\Models\Server; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Mcp\Request; use Laravel\Mcp\Response; -use Laravel\Mcp\Server\Attributes\Description; -use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Tool; -#[Name('get_server')] -#[Description('Get full details for a single server by UUID.')] class GetServer extends Tool { + protected string $name = 'get_server'; + + protected string $description = 'Get full details for a single server by UUID.'; + use BuildsResponse; use ResolvesTeam; public function handle(Request $request): Response { - if ($error = $this->ensureAbility($request, 'read')) { + if ($error = $this->ensureAbility($request, 'read', $this->name)) { return $error; } $teamId = $this->resolveTeamId($request); if (is_null($teamId)) { - return Response::error('Invalid token.'); + return $this->mcpError($request, 'Invalid token.'); } $uuid = $request->get('uuid'); if (! is_string($uuid) || $uuid === '') { - return Response::error('uuid argument is required.'); + return $this->mcpError($request, 'uuid argument is required.'); } $server = Server::whereTeamId($teamId)->where('uuid', $uuid)->with('settings')->first(); if (! $server) { - return Response::error("Server [{$uuid}] not found."); + return $this->mcpError($request, "Server [{$uuid}] not found.", ['resource_uuid' => $uuid]); } $data = $this->scrubSensitive($server->toArray()); @@ -45,7 +45,7 @@ class GetServer extends Tool $data['is_usable'] = $server->settings?->is_usable; $data['connection_timeout'] = $server->settings?->connection_timeout; - return $this->respond($data, $this->actionsForServer($uuid)); + return $this->mcpSuccess($request, $this->respond($data, $this->actionsForServer($uuid)), ['resource_uuid' => $uuid]); } public function schema(JsonSchema $schema): array diff --git a/app/Mcp/Tools/GetService.php b/app/Mcp/Tools/GetService.php index 475958272..ad14ddb49 100644 --- a/app/Mcp/Tools/GetService.php +++ b/app/Mcp/Tools/GetService.php @@ -8,31 +8,31 @@ use App\Models\Service; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Mcp\Request; use Laravel\Mcp\Response; -use Laravel\Mcp\Server\Attributes\Description; -use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Tool; -#[Name('get_service')] -#[Description('Get full details for a single service (multi-container stack) by UUID.')] class GetService extends Tool { + protected string $name = 'get_service'; + + protected string $description = 'Get full details for a single service (multi-container stack) by UUID.'; + use BuildsResponse; use ResolvesTeam; public function handle(Request $request): Response { - if ($error = $this->ensureAbility($request, 'read')) { + if ($error = $this->ensureAbility($request, 'read', $this->name)) { return $error; } $teamId = $this->resolveTeamId($request); if (is_null($teamId)) { - return Response::error('Invalid token.'); + return $this->mcpError($request, 'Invalid token.'); } $uuid = $request->get('uuid'); if (! is_string($uuid) || $uuid === '') { - return Response::error('uuid argument is required.'); + return $this->mcpError($request, 'uuid argument is required.'); } $service = Service::whereRelation('environment.project.team', 'id', $teamId) @@ -40,16 +40,16 @@ class GetService extends Tool ->first(); if (! $service) { - return Response::error("Service [{$uuid}] not found."); + return $this->mcpError($request, "Service [{$uuid}] not found.", ['resource_uuid' => $uuid]); } $service->setRelations([]); $service->makeHidden(['destination', 'source', 'environment', 'applications', 'databases', 'serviceApplications', 'serviceDatabases']); - return $this->respond( + return $this->mcpSuccess($request, $this->respond( $this->scrubSensitive($service->toArray()), $this->actionsForService($uuid, $service->status ?? null), - ); + ), ['resource_uuid' => $uuid]); } public function schema(JsonSchema $schema): array diff --git a/app/Mcp/Tools/ListApplications.php b/app/Mcp/Tools/ListApplications.php index 815edd61a..bf31131b2 100644 --- a/app/Mcp/Tools/ListApplications.php +++ b/app/Mcp/Tools/ListApplications.php @@ -8,31 +8,31 @@ use App\Models\Application; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Mcp\Request; use Laravel\Mcp\Response; -use Laravel\Mcp\Server\Attributes\Description; -use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Tool; -#[Name('list_applications')] -#[Description('List applications owned by the authenticated team. Returns summary (uuid, name, status, fqdn, git_repository). Optional "tag" argument filters by tag name. Use get_application for full details.')] class ListApplications extends Tool { + protected string $name = 'list_applications'; + + protected string $description = 'List applications owned by the authenticated team. Returns summary (uuid, name, status, fqdn, git_repository). Optional "tag" argument filters by tag name. Use get_application for full details.'; + use BuildsResponse; use ResolvesTeam; public function handle(Request $request): Response { - if ($error = $this->ensureAbility($request, 'read')) { + if ($error = $this->ensureAbility($request, 'read', $this->name)) { return $error; } $teamId = $this->resolveTeamId($request); if (is_null($teamId)) { - return Response::error('Invalid token.'); + return $this->mcpError($request, 'Invalid token.'); } $tagName = $request->get('tag'); if ($tagName !== null && (! is_string($tagName) || trim($tagName) === '')) { - return Response::error('tag argument must be a non-empty string.'); + return $this->mcpError($request, 'tag argument must be a non-empty string.'); } $args = $this->paginationArgs($request); @@ -59,11 +59,11 @@ class ListApplications extends Tool $extra = $tagName ? ['tag' => $tagName] : []; - return $this->respond( + return $this->mcpSuccess($request, $this->respond( $summaries, [], $this->paginationMeta('list_applications', $args, $total, $extra), - ); + )); } public function schema(JsonSchema $schema): array diff --git a/app/Mcp/Tools/ListDatabases.php b/app/Mcp/Tools/ListDatabases.php index 7eb1fde00..98de6ecee 100644 --- a/app/Mcp/Tools/ListDatabases.php +++ b/app/Mcp/Tools/ListDatabases.php @@ -8,26 +8,26 @@ use App\Models\Project; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Mcp\Request; use Laravel\Mcp\Response; -use Laravel\Mcp\Server\Attributes\Description; -use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Tool; -#[Name('list_databases')] -#[Description('List standalone databases owned by the authenticated team. Returns summary (uuid, name, status, type). Use get_database for full details.')] class ListDatabases extends Tool { + protected string $name = 'list_databases'; + + protected string $description = 'List standalone databases owned by the authenticated team. Returns summary (uuid, name, status, type). Use get_database for full details.'; + use BuildsResponse; use ResolvesTeam; public function handle(Request $request): Response { - if ($error = $this->ensureAbility($request, 'read')) { + if ($error = $this->ensureAbility($request, 'read', $this->name)) { return $error; } $teamId = $this->resolveTeamId($request); if (is_null($teamId)) { - return Response::error('Invalid token.'); + return $this->mcpError($request, 'Invalid token.'); } $args = $this->paginationArgs($request); @@ -52,11 +52,11 @@ class ListDatabases extends Tool ->values() ->all(); - return $this->respond( + return $this->mcpSuccess($request, $this->respond( $summaries, [], $this->paginationMeta('list_databases', $args, $total), - ); + )); } public function schema(JsonSchema $schema): array diff --git a/app/Mcp/Tools/ListProjects.php b/app/Mcp/Tools/ListProjects.php index 9ce1576b9..0a6de7f60 100644 --- a/app/Mcp/Tools/ListProjects.php +++ b/app/Mcp/Tools/ListProjects.php @@ -8,26 +8,26 @@ use App\Models\Project; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Mcp\Request; use Laravel\Mcp\Response; -use Laravel\Mcp\Server\Attributes\Description; -use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Tool; -#[Name('list_projects')] -#[Description('List projects owned by the authenticated team. Returns summary (uuid, name, description).')] class ListProjects extends Tool { + protected string $name = 'list_projects'; + + protected string $description = 'List projects owned by the authenticated team. Returns summary (uuid, name, description).'; + use BuildsResponse; use ResolvesTeam; public function handle(Request $request): Response { - if ($error = $this->ensureAbility($request, 'read')) { + if ($error = $this->ensureAbility($request, 'read', $this->name)) { return $error; } $teamId = $this->resolveTeamId($request); if (is_null($teamId)) { - return Response::error('Invalid token.'); + return $this->mcpError($request, 'Invalid token.'); } $args = $this->paginationArgs($request); @@ -49,11 +49,11 @@ class ListProjects extends Tool ->values() ->all(); - return $this->respond( + return $this->mcpSuccess($request, $this->respond( $summaries, [], $this->paginationMeta('list_projects', $args, $total), - ); + )); } public function schema(JsonSchema $schema): array diff --git a/app/Mcp/Tools/ListServers.php b/app/Mcp/Tools/ListServers.php index 20250c454..ed10afc93 100644 --- a/app/Mcp/Tools/ListServers.php +++ b/app/Mcp/Tools/ListServers.php @@ -8,26 +8,26 @@ use App\Models\Server; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Mcp\Request; use Laravel\Mcp\Response; -use Laravel\Mcp\Server\Attributes\Description; -use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Tool; -#[Name('list_servers')] -#[Description('List servers visible to the authenticated team token. Returns summary (uuid, name, ip, reachability). Use get_server for full details.')] class ListServers extends Tool { + protected string $name = 'list_servers'; + + protected string $description = 'List servers visible to the authenticated team token. Returns summary (uuid, name, ip, reachability). Use get_server for full details.'; + use BuildsResponse; use ResolvesTeam; public function handle(Request $request): Response { - if ($error = $this->ensureAbility($request, 'read')) { + if ($error = $this->ensureAbility($request, 'read', $this->name)) { return $error; } $teamId = $this->resolveTeamId($request); if (is_null($teamId)) { - return Response::error('Invalid token.'); + return $this->mcpError($request, 'Invalid token.'); } $args = $this->paginationArgs($request); @@ -50,11 +50,11 @@ class ListServers extends Tool ->values() ->all(); - return $this->respond( + return $this->mcpSuccess($request, $this->respond( $summaries, [], $this->paginationMeta('list_servers', $args, $total), - ); + )); } public function schema(JsonSchema $schema): array diff --git a/app/Mcp/Tools/ListServices.php b/app/Mcp/Tools/ListServices.php index b0bff4fad..3a0ea158a 100644 --- a/app/Mcp/Tools/ListServices.php +++ b/app/Mcp/Tools/ListServices.php @@ -8,26 +8,26 @@ use App\Models\Service; use Illuminate\Contracts\JsonSchema\JsonSchema; use Laravel\Mcp\Request; use Laravel\Mcp\Response; -use Laravel\Mcp\Server\Attributes\Description; -use Laravel\Mcp\Server\Attributes\Name; use Laravel\Mcp\Server\Tool; -#[Name('list_services')] -#[Description('List services (multi-container stacks) owned by the authenticated team. Returns summary (uuid, name, status). Use get_service for full details.')] class ListServices extends Tool { + protected string $name = 'list_services'; + + protected string $description = 'List services (multi-container stacks) owned by the authenticated team. Returns summary (uuid, name, status). Use get_service for full details.'; + use BuildsResponse; use ResolvesTeam; public function handle(Request $request): Response { - if ($error = $this->ensureAbility($request, 'read')) { + if ($error = $this->ensureAbility($request, 'read', $this->name)) { return $error; } $teamId = $this->resolveTeamId($request); if (is_null($teamId)) { - return Response::error('Invalid token.'); + return $this->mcpError($request, 'Invalid token.'); } $args = $this->paginationArgs($request); @@ -49,11 +49,11 @@ class ListServices extends Tool ->values() ->all(); - return $this->respond( + return $this->mcpSuccess($request, $this->respond( $summaries, [], $this->paginationMeta('list_services', $args, $total), - ); + )); } public function schema(JsonSchema $schema): array diff --git a/app/Models/Application.php b/app/Models/Application.php index a6f4b8892..ebfa2cd9b 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -23,7 +23,6 @@ use RuntimeException; use Spatie\Activitylog\Models\Activity; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; -use Visus\Cuid2\Cuid2; #[OA\Schema( description: 'Application model', @@ -1945,7 +1944,7 @@ class Application extends BaseModel if ($isInit && $this->docker_compose_raw) { return; } - $uuid = new Cuid2; + $uuid = new_public_id(); ['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: 'checkout'); $cloneCommand = str_replace(' clone ', ' clone --quiet ', $cloneCommand); $workdir = rtrim($this->base_directory, '/'); diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index 9159fd0d8..6e4b696d5 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -5,7 +5,6 @@ namespace App\Models; use App\Support\ValidationPatterns; use Illuminate\Database\Eloquent\SoftDeletes; use Spatie\Url\Url; -use Visus\Cuid2\Cuid2; class ApplicationPreview extends BaseModel { @@ -111,7 +110,7 @@ class ApplicationPreview extends BaseModel $port = $portInt !== null ? ':'.$portInt : ''; $urlPath = $url->getPath(); $path = ($urlPath !== '' && $urlPath !== '/') ? $urlPath : ''; - $random = new Cuid2; + $random = new_public_id(); $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); @@ -173,7 +172,7 @@ class ApplicationPreview extends BaseModel $port = $portInt !== null ? ':'.$portInt : ''; $urlPath = $url->getPath(); $path = ($urlPath !== '' && $urlPath !== '/') ? $urlPath : ''; - $random = new Cuid2; + $random = new_public_id(); $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 727abed5f..d657fbec4 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -4,7 +4,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; -use Visus\Cuid2\Cuid2; abstract class BaseModel extends Model { @@ -15,7 +14,7 @@ abstract class BaseModel extends Model static::creating(function (Model $model) { // Generate a UUID if one isn't set if (! $model->uuid) { - $model->uuid = (string) new Cuid2; + $model->uuid = new_public_id(); } }); } diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index ee330aa19..d16213ad7 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Traits\HasSafeStringAttribute; use DanHarrin\LivewireRateLimiting\WithRateLimiting; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; @@ -30,7 +31,7 @@ use phpseclib3\Crypt\PublicKeyLoader; )] class PrivateKey extends BaseModel { - use HasSafeStringAttribute, WithRateLimiting; + use HasFactory, HasSafeStringAttribute, WithRateLimiting; protected $fillable = [ 'name', diff --git a/app/Models/Project.php b/app/Models/Project.php index 236ef3a26..5c821b017 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -7,7 +7,6 @@ use App\Traits\HasSafeStringAttribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Support\Collection; use OpenApi\Attributes as OA; -use Visus\Cuid2\Cuid2; #[OA\Schema( description: 'Project model', @@ -60,7 +59,7 @@ class Project extends BaseModel Environment::create([ 'name' => 'production', 'project_id' => $project->id, - 'uuid' => (string) new Cuid2, + 'uuid' => new_public_id(), ]); }); static::deleting(function ($project) { diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php index 5e21ed3b1..69eb4fc48 100644 --- a/app/Models/S3Storage.php +++ b/app/Models/S3Storage.php @@ -181,21 +181,25 @@ class S3Storage extends BaseModel $exception = $this->toUserFriendlyConnectionException($e); $this->is_usable = false; if ($this->unusable_email_sent === false && is_transactional_emails_enabled()) { - $mail = new MailMessage; - $mail->subject('Coolify: S3 Storage Connection Error'); - $mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $exception->getMessage(), 'url' => route('storage.show', ['storage_uuid' => $this->uuid])]); + try { + $mail = new MailMessage; + $mail->subject('Coolify: S3 Storage Connection Error'); + $mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $exception->getMessage(), 'url' => route('storage.show', ['storage_uuid' => $this->uuid])]); - // Load the team with its members and their roles explicitly - $team = $this->team()->with(['members' => function ($query) { - $query->withPivot('role'); - }])->first(); + // Load the team with its members and their roles explicitly + $team = $this->team()->with(['members' => function ($query) { + $query->withPivot('role'); + }])->first(); - // Get admins directly from the pivot relationship for this specific team - $users = $team->members()->wherePivotIn('role', ['admin', 'owner'])->get(['users.id', 'users.email']); - foreach ($users as $user) { - send_user_an_email($mail, $user->email); + // Get admins directly from the pivot relationship for this specific team + $users = $team->members()->wherePivotIn('role', ['admin', 'owner'])->get(['users.id', 'users.email']); + foreach ($users as $user) { + send_user_an_email($mail, $user->email); + } + $this->unusable_email_sent = true; + } catch (\Throwable $emailException) { + \Log::warning('Failed to send S3 connection error notification: '.$emailException->getMessage()); } - $this->unusable_email_sent = true; } throw $exception; diff --git a/app/Models/Server.php b/app/Models/Server.php index 20a4ee95b..7dd2bd472 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -37,7 +37,6 @@ use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\Url\Url; use Stevebauman\Purify\Facades\Purify; use Symfony\Component\Yaml\Yaml; -use Visus\Cuid2\Cuid2; /** * @property array{ @@ -1051,7 +1050,7 @@ $schema://$host { { $attributes = [ 'name' => 'coolify', - 'uuid' => (string) new Cuid2, + 'uuid' => new_public_id(), 'network' => 'coolify', 'server_id' => $this->id, ]; diff --git a/app/Models/Service.php b/app/Models/Service.php index a4ceb01f1..d2de68c7d 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -16,7 +16,6 @@ use OpenApi\Attributes as OA; use Spatie\Activitylog\Models\Activity; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; -use Visus\Cuid2\Cuid2; #[OA\Schema( description: 'Service model', @@ -82,7 +81,7 @@ class Service extends BaseModel { static::creating(function ($service) { if (blank($service->name)) { - $service->name = 'service-'.(new Cuid2); + $service->name = 'service-'.new_public_id(); } }); static::created(function ($service) { @@ -1575,7 +1574,7 @@ class Service extends BaseModel "cd $workdir", ], $this->server); - $filename = new Cuid2.'-docker-compose.yml'; + $filename = new_public_id().'-docker-compose.yml'; Storage::disk('local')->put("tmp/{$filename}", $this->docker_compose); $path = Storage::path("tmp/{$filename}"); instant_scp($path, "{$workdir}/docker-compose.yml", $this->server); diff --git a/app/Models/Team.php b/app/Models/Team.php index f0a50cf69..23e2badb3 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -66,7 +66,7 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen $team->webhookNotificationSettings()->create(); }); - static::saving(function ($team) { + static::updating(function ($team) { if (auth()->user()?->isMember()) { throw new \Exception('You are not allowed to update this team.'); } diff --git a/app/Models/User.php b/app/Models/User.php index 9cbe88835..b59b553d9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -349,6 +349,11 @@ class User extends Authenticatable implements SendsEmail { $sessionTeamId = data_get(session('currentTeam'), 'id'); + // Fallback for stateless API requests: resolve team from Sanctum token + if (is_null($sessionTeamId) && $this->currentAccessToken()) { + $sessionTeamId = data_get($this->currentAccessToken(), 'team_id'); + } + if (is_null($sessionTeamId)) { return null; } diff --git a/app/Policies/ApiTokenPolicy.php b/app/Policies/ApiTokenPolicy.php index 761227118..ba9bade01 100644 --- a/app/Policies/ApiTokenPolicy.php +++ b/app/Policies/ApiTokenPolicy.php @@ -12,11 +12,6 @@ class ApiTokenPolicy */ public function viewAny(User $user): bool { - // Authorization temporarily disabled - /* - // Users can view their own API tokens - return true; - */ return true; } @@ -25,12 +20,7 @@ class ApiTokenPolicy */ public function view(User $user, PersonalAccessToken $token): bool { - // Authorization temporarily disabled - /* - // Users can only view their own tokens return $user->id === $token->tokenable_id && $token->tokenable_type === User::class; - */ - return true; } /** @@ -38,11 +28,6 @@ class ApiTokenPolicy */ public function create(User $user): bool { - // Authorization temporarily disabled - /* - // All authenticated users can create their own API tokens - return true; - */ return true; } @@ -51,12 +36,7 @@ class ApiTokenPolicy */ public function update(User $user, PersonalAccessToken $token): bool { - // Authorization temporarily disabled - /* - // Users can only update their own tokens return $user->id === $token->tokenable_id && $token->tokenable_type === User::class; - */ - return true; } /** @@ -64,12 +44,7 @@ class ApiTokenPolicy */ public function delete(User $user, PersonalAccessToken $token): bool { - // Authorization temporarily disabled - /* - // Users can only delete their own tokens return $user->id === $token->tokenable_id && $token->tokenable_type === User::class; - */ - return true; } /** @@ -77,11 +52,6 @@ class ApiTokenPolicy */ public function manage(User $user): bool { - // Authorization temporarily disabled - /* - // All authenticated users can manage their own API tokens - return true; - */ return true; } @@ -90,7 +60,6 @@ class ApiTokenPolicy */ public function useRootPermissions(User $user): bool { - // Only admins and owners can use root permissions return $user->isAdmin() || $user->isOwner(); } @@ -99,11 +68,22 @@ class ApiTokenPolicy */ public function useWritePermissions(User $user): bool { - // Authorization temporarily disabled - /* - // Only admins and owners can use write permissions return $user->isAdmin() || $user->isOwner(); - */ - return true; + } + + /** + * Determine whether the user can use deploy permissions for API tokens. + */ + public function useDeployPermissions(User $user): bool + { + return $user->isAdmin() || $user->isOwner(); + } + + /** + * Determine whether the user can use read:sensitive permissions for API tokens. + */ + public function useSensitivePermissions(User $user): bool + { + return $user->isAdmin() || $user->isOwner(); } } diff --git a/app/Policies/ApplicationPolicy.php b/app/Policies/ApplicationPolicy.php index d64a436ad..1a1290b55 100644 --- a/app/Policies/ApplicationPolicy.php +++ b/app/Policies/ApplicationPolicy.php @@ -13,10 +13,6 @@ class ApplicationPolicy */ public function viewAny(User $user): bool { - // Authorization temporarily disabled - /* - return true; - */ return true; } @@ -25,11 +21,9 @@ class ApplicationPolicy */ public function view(User $user, Application $application): bool { - // Authorization temporarily disabled - /* - return true; - */ - return true; + $teamId = $this->getTeamId($application); + + return $teamId !== null && $user->teams->contains('id', $teamId); } /** @@ -37,15 +31,7 @@ class ApplicationPolicy */ public function create(User $user): bool { - // Authorization temporarily disabled - /* - if ($user->isAdmin()) { - return true; - } - - return false; - */ - return true; + return $user->isAdmin(); } /** @@ -53,15 +39,17 @@ class ApplicationPolicy */ public function update(User $user, Application $application): Response { - // Authorization temporarily disabled - /* - if ($user->isAdmin()) { + $teamId = $this->getTeamId($application); + + if ($teamId === null) { + return Response::deny('Application team not found.'); + } + + if ($user->isAdminOfTeam($teamId)) { return Response::allow(); } - return Response::deny('As a member, you cannot update this application.

You need at least admin or owner permissions.'); - */ - return Response::allow(); + return Response::deny('You need at least admin or owner permissions to update this application.'); } /** @@ -69,15 +57,9 @@ class ApplicationPolicy */ public function delete(User $user, Application $application): bool { - // Authorization temporarily disabled - /* - if ($user->isAdmin()) { - return true; - } + $teamId = $this->getTeamId($application); - return false; - */ - return true; + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -85,11 +67,7 @@ class ApplicationPolicy */ public function restore(User $user, Application $application): bool { - // Authorization temporarily disabled - /* - return true; - */ - return true; + return false; } /** @@ -97,11 +75,25 @@ class ApplicationPolicy */ public function forceDelete(User $user, Application $application): bool { - // Authorization temporarily disabled - /* - return $user->isAdmin() && $user->teams->contains('id', $application->team()->first()->id); - */ - return true; + return false; + } + + /** + * Determine whether the user can upload a backup archive for this application. + */ + public function uploadBackup(User $user, Application $application): Response + { + $teamId = $this->getTeamId($application); + + if ($teamId === null) { + return Response::deny('Application team not found.'); + } + + if ($user->isAdminOfTeam($teamId)) { + return Response::allow(); + } + + return Response::deny('You need at least admin or owner permissions to upload backups for this application.'); } /** @@ -109,11 +101,9 @@ class ApplicationPolicy */ public function deploy(User $user, Application $application): bool { - // Authorization temporarily disabled - /* - return $user->teams->contains('id', $application->team()->first()->id); - */ - return true; + $teamId = $this->getTeamId($application); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -121,11 +111,9 @@ class ApplicationPolicy */ public function manageDeployments(User $user, Application $application): bool { - // Authorization temporarily disabled - /* - return $user->isAdmin() && $user->teams->contains('id', $application->team()->first()->id); - */ - return true; + $teamId = $this->getTeamId($application); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -133,11 +121,9 @@ class ApplicationPolicy */ public function manageEnvironment(User $user, Application $application): bool { - // Authorization temporarily disabled - /* - return $user->isAdmin() && $user->teams->contains('id', $application->team()->first()->id); - */ - return true; + $teamId = $this->getTeamId($application); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -145,10 +131,11 @@ class ApplicationPolicy */ public function cleanupDeploymentQueue(User $user): bool { - // Authorization temporarily disabled - /* return $user->isAdmin(); - */ - return true; + } + + private function getTeamId(Application $application): ?int + { + return $application->team()?->id; } } diff --git a/app/Policies/ApplicationPreviewPolicy.php b/app/Policies/ApplicationPreviewPolicy.php index 4d371cc38..f3c13acd9 100644 --- a/app/Policies/ApplicationPreviewPolicy.php +++ b/app/Policies/ApplicationPreviewPolicy.php @@ -21,8 +21,9 @@ class ApplicationPreviewPolicy */ public function view(User $user, ApplicationPreview $applicationPreview): bool { - // return $user->teams->contains('id', $applicationPreview->application->team()->first()->id); - return true; + $teamId = $this->getTeamId($applicationPreview); + + return $teamId !== null && $user->teams->contains('id', $teamId); } /** @@ -30,21 +31,25 @@ class ApplicationPreviewPolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** * Determine whether the user can update the model. */ - public function update(User $user, ApplicationPreview $applicationPreview) + public function update(User $user, ApplicationPreview $applicationPreview): Response { - // if ($user->isAdmin()) { - // return Response::allow(); - // } + $teamId = $this->getTeamId($applicationPreview); - // return Response::deny('As a member, you cannot update this preview.

You need at least admin or owner permissions.'); - return true; + if ($teamId === null) { + return Response::deny('Application preview team not found.'); + } + + if ($user->isAdminOfTeam($teamId)) { + return Response::allow(); + } + + return Response::deny('You need at least admin or owner permissions to update this preview.'); } /** @@ -52,8 +57,9 @@ class ApplicationPreviewPolicy */ public function delete(User $user, ApplicationPreview $applicationPreview): bool { - // return $user->isAdmin() && $user->teams->contains('id', $applicationPreview->application->team()->first()->id); - return true; + $teamId = $this->getTeamId($applicationPreview); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -61,8 +67,7 @@ class ApplicationPreviewPolicy */ public function restore(User $user, ApplicationPreview $applicationPreview): bool { - // return $user->isAdmin() && $user->teams->contains('id', $applicationPreview->application->team()->first()->id); - return true; + return false; } /** @@ -70,8 +75,7 @@ class ApplicationPreviewPolicy */ public function forceDelete(User $user, ApplicationPreview $applicationPreview): bool { - // return $user->isAdmin() && $user->teams->contains('id', $applicationPreview->application->team()->first()->id); - return true; + return false; } /** @@ -79,8 +83,9 @@ class ApplicationPreviewPolicy */ public function deploy(User $user, ApplicationPreview $applicationPreview): bool { - // return $user->teams->contains('id', $applicationPreview->application->team()->first()->id); - return true; + $teamId = $this->getTeamId($applicationPreview); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -88,7 +93,13 @@ class ApplicationPreviewPolicy */ public function manageDeployments(User $user, ApplicationPreview $applicationPreview): bool { - // return $user->isAdmin() && $user->teams->contains('id', $applicationPreview->application->team()->first()->id); - return true; + $teamId = $this->getTeamId($applicationPreview); + + return $teamId !== null && $user->isAdminOfTeam($teamId); + } + + private function getTeamId(ApplicationPreview $applicationPreview): ?int + { + return $applicationPreview->application?->team()?->id; } } diff --git a/app/Policies/ApplicationSettingPolicy.php b/app/Policies/ApplicationSettingPolicy.php index 848dc9aee..be2137cb8 100644 --- a/app/Policies/ApplicationSettingPolicy.php +++ b/app/Policies/ApplicationSettingPolicy.php @@ -20,8 +20,9 @@ class ApplicationSettingPolicy */ public function view(User $user, ApplicationSetting $applicationSetting): bool { - // return $user->teams->contains('id', $applicationSetting->application->team()->first()->id); - return true; + $teamId = $this->getTeamId($applicationSetting); + + return $teamId !== null && $user->teams->contains('id', $teamId); } /** @@ -29,8 +30,7 @@ class ApplicationSettingPolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -38,8 +38,9 @@ class ApplicationSettingPolicy */ public function update(User $user, ApplicationSetting $applicationSetting): bool { - // return $user->isAdmin() && $user->teams->contains('id', $applicationSetting->application->team()->first()->id); - return true; + $teamId = $this->getTeamId($applicationSetting); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -47,8 +48,9 @@ class ApplicationSettingPolicy */ public function delete(User $user, ApplicationSetting $applicationSetting): bool { - // return $user->isAdmin() && $user->teams->contains('id', $applicationSetting->application->team()->first()->id); - return true; + $teamId = $this->getTeamId($applicationSetting); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -56,8 +58,7 @@ class ApplicationSettingPolicy */ public function restore(User $user, ApplicationSetting $applicationSetting): bool { - // return $user->isAdmin() && $user->teams->contains('id', $applicationSetting->application->team()->first()->id); - return true; + return false; } /** @@ -65,7 +66,11 @@ class ApplicationSettingPolicy */ public function forceDelete(User $user, ApplicationSetting $applicationSetting): bool { - // return $user->isAdmin() && $user->teams->contains('id', $applicationSetting->application->team()->first()->id); - return true; + return false; + } + + private function getTeamId(ApplicationSetting $applicationSetting): ?int + { + return $applicationSetting->application?->team()?->id; } } diff --git a/app/Policies/DatabasePolicy.php b/app/Policies/DatabasePolicy.php index f8e8af637..4217432b5 100644 --- a/app/Policies/DatabasePolicy.php +++ b/app/Policies/DatabasePolicy.php @@ -20,8 +20,9 @@ class DatabasePolicy */ public function view(User $user, $database): bool { - // return $user->teams->contains('id', $database->team()->first()->id); - return true; + $teamId = $this->getTeamId($database); + + return $teamId !== null && $user->teams->contains('id', $teamId); } /** @@ -29,21 +30,25 @@ class DatabasePolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** * Determine whether the user can update the model. */ - public function update(User $user, $database) + public function update(User $user, $database): Response { - // if ($user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id)) { - // return Response::allow(); - // } + $teamId = $this->getTeamId($database); - // return Response::deny('As a member, you cannot update this database.

You need at least admin or owner permissions.'); - return true; + if ($teamId === null) { + return Response::deny('Database team not found.'); + } + + if ($user->isAdminOfTeam($teamId)) { + return Response::allow(); + } + + return Response::deny('You need at least admin or owner permissions to update this database.'); } /** @@ -51,8 +56,9 @@ class DatabasePolicy */ public function delete(User $user, $database): bool { - // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id); - return true; + $teamId = $this->getTeamId($database); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -60,8 +66,7 @@ class DatabasePolicy */ public function restore(User $user, $database): bool { - // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id); - return true; + return false; } /** @@ -69,8 +74,7 @@ class DatabasePolicy */ public function forceDelete(User $user, $database): bool { - // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id); - return true; + return false; } /** @@ -78,8 +82,27 @@ class DatabasePolicy */ public function manage(User $user, $database): bool { - // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id); - return true; + $teamId = $this->getTeamId($database); + + return $teamId !== null && $user->isAdminOfTeam($teamId); + } + + /** + * Determine whether the user can upload a backup archive for this database. + */ + public function uploadBackup(User $user, $database): Response + { + $teamId = $this->getTeamId($database); + + if ($teamId === null) { + return Response::deny('Database team not found.'); + } + + if ($user->isAdminOfTeam($teamId)) { + return Response::allow(); + } + + return Response::deny('You need at least admin or owner permissions to upload backups for this database.'); } /** @@ -87,8 +110,9 @@ class DatabasePolicy */ public function manageBackups(User $user, $database): bool { - // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id); - return true; + $teamId = $this->getTeamId($database); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -96,7 +120,22 @@ class DatabasePolicy */ public function manageEnvironment(User $user, $database): bool { - // return $user->isAdmin() && $user->teams->contains('id', $database->team()->first()->id); - return true; + $teamId = $this->getTeamId($database); + + return $teamId !== null && $user->isAdminOfTeam($teamId); + } + + private function getTeamId($database): ?int + { + // Instance-level databases (e.g., coolify-db) belong to root team + if (isset($database->id) && $database->id === 0) { + return 0; + } + + if (method_exists($database, 'team')) { + return $database->team()?->id; + } + + return null; } } diff --git a/app/Policies/EnvironmentPolicy.php b/app/Policies/EnvironmentPolicy.php index 7199abb25..e400ec903 100644 --- a/app/Policies/EnvironmentPolicy.php +++ b/app/Policies/EnvironmentPolicy.php @@ -20,8 +20,9 @@ class EnvironmentPolicy */ public function view(User $user, Environment $environment): bool { - // return $user->teams->contains('id', $environment->project->team_id); - return true; + $teamId = $this->getTeamId($environment); + + return $teamId !== null && $user->teams->contains('id', $teamId); } /** @@ -29,8 +30,7 @@ class EnvironmentPolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -38,8 +38,9 @@ class EnvironmentPolicy */ public function update(User $user, Environment $environment): bool { - // return $user->isAdmin() && $user->teams->contains('id', $environment->project->team_id); - return true; + $teamId = $this->getTeamId($environment); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -47,8 +48,9 @@ class EnvironmentPolicy */ public function delete(User $user, Environment $environment): bool { - // return $user->isAdmin() && $user->teams->contains('id', $environment->project->team_id); - return true; + $teamId = $this->getTeamId($environment); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -56,8 +58,7 @@ class EnvironmentPolicy */ public function restore(User $user, Environment $environment): bool { - // return $user->isAdmin() && $user->teams->contains('id', $environment->project->team_id); - return true; + return false; } /** @@ -65,7 +66,11 @@ class EnvironmentPolicy */ public function forceDelete(User $user, Environment $environment): bool { - // return $user->isAdmin() && $user->teams->contains('id', $environment->project->team_id); - return true; + return false; + } + + private function getTeamId(Environment $environment): ?int + { + return $environment->project?->team_id; } } diff --git a/app/Policies/EnvironmentVariablePolicy.php b/app/Policies/EnvironmentVariablePolicy.php index 21e2ea443..dd0f58918 100644 --- a/app/Policies/EnvironmentVariablePolicy.php +++ b/app/Policies/EnvironmentVariablePolicy.php @@ -20,7 +20,9 @@ class EnvironmentVariablePolicy */ public function view(User $user, EnvironmentVariable $environmentVariable): bool { - return true; + $teamId = $this->getTeamId($environmentVariable); + + return $teamId !== null && $user->teams->contains('id', $teamId); } /** @@ -28,7 +30,7 @@ class EnvironmentVariablePolicy */ public function create(User $user): bool { - return true; + return $user->isAdmin(); } /** @@ -36,7 +38,9 @@ class EnvironmentVariablePolicy */ public function update(User $user, EnvironmentVariable $environmentVariable): bool { - return true; + $teamId = $this->getTeamId($environmentVariable); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -44,7 +48,9 @@ class EnvironmentVariablePolicy */ public function delete(User $user, EnvironmentVariable $environmentVariable): bool { - return true; + $teamId = $this->getTeamId($environmentVariable); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -52,7 +58,7 @@ class EnvironmentVariablePolicy */ public function restore(User $user, EnvironmentVariable $environmentVariable): bool { - return true; + return false; } /** @@ -60,7 +66,7 @@ class EnvironmentVariablePolicy */ public function forceDelete(User $user, EnvironmentVariable $environmentVariable): bool { - return true; + return false; } /** @@ -68,6 +74,19 @@ class EnvironmentVariablePolicy */ public function manageEnvironment(User $user, EnvironmentVariable $environmentVariable): bool { - return true; + $teamId = $this->getTeamId($environmentVariable); + + return $teamId !== null && $user->isAdminOfTeam($teamId); + } + + private function getTeamId(EnvironmentVariable $environmentVariable): ?int + { + $resource = $environmentVariable->resourceable; + + if (! $resource || ! method_exists($resource, 'team')) { + return null; + } + + return $resource->team()?->id; } } diff --git a/app/Policies/GithubAppPolicy.php b/app/Policies/GithubAppPolicy.php index 56bec7032..79dd79838 100644 --- a/app/Policies/GithubAppPolicy.php +++ b/app/Policies/GithubAppPolicy.php @@ -20,8 +20,11 @@ class GithubAppPolicy */ public function view(User $user, GithubApp $githubApp): bool { - // return $user->teams->contains('id', $githubApp->team_id) || $githubApp->is_system_wide; - return true; + if ($githubApp->is_system_wide) { + return true; + } + + return $user->teams->contains('id', $githubApp->team_id); } /** @@ -29,8 +32,7 @@ class GithubAppPolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -39,12 +41,10 @@ class GithubAppPolicy public function update(User $user, GithubApp $githubApp): bool { if ($githubApp->is_system_wide) { - // return $user->isAdmin(); - return true; + return $user->canAccessSystemResources(); } - // return $user->isAdmin() && $user->teams->contains('id', $githubApp->team_id); - return true; + return $user->isAdminOfTeam($githubApp->team_id); } /** @@ -53,12 +53,10 @@ class GithubAppPolicy public function delete(User $user, GithubApp $githubApp): bool { if ($githubApp->is_system_wide) { - // return $user->isAdmin(); - return true; + return $user->canAccessSystemResources(); } - // return $user->isAdmin() && $user->teams->contains('id', $githubApp->team_id); - return true; + return $user->isAdminOfTeam($githubApp->team_id); } /** diff --git a/app/Policies/NotificationPolicy.php b/app/Policies/NotificationPolicy.php index 4f3be431d..e8764bf13 100644 --- a/app/Policies/NotificationPolicy.php +++ b/app/Policies/NotificationPolicy.php @@ -12,13 +12,11 @@ class NotificationPolicy */ public function view(User $user, Model $notificationSettings): bool { - // Check if the notification settings belong to the user's current team if (! $notificationSettings->team) { return false; } - // return $user->teams()->where('teams.id', $notificationSettings->team->id)->exists(); - return true; + return $user->teams->contains('id', $notificationSettings->team->id); } /** @@ -26,14 +24,13 @@ class NotificationPolicy */ public function update(User $user, Model $notificationSettings): bool { - // Check if the notification settings belong to the user's current team if (! $notificationSettings->team) { return false; } - // Only owners and admins can update notification settings - // return $user->isAdmin() || $user->isOwner(); - return true; + $teamId = $notificationSettings->team->id; + + return $user->isAdminOfTeam($teamId); } /** @@ -41,8 +38,7 @@ class NotificationPolicy */ public function manage(User $user, Model $notificationSettings): bool { - // return $this->update($user, $notificationSettings); - return true; + return $this->update($user, $notificationSettings); } /** @@ -50,7 +46,6 @@ class NotificationPolicy */ public function sendTest(User $user, Model $notificationSettings): bool { - // return $this->update($user, $notificationSettings); - return true; + return $this->update($user, $notificationSettings); } } diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php index e188c293f..9d65b9130 100644 --- a/app/Policies/ProjectPolicy.php +++ b/app/Policies/ProjectPolicy.php @@ -20,8 +20,7 @@ class ProjectPolicy */ public function view(User $user, Project $project): bool { - // return $user->teams->contains('id', $project->team_id); - return true; + return $user->teams->contains('id', $project->team_id); } /** @@ -29,8 +28,7 @@ class ProjectPolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -38,8 +36,7 @@ class ProjectPolicy */ public function update(User $user, Project $project): bool { - // return $user->isAdmin() && $user->teams->contains('id', $project->team_id); - return true; + return $user->isAdminOfTeam($project->team_id); } /** @@ -47,8 +44,7 @@ class ProjectPolicy */ public function delete(User $user, Project $project): bool { - // return $user->isAdmin() && $user->teams->contains('id', $project->team_id); - return true; + return $user->isAdminOfTeam($project->team_id); } /** @@ -56,8 +52,7 @@ class ProjectPolicy */ public function restore(User $user, Project $project): bool { - // return $user->isAdmin() && $user->teams->contains('id', $project->team_id); - return true; + return false; } /** @@ -65,7 +60,6 @@ class ProjectPolicy */ public function forceDelete(User $user, Project $project): bool { - // return $user->isAdmin() && $user->teams->contains('id', $project->team_id); - return true; + return false; } } diff --git a/app/Policies/ResourceCreatePolicy.php b/app/Policies/ResourceCreatePolicy.php index 9ed2b66ab..a7a855402 100644 --- a/app/Policies/ResourceCreatePolicy.php +++ b/app/Policies/ResourceCreatePolicy.php @@ -38,8 +38,7 @@ class ResourceCreatePolicy */ public function createAny(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -51,8 +50,7 @@ class ResourceCreatePolicy return false; } - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** diff --git a/app/Policies/S3StoragePolicy.php b/app/Policies/S3StoragePolicy.php index 982c7c523..81cbd164f 100644 --- a/app/Policies/S3StoragePolicy.php +++ b/app/Policies/S3StoragePolicy.php @@ -36,8 +36,7 @@ class S3StoragePolicy */ public function update(User $user, S3Storage $storage): bool { - // return $user->teams->contains('id', $storage->team_id) && $user->isAdmin(); - return $user->teams->contains('id', $storage->team_id); + return $user->teams->contains('id', $storage->team_id) && $user->isAdminOfTeam($storage->team_id); } /** @@ -45,8 +44,7 @@ class S3StoragePolicy */ public function delete(User $user, S3Storage $storage): bool { - // return $user->teams->contains('id', $storage->team_id) && $user->isAdmin(); - return $user->teams->contains('id', $storage->team_id); + return $user->teams->contains('id', $storage->team_id) && $user->isAdminOfTeam($storage->team_id); } /** @@ -70,6 +68,6 @@ class S3StoragePolicy */ public function validateConnection(User $user, S3Storage $storage): bool { - return $user->teams->contains('id', $storage->team_id); + return $user->teams->contains('id', $storage->team_id) && $user->isAdminOfTeam($storage->team_id); } } diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index 6d2396a7d..c1369ce67 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -28,8 +28,7 @@ class ServerPolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -37,8 +36,7 @@ class ServerPolicy */ public function update(User $user, Server $server): bool { - // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); - return true; + return $user->isAdminOfTeam($server->team_id); } /** @@ -46,8 +44,7 @@ class ServerPolicy */ public function delete(User $user, Server $server): bool { - // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); - return true; + return $user->isAdminOfTeam($server->team_id); } /** @@ -71,8 +68,7 @@ class ServerPolicy */ public function manageProxy(User $user, Server $server): bool { - // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); - return true; + return $user->isAdminOfTeam($server->team_id); } /** @@ -80,8 +76,15 @@ class ServerPolicy */ public function manageSentinel(User $user, Server $server): bool { - // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); - return true; + return $user->isAdminOfTeam($server->team_id); + } + + /** + * Determine whether the user can view Sentinel configuration and logs. + */ + public function viewSentinel(User $user, Server $server): bool + { + return $user->isAdminOfTeam($server->team_id); } /** @@ -89,8 +92,7 @@ class ServerPolicy */ public function manageCaCertificate(User $user, Server $server): bool { - // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); - return true; + return $user->isAdminOfTeam($server->team_id); } /** @@ -98,7 +100,6 @@ class ServerPolicy */ public function viewSecurity(User $user, Server $server): bool { - // return $user->isAdmin() && $user->teams->contains('id', $server->team_id); - return true; + return $user->isAdminOfTeam($server->team_id); } } diff --git a/app/Policies/ServiceApplicationPolicy.php b/app/Policies/ServiceApplicationPolicy.php index af380a90f..c730ab0c6 100644 --- a/app/Policies/ServiceApplicationPolicy.php +++ b/app/Policies/ServiceApplicationPolicy.php @@ -21,8 +21,7 @@ class ServiceApplicationPolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -30,8 +29,7 @@ class ServiceApplicationPolicy */ public function update(User $user, ServiceApplication $serviceApplication): bool { - // return Gate::allows('update', $serviceApplication->service); - return true; + return Gate::allows('update', $serviceApplication->service); } /** @@ -39,8 +37,7 @@ class ServiceApplicationPolicy */ public function delete(User $user, ServiceApplication $serviceApplication): bool { - // return Gate::allows('delete', $serviceApplication->service); - return true; + return Gate::allows('delete', $serviceApplication->service); } /** @@ -48,8 +45,7 @@ class ServiceApplicationPolicy */ public function restore(User $user, ServiceApplication $serviceApplication): bool { - // return Gate::allows('update', $serviceApplication->service); - return true; + return false; } /** @@ -57,7 +53,6 @@ class ServiceApplicationPolicy */ public function forceDelete(User $user, ServiceApplication $serviceApplication): bool { - // return Gate::allows('delete', $serviceApplication->service); - return true; + return false; } } diff --git a/app/Policies/ServiceDatabasePolicy.php b/app/Policies/ServiceDatabasePolicy.php index f72f1f327..e94658e29 100644 --- a/app/Policies/ServiceDatabasePolicy.php +++ b/app/Policies/ServiceDatabasePolicy.php @@ -13,7 +13,7 @@ class ServiceDatabasePolicy */ public function view(User $user, ServiceDatabase $serviceDatabase): bool { - return true; + return Gate::allows('view', $serviceDatabase->service); } /** @@ -21,8 +21,7 @@ class ServiceDatabasePolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -30,9 +29,7 @@ class ServiceDatabasePolicy */ public function update(User $user, ServiceDatabase $serviceDatabase): bool { - - // return Gate::allows('update', $serviceDatabase->service); - return true; + return Gate::allows('update', $serviceDatabase->service); } /** @@ -40,8 +37,7 @@ class ServiceDatabasePolicy */ public function delete(User $user, ServiceDatabase $serviceDatabase): bool { - // return Gate::allows('delete', $serviceDatabase->service); - return true; + return Gate::allows('delete', $serviceDatabase->service); } /** @@ -49,8 +45,7 @@ class ServiceDatabasePolicy */ public function restore(User $user, ServiceDatabase $serviceDatabase): bool { - // return Gate::allows('update', $serviceDatabase->service); - return true; + return false; } /** @@ -58,12 +53,22 @@ class ServiceDatabasePolicy */ public function forceDelete(User $user, ServiceDatabase $serviceDatabase): bool { - // return Gate::allows('delete', $serviceDatabase->service); - return true; + return false; } + /** + * Determine whether the user can manage database backups. + */ public function manageBackups(User $user, ServiceDatabase $serviceDatabase): bool { - return true; + return Gate::allows('update', $serviceDatabase->service); + } + + /** + * Determine whether the user can upload a backup archive for this service database. + */ + public function uploadBackup(User $user, ServiceDatabase $serviceDatabase): bool + { + return Gate::allows('uploadBackup', $serviceDatabase->service); } } diff --git a/app/Policies/ServicePolicy.php b/app/Policies/ServicePolicy.php index 7ab0fe7d0..6ca79b42a 100644 --- a/app/Policies/ServicePolicy.php +++ b/app/Policies/ServicePolicy.php @@ -20,7 +20,9 @@ class ServicePolicy */ public function view(User $user, Service $service): bool { - return true; + $teamId = $this->getTeamId($service); + + return $teamId !== null && $user->teams->contains('id', $teamId); } /** @@ -28,8 +30,7 @@ class ServicePolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -37,13 +38,9 @@ class ServicePolicy */ public function update(User $user, Service $service): bool { - $team = $service->team(); - if (! $team) { - return false; - } + $teamId = $this->getTeamId($service); - // return $user->isAdmin() && $user->teams->contains('id', $team->id); - return true; + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -51,12 +48,9 @@ class ServicePolicy */ public function delete(User $user, Service $service): bool { - // if ($user->isAdmin()) { - // return true; - // } + $teamId = $this->getTeamId($service); - // return false; - return true; + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -64,8 +58,7 @@ class ServicePolicy */ public function restore(User $user, Service $service): bool { - // return true; - return true; + return false; } /** @@ -73,23 +66,17 @@ class ServicePolicy */ public function forceDelete(User $user, Service $service): bool { - // if ($user->isAdmin()) { - // return true; - // } - - // return false; - return true; + return false; } + /** + * Determine whether the user can stop the service. + */ public function stop(User $user, Service $service): bool { - $team = $service->team(); - if (! $team) { - return false; - } + $teamId = $this->getTeamId($service); - // return $user->teams->contains('id', $team->id); - return true; + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -97,13 +84,19 @@ class ServicePolicy */ public function manageEnvironment(User $user, Service $service): bool { - $team = $service->team(); - if (! $team) { - return false; - } + $teamId = $this->getTeamId($service); - // return $user->isAdmin() && $user->teams->contains('id', $team->id); - return true; + return $teamId !== null && $user->isAdminOfTeam($teamId); + } + + /** + * Determine whether the user can upload a backup archive for a database within this service. + */ + public function uploadBackup(User $user, Service $service): bool + { + $teamId = $this->getTeamId($service); + + return $teamId !== null && $user->isAdminOfTeam($teamId); } /** @@ -111,18 +104,23 @@ class ServicePolicy */ public function deploy(User $user, Service $service): bool { - $team = $service->team(); - if (! $team) { - return false; - } + $teamId = $this->getTeamId($service); - // return $user->teams->contains('id', $team->id); - return true; + return $teamId !== null && $user->isAdminOfTeam($teamId); } + /** + * Determine whether the user can access the terminal. + */ public function accessTerminal(User $user, Service $service): bool { - // return $user->isAdmin() || $user->teams->contains('id', $service->team()->id); - return true; + $teamId = $this->getTeamId($service); + + return $teamId !== null && $user->isAdminOfTeam($teamId); + } + + private function getTeamId(Service $service): ?int + { + return $service->team()?->id; } } diff --git a/app/Policies/SharedEnvironmentVariablePolicy.php b/app/Policies/SharedEnvironmentVariablePolicy.php index b465d8a0c..21b6acb27 100644 --- a/app/Policies/SharedEnvironmentVariablePolicy.php +++ b/app/Policies/SharedEnvironmentVariablePolicy.php @@ -28,8 +28,7 @@ class SharedEnvironmentVariablePolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -37,8 +36,7 @@ class SharedEnvironmentVariablePolicy */ public function update(User $user, SharedEnvironmentVariable $sharedEnvironmentVariable): bool { - // return $user->isAdmin() && $user->teams->contains('id', $sharedEnvironmentVariable->team_id); - return true; + return $user->isAdminOfTeam($sharedEnvironmentVariable->team_id); } /** @@ -46,8 +44,7 @@ class SharedEnvironmentVariablePolicy */ public function delete(User $user, SharedEnvironmentVariable $sharedEnvironmentVariable): bool { - // return $user->isAdmin() && $user->teams->contains('id', $sharedEnvironmentVariable->team_id); - return true; + return $user->isAdminOfTeam($sharedEnvironmentVariable->team_id); } /** @@ -55,8 +52,7 @@ class SharedEnvironmentVariablePolicy */ public function restore(User $user, SharedEnvironmentVariable $sharedEnvironmentVariable): bool { - // return $user->isAdmin() && $user->teams->contains('id', $sharedEnvironmentVariable->team_id); - return true; + return false; } /** @@ -64,8 +60,7 @@ class SharedEnvironmentVariablePolicy */ public function forceDelete(User $user, SharedEnvironmentVariable $sharedEnvironmentVariable): bool { - // return $user->isAdmin() && $user->teams->contains('id', $sharedEnvironmentVariable->team_id); - return true; + return false; } /** @@ -73,7 +68,6 @@ class SharedEnvironmentVariablePolicy */ public function manageEnvironment(User $user, SharedEnvironmentVariable $sharedEnvironmentVariable): bool { - // return $user->isAdmin() && $user->teams->contains('id', $sharedEnvironmentVariable->team_id); - return true; + return $user->isAdminOfTeam($sharedEnvironmentVariable->team_id); } } diff --git a/app/Policies/StandaloneDockerPolicy.php b/app/Policies/StandaloneDockerPolicy.php index 3e1f83d12..33eda183a 100644 --- a/app/Policies/StandaloneDockerPolicy.php +++ b/app/Policies/StandaloneDockerPolicy.php @@ -28,8 +28,7 @@ class StandaloneDockerPolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -37,7 +36,7 @@ class StandaloneDockerPolicy */ public function update(User $user, StandaloneDocker $standaloneDocker): bool { - return $user->teams->contains('id', $standaloneDocker->server->team_id); + return $user->isAdminOfTeam($standaloneDocker->server->team_id); } /** @@ -45,7 +44,7 @@ class StandaloneDockerPolicy */ public function delete(User $user, StandaloneDocker $standaloneDocker): bool { - return $user->teams->contains('id', $standaloneDocker->server->team_id); + return $user->isAdminOfTeam($standaloneDocker->server->team_id); } /** diff --git a/app/Policies/SwarmDockerPolicy.php b/app/Policies/SwarmDockerPolicy.php index 82a75910b..b19ab4907 100644 --- a/app/Policies/SwarmDockerPolicy.php +++ b/app/Policies/SwarmDockerPolicy.php @@ -28,8 +28,7 @@ class SwarmDockerPolicy */ public function create(User $user): bool { - // return $user->isAdmin(); - return true; + return $user->isAdmin(); } /** @@ -37,7 +36,7 @@ class SwarmDockerPolicy */ public function update(User $user, SwarmDocker $swarmDocker): bool { - return $user->teams->contains('id', $swarmDocker->server->team_id); + return $user->isAdminOfTeam($swarmDocker->server->team_id); } /** @@ -45,7 +44,7 @@ class SwarmDockerPolicy */ public function delete(User $user, SwarmDocker $swarmDocker): bool { - return $user->teams->contains('id', $swarmDocker->server->team_id); + return $user->isAdminOfTeam($swarmDocker->server->team_id); } /** diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index e473d2875..5c1a79cf7 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -3,9 +3,67 @@ namespace App\Providers; // use Illuminate\Support\Facades\Gate; +use App\Models\Application; +use App\Models\ApplicationPreview; +use App\Models\ApplicationSetting; +use App\Models\CloudInitScript; +use App\Models\CloudProviderToken; +use App\Models\DiscordNotificationSettings; +use App\Models\EmailNotificationSettings; +use App\Models\Environment; +use App\Models\EnvironmentVariable; +use App\Models\GithubApp; +use App\Models\InstanceSettings; +use App\Models\PrivateKey; +use App\Models\Project; +use App\Models\PushoverNotificationSettings; +use App\Models\S3Storage; +use App\Models\Server; +use App\Models\Service; +use App\Models\ServiceApplication; +use App\Models\ServiceDatabase; +use App\Models\SharedEnvironmentVariable; +use App\Models\SlackNotificationSettings; +use App\Models\StandaloneClickhouse; +use App\Models\StandaloneDocker; +use App\Models\StandaloneDragonfly; +use App\Models\StandaloneKeydb; +use App\Models\StandaloneMariadb; +use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; +use App\Models\StandalonePostgresql; +use App\Models\StandaloneRedis; +use App\Models\SwarmDocker; +use App\Models\Team; +use App\Models\TelegramNotificationSettings; +use App\Models\WebhookNotificationSettings; +use App\Policies\ApiTokenPolicy; +use App\Policies\ApplicationPolicy; +use App\Policies\ApplicationPreviewPolicy; +use App\Policies\ApplicationSettingPolicy; +use App\Policies\CloudInitScriptPolicy; +use App\Policies\CloudProviderTokenPolicy; +use App\Policies\DatabasePolicy; +use App\Policies\EnvironmentPolicy; +use App\Policies\EnvironmentVariablePolicy; +use App\Policies\GithubAppPolicy; +use App\Policies\InstanceSettingsPolicy; +use App\Policies\NotificationPolicy; +use App\Policies\PrivateKeyPolicy; +use App\Policies\ProjectPolicy; use App\Policies\ResourceCreatePolicy; +use App\Policies\S3StoragePolicy; +use App\Policies\ServerPolicy; +use App\Policies\ServiceApplicationPolicy; +use App\Policies\ServiceDatabasePolicy; +use App\Policies\ServicePolicy; +use App\Policies\SharedEnvironmentVariablePolicy; +use App\Policies\StandaloneDockerPolicy; +use App\Policies\SwarmDockerPolicy; +use App\Policies\TeamPolicy; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; +use Laravel\Sanctum\PersonalAccessToken; class AuthServiceProvider extends ServiceProvider { @@ -15,49 +73,54 @@ class AuthServiceProvider extends ServiceProvider * @var array */ protected $policies = [ - \App\Models\Server::class => \App\Policies\ServerPolicy::class, - \App\Models\PrivateKey::class => \App\Policies\PrivateKeyPolicy::class, - \App\Models\StandaloneDocker::class => \App\Policies\StandaloneDockerPolicy::class, - \App\Models\SwarmDocker::class => \App\Policies\SwarmDockerPolicy::class, - \App\Models\Application::class => \App\Policies\ApplicationPolicy::class, - \App\Models\ApplicationPreview::class => \App\Policies\ApplicationPreviewPolicy::class, - \App\Models\ApplicationSetting::class => \App\Policies\ApplicationSettingPolicy::class, - \App\Models\Service::class => \App\Policies\ServicePolicy::class, - \App\Models\ServiceApplication::class => \App\Policies\ServiceApplicationPolicy::class, - \App\Models\ServiceDatabase::class => \App\Policies\ServiceDatabasePolicy::class, - \App\Models\Project::class => \App\Policies\ProjectPolicy::class, - \App\Models\Environment::class => \App\Policies\EnvironmentPolicy::class, - \App\Models\EnvironmentVariable::class => \App\Policies\EnvironmentVariablePolicy::class, - \App\Models\SharedEnvironmentVariable::class => \App\Policies\SharedEnvironmentVariablePolicy::class, + Server::class => ServerPolicy::class, + PrivateKey::class => PrivateKeyPolicy::class, + StandaloneDocker::class => StandaloneDockerPolicy::class, + SwarmDocker::class => SwarmDockerPolicy::class, + Application::class => ApplicationPolicy::class, + ApplicationPreview::class => ApplicationPreviewPolicy::class, + ApplicationSetting::class => ApplicationSettingPolicy::class, + Service::class => ServicePolicy::class, + ServiceApplication::class => ServiceApplicationPolicy::class, + ServiceDatabase::class => ServiceDatabasePolicy::class, + Project::class => ProjectPolicy::class, + Environment::class => EnvironmentPolicy::class, + EnvironmentVariable::class => EnvironmentVariablePolicy::class, + SharedEnvironmentVariable::class => SharedEnvironmentVariablePolicy::class, // Database policies - all use the shared DatabasePolicy - \App\Models\StandalonePostgresql::class => \App\Policies\DatabasePolicy::class, - \App\Models\StandaloneMysql::class => \App\Policies\DatabasePolicy::class, - \App\Models\StandaloneMariadb::class => \App\Policies\DatabasePolicy::class, - \App\Models\StandaloneMongodb::class => \App\Policies\DatabasePolicy::class, - \App\Models\StandaloneRedis::class => \App\Policies\DatabasePolicy::class, - \App\Models\StandaloneKeydb::class => \App\Policies\DatabasePolicy::class, - \App\Models\StandaloneDragonfly::class => \App\Policies\DatabasePolicy::class, - \App\Models\StandaloneClickhouse::class => \App\Policies\DatabasePolicy::class, + StandalonePostgresql::class => DatabasePolicy::class, + StandaloneMysql::class => DatabasePolicy::class, + StandaloneMariadb::class => DatabasePolicy::class, + StandaloneMongodb::class => DatabasePolicy::class, + StandaloneRedis::class => DatabasePolicy::class, + StandaloneKeydb::class => DatabasePolicy::class, + StandaloneDragonfly::class => DatabasePolicy::class, + StandaloneClickhouse::class => DatabasePolicy::class, // Notification policies - all use the shared NotificationPolicy - \App\Models\EmailNotificationSettings::class => \App\Policies\NotificationPolicy::class, - \App\Models\DiscordNotificationSettings::class => \App\Policies\NotificationPolicy::class, - \App\Models\TelegramNotificationSettings::class => \App\Policies\NotificationPolicy::class, - \App\Models\SlackNotificationSettings::class => \App\Policies\NotificationPolicy::class, - \App\Models\PushoverNotificationSettings::class => \App\Policies\NotificationPolicy::class, - \App\Models\WebhookNotificationSettings::class => \App\Policies\NotificationPolicy::class, + EmailNotificationSettings::class => NotificationPolicy::class, + DiscordNotificationSettings::class => NotificationPolicy::class, + TelegramNotificationSettings::class => NotificationPolicy::class, + SlackNotificationSettings::class => NotificationPolicy::class, + PushoverNotificationSettings::class => NotificationPolicy::class, + WebhookNotificationSettings::class => NotificationPolicy::class, // API Token policy - \Laravel\Sanctum\PersonalAccessToken::class => \App\Policies\ApiTokenPolicy::class, + PersonalAccessToken::class => ApiTokenPolicy::class, // Instance settings policy - \App\Models\InstanceSettings::class => \App\Policies\InstanceSettingsPolicy::class, + InstanceSettings::class => InstanceSettingsPolicy::class, + + // S3 storage policy + S3Storage::class => S3StoragePolicy::class, // Team policy - \App\Models\Team::class => \App\Policies\TeamPolicy::class, + Team::class => TeamPolicy::class, // Git source policies - \App\Models\GithubApp::class => \App\Policies\GithubAppPolicy::class, + GithubApp::class => GithubAppPolicy::class, + CloudProviderToken::class => CloudProviderTokenPolicy::class, + CloudInitScript::class => CloudInitScriptPolicy::class, ]; diff --git a/app/Traits/HasDatabaseStatusInfo.php b/app/Traits/HasDatabaseStatusInfo.php index e46cccf0c..5127e0595 100644 --- a/app/Traits/HasDatabaseStatusInfo.php +++ b/app/Traits/HasDatabaseStatusInfo.php @@ -30,6 +30,8 @@ trait HasDatabaseStatusInfo public ?Carbon $certificateValidUntil = null; + public bool $isPasswordHiddenForMember = false; + abstract protected function databaseLabel(): string; protected function supportsSsl(): bool @@ -73,14 +75,20 @@ trait HasDatabaseStatusInfo public function mount(): void { + $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false; $this->refresh(); } public function refresh(): void { $this->database->refresh(); - $this->dbUrl = $this->database->internal_db_url; - $this->dbUrlPublic = $this->database->external_db_url; + if ($this->isPasswordHiddenForMember) { + $this->dbUrl = null; + $this->dbUrlPublic = null; + } else { + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } if ($this->supportsSsl()) { $this->enableSsl = (bool) $this->database->enable_ssl; $this->certificateValidUntil = $this->database->sslCertificates()->first()?->valid_until; @@ -167,6 +175,7 @@ trait HasDatabaseStatusInfo 'sslModeHelper' => $this->sslModeHelper(), 'showPublicUrlPlaceholder' => $this->showPublicUrlPlaceholder(), 'isExited' => str($this->database->status)->contains('exited'), + 'isPasswordHiddenForMember' => $this->isPasswordHiddenForMember, ]); } } diff --git a/app/View/Components/Forms/Button.php b/app/View/Components/Forms/Button.php index b54444261..8511c87db 100644 --- a/app/View/Components/Forms/Button.php +++ b/app/View/Components/Forms/Button.php @@ -14,6 +14,7 @@ class Button extends Component */ public function __construct( public bool $disabled = false, + public bool $authDisabled = false, public bool $noStyle = false, public ?string $modalId = null, public string $defaultClass = 'button', @@ -28,6 +29,7 @@ class Button extends Component if (! $hasPermission) { $this->disabled = true; + $this->authDisabled = true; } } diff --git a/app/View/Components/Forms/Checkbox.php b/app/View/Components/Forms/Checkbox.php index eb38d84af..e33e4b919 100644 --- a/app/View/Components/Forms/Checkbox.php +++ b/app/View/Components/Forms/Checkbox.php @@ -6,7 +6,6 @@ use Closure; use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Gate; use Illuminate\View\Component; -use Visus\Cuid2\Cuid2; class Checkbox extends Component { @@ -58,7 +57,7 @@ class Checkbox extends Component // Generate unique HTML ID by adding random suffix // This prevents duplicate IDs when multiple forms are on the same page if ($this->id) { - $uniqueSuffix = new Cuid2; + $uniqueSuffix = new_public_id(); $this->htmlId = $this->id.'-'.$uniqueSuffix; } else { $this->htmlId = $this->id; diff --git a/app/View/Components/Forms/Datalist.php b/app/View/Components/Forms/Datalist.php index 3b7a9ee34..b0f85c8cb 100644 --- a/app/View/Components/Forms/Datalist.php +++ b/app/View/Components/Forms/Datalist.php @@ -6,7 +6,6 @@ use Closure; use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Gate; use Illuminate\View\Component; -use Visus\Cuid2\Cuid2; class Datalist extends Component { @@ -55,7 +54,7 @@ class Datalist extends Component $this->modelBinding = $this->id; if (is_null($this->id)) { - $this->id = new Cuid2; + $this->id = new_public_id(); // Don't create wire:model binding for auto-generated IDs $this->modelBinding = 'null'; } @@ -64,7 +63,7 @@ class Datalist extends Component // This prevents duplicate IDs when multiple forms are on the same page if ($this->modelBinding && $this->modelBinding !== 'null') { // Use original ID with random suffix for uniqueness - $uniqueSuffix = new Cuid2; + $uniqueSuffix = new_public_id(); $this->htmlId = $this->modelBinding.'-'.$uniqueSuffix; } else { $this->htmlId = (string) $this->id; diff --git a/app/View/Components/Forms/EnvVarInput.php b/app/View/Components/Forms/EnvVarInput.php index faef64a36..2f26e44cc 100644 --- a/app/View/Components/Forms/EnvVarInput.php +++ b/app/View/Components/Forms/EnvVarInput.php @@ -6,7 +6,6 @@ use Closure; use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Gate; use Illuminate\View\Component; -use Visus\Cuid2\Cuid2; class EnvVarInput extends Component { @@ -56,7 +55,7 @@ class EnvVarInput extends Component $this->modelBinding = $this->id; if (is_null($this->id)) { - $this->id = new Cuid2; + $this->id = new_public_id(); // Don't create wire:model binding for auto-generated IDs $this->modelBinding = 'null'; } @@ -64,7 +63,7 @@ class EnvVarInput extends Component // This prevents duplicate IDs when multiple forms are on the same page if ($this->modelBinding && $this->modelBinding !== 'null') { // Use original ID with random suffix for uniqueness - $uniqueSuffix = new Cuid2; + $uniqueSuffix = new_public_id(); $this->htmlId = $this->modelBinding.'-'.$uniqueSuffix; } else { $this->htmlId = (string) $this->id; diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index 5ed347f42..303856926 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -5,8 +5,8 @@ namespace App\View\Components\Forms; use Closure; use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Str; use Illuminate\View\Component; -use Visus\Cuid2\Cuid2; class Input extends Component { @@ -51,7 +51,7 @@ class Input extends Component $this->modelBinding = $this->id; if (is_null($this->id)) { - $this->id = new Cuid2; + $this->id = new_public_id(); // Don't create wire:model binding for auto-generated IDs $this->modelBinding = 'null'; } @@ -59,7 +59,7 @@ class Input extends Component // This prevents duplicate IDs when multiple forms are on the same page if ($this->modelBinding && $this->modelBinding !== 'null') { // Use original ID with random suffix for uniqueness - $uniqueSuffix = new Cuid2; + $uniqueSuffix = new_public_id(); $this->htmlId = $this->modelBinding.'-'.$uniqueSuffix; } else { $this->htmlId = (string) $this->id; diff --git a/app/View/Components/Forms/Select.php b/app/View/Components/Forms/Select.php index 026e3ba8c..327c33da6 100644 --- a/app/View/Components/Forms/Select.php +++ b/app/View/Components/Forms/Select.php @@ -6,7 +6,6 @@ use Closure; use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Gate; use Illuminate\View\Component; -use Visus\Cuid2\Cuid2; class Select extends Component { @@ -48,7 +47,7 @@ class Select extends Component $this->modelBinding = $this->id; if (is_null($this->id)) { - $this->id = new Cuid2; + $this->id = new_public_id(); // Don't create wire:model binding for auto-generated IDs $this->modelBinding = 'null'; } @@ -57,7 +56,7 @@ class Select extends Component // This prevents duplicate IDs when multiple forms are on the same page if ($this->modelBinding && $this->modelBinding !== 'null') { // Use original ID with random suffix for uniqueness - $uniqueSuffix = new Cuid2; + $uniqueSuffix = new_public_id(); $this->htmlId = $this->modelBinding.'-'.$uniqueSuffix; } else { $this->htmlId = (string) $this->id; diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index 02a23a26a..5a5f975c6 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -5,8 +5,8 @@ namespace App\View\Components\Forms; use Closure; use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Str; use Illuminate\View\Component; -use Visus\Cuid2\Cuid2; class Textarea extends Component { @@ -63,7 +63,7 @@ class Textarea extends Component $this->modelBinding = $this->id; if (is_null($this->id)) { - $this->id = new Cuid2; + $this->id = new_public_id(); // Don't create wire:model binding for auto-generated IDs $this->modelBinding = 'null'; } @@ -72,7 +72,7 @@ class Textarea extends Component // This prevents duplicate IDs when multiple forms are on the same page if ($this->modelBinding && $this->modelBinding !== 'null') { // Use original ID with random suffix for uniqueness - $uniqueSuffix = new Cuid2; + $uniqueSuffix = new_public_id(); $this->htmlId = $this->modelBinding.'-'.$uniqueSuffix; } else { $this->htmlId = (string) $this->id; diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 4707b0a07..b7e4af7ab 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -10,7 +10,6 @@ use App\Models\EnvironmentVariable; use App\Models\Server; use App\Models\StandaloneDocker; use Spatie\Url\Url; -use Visus\Cuid2\Cuid2; function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, ?string $commit = null, bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false, ?string $docker_registry_image_tag = null) { @@ -192,7 +191,7 @@ function next_after_cancel(?Server $server = null) function clone_application(Application $source, $destination, array $overrides = [], bool $cloneVolumeData = false): Application { - $uuid = $overrides['uuid'] ?? (string) new Cuid2; + $uuid = $overrides['uuid'] ?? new_public_id(); $server = $destination->server; if ($server->team_id !== currentTeam()->id) { @@ -259,7 +258,7 @@ function clone_application(Application $source, $destination, array $overrides = 'created_at', 'updated_at', ])->fill([ - 'uuid' => (string) new Cuid2, + 'uuid' => new_public_id(), 'application_id' => $newApplication->id, 'team_id' => currentTeam()->id, ]); @@ -274,7 +273,7 @@ function clone_application(Application $source, $destination, array $overrides = 'created_at', 'updated_at', ])->fill([ - 'uuid' => (string) new Cuid2, + 'uuid' => new_public_id(), 'application_id' => $newApplication->id, 'status' => 'exited', 'fqdn' => null, @@ -322,7 +321,7 @@ function clone_application(Application $source, $destination, array $overrides = VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); queue_application_deployment( - deployment_uuid: (string) new Cuid2, + deployment_uuid: new_public_id(), application: $source, server: $sourceServer, destination: $source->destination, diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index 4d5e085f3..5f0b2e690 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -17,12 +17,11 @@ use App\Models\SwarmDocker; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; -use Visus\Cuid2\Cuid2; function create_standalone_postgresql($environmentId, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null, string $databaseImage = 'postgres:16-alpine'): StandalonePostgresql { $database = new StandalonePostgresql; - $database->uuid = (new Cuid2); + $database->uuid = new_public_id(); $database->name = 'postgresql-database-'.$database->uuid; $database->image = $databaseImage; $database->postgres_password = Str::password(length: 64, symbols: false); @@ -40,7 +39,7 @@ function create_standalone_postgresql($environmentId, StandaloneDocker|SwarmDock function create_standalone_redis($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneRedis { $database = new StandaloneRedis; - $database->uuid = (new Cuid2); + $database->uuid = new_public_id(); $database->name = 'redis-database-'.$database->uuid; $redis_password = Str::password(length: 64, symbols: false); @@ -79,7 +78,7 @@ function create_standalone_redis($environment_id, StandaloneDocker|SwarmDocker $ function create_standalone_mongodb($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneMongodb { $database = new StandaloneMongodb; - $database->uuid = (new Cuid2); + $database->uuid = new_public_id(); $database->name = 'mongodb-database-'.$database->uuid; $database->mongo_initdb_root_password = Str::password(length: 64, symbols: false); $database->environment_id = $environment_id; @@ -96,7 +95,7 @@ function create_standalone_mongodb($environment_id, StandaloneDocker|SwarmDocker function create_standalone_mysql($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneMysql { $database = new StandaloneMysql; - $database->uuid = (new Cuid2); + $database->uuid = new_public_id(); $database->name = 'mysql-database-'.$database->uuid; $database->mysql_root_password = Str::password(length: 64, symbols: false); $database->mysql_password = Str::password(length: 64, symbols: false); @@ -114,7 +113,7 @@ function create_standalone_mysql($environment_id, StandaloneDocker|SwarmDocker $ function create_standalone_mariadb($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneMariadb { $database = new StandaloneMariadb; - $database->uuid = (new Cuid2); + $database->uuid = new_public_id(); $database->name = 'mariadb-database-'.$database->uuid; $database->mariadb_root_password = Str::password(length: 64, symbols: false); $database->mariadb_password = Str::password(length: 64, symbols: false); @@ -132,7 +131,7 @@ function create_standalone_mariadb($environment_id, StandaloneDocker|SwarmDocker function create_standalone_keydb($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneKeydb { $database = new StandaloneKeydb; - $database->uuid = (new Cuid2); + $database->uuid = new_public_id(); $database->name = 'keydb-database-'.$database->uuid; $database->keydb_password = Str::password(length: 64, symbols: false); $database->environment_id = $environment_id; @@ -149,7 +148,7 @@ function create_standalone_keydb($environment_id, StandaloneDocker|SwarmDocker $ function create_standalone_dragonfly($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneDragonfly { $database = new StandaloneDragonfly; - $database->uuid = (new Cuid2); + $database->uuid = new_public_id(); $database->name = 'dragonfly-database-'.$database->uuid; $database->dragonfly_password = Str::password(length: 64, symbols: false); $database->environment_id = $environment_id; @@ -166,7 +165,7 @@ function create_standalone_dragonfly($environment_id, StandaloneDocker|SwarmDock function create_standalone_clickhouse($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneClickhouse { $database = new StandaloneClickhouse; - $database->uuid = (new Cuid2); + $database->uuid = new_public_id(); $database->name = 'clickhouse-database-'.$database->uuid; $database->clickhouse_admin_password = Str::password(length: 64, symbols: false); $database->environment_id = $environment_id; diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 2cf159bfd..1b389c77c 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -9,7 +9,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; -use Visus\Cuid2\Cuid2; function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection { @@ -459,7 +458,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ foreach ($domains as $loop => $domain) { try { if ($generate_unique_uuid) { - $uuid = new Cuid2; + $uuid = new_public_id(); } $url = Url::fromString($domain); diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 123cf906a..6632e1fd5 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -14,7 +14,6 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Str; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; -use Visus\Cuid2\Cuid2; /** * Validates a Docker Compose YAML string for command injection vulnerabilities. @@ -1240,7 +1239,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $schema = $url->getScheme(); $portInt = $url->getPort(); $port = $portInt !== null ? ':'.$portInt : ''; - $random = new Cuid2; + $random = new_public_id(); $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $pullRequestId, $preview_fqdn); diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 3a516378f..0f3a7c4dd 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -202,6 +202,11 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d $is_debug_enabled = data_get($application, 'settings.is_debug_enabled'); $serverTimezone = getServerTimezone(data_get($application, 'destination.server')); + // Members should never see debug logs, even if an admin enabled debug mode + if ($is_debug_enabled && auth()->check() && auth()->user()->isMember()) { + $is_debug_enabled = false; + } + $logs = data_get($application_deployment_queue, 'logs'); if (empty($logs)) { return collect([]); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 8fdfceaf1..7b113e7b8 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -64,7 +64,6 @@ use PurplePixie\PhpDns\DNSQuery; use PurplePixie\PhpDns\DNSTypes; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; -use Visus\Cuid2\Cuid2; function base_configuration_dir(): string { @@ -115,6 +114,13 @@ function sanitize_string(?string $input = null): ?string return $sanitized; } +function new_public_id(int $length = 24): string +{ + $length = max(1, $length); + + return Str::lower(Str::random($length)); +} + /** * Validate that a path or identifier is safe for use in shell commands. * @@ -455,7 +461,7 @@ function generate_random_name(?string $cuid = null): string ] ); if (is_null($cuid)) { - $cuid = new Cuid2; + $cuid = new_public_id(); } return Str::kebab("{$generator->getName()}-$cuid"); @@ -491,7 +497,7 @@ function formatPrivateKey(string $privateKey) function generate_application_name(string $git_repository, string $git_branch, ?string $cuid = null): string { if (is_null($cuid)) { - $cuid = new Cuid2; + $cuid = new_public_id(); } $repo_name = str_contains($git_repository, '/') ? last(explode('/', $git_repository)) : $git_repository; @@ -3259,7 +3265,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $template = $resource->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2; + $random = new_public_id(); $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn); diff --git a/database/factories/CloudProviderTokenFactory.php b/database/factories/CloudProviderTokenFactory.php index 303f08fc8..4da7a2d08 100644 --- a/database/factories/CloudProviderTokenFactory.php +++ b/database/factories/CloudProviderTokenFactory.php @@ -13,18 +13,13 @@ class CloudProviderTokenFactory extends Factory { protected $model = CloudProviderToken::class; - /** - * Define the model's default state. - * - * @return array - */ public function definition(): array { return [ 'team_id' => Team::factory(), 'provider' => 'hetzner', - 'token' => $this->faker->sha256(), - 'name' => $this->faker->words(2, true), + 'token' => 'test-cloud-provider-token', + 'name' => fake()->words(3, true), ]; } } diff --git a/database/factories/PrivateKeyFactory.php b/database/factories/PrivateKeyFactory.php new file mode 100644 index 000000000..51cfdcaa2 --- /dev/null +++ b/database/factories/PrivateKeyFactory.php @@ -0,0 +1,37 @@ + + */ +class PrivateKeyFactory extends Factory +{ + protected $model = PrivateKey::class; + + public function definition(): array + { + return [ + 'name' => fake()->words(2, true), + 'description' => fake()->sentence(), + 'private_key' => $this->privateKey(), + 'team_id' => Team::factory(), + 'is_git_related' => false, + ]; + } + + private function privateKey(): string + { + return '-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk +hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA +AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV +uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== +-----END OPENSSH PRIVATE KEY-----'; + } +} diff --git a/database/migrations/2026_05_29_000000_encrypt_application_deployment_configuration_columns.php b/database/migrations/2026_05_29_000000_encrypt_application_deployment_configuration_columns.php index 63bd39f6c..19c4445b2 100644 --- a/database/migrations/2026_05_29_000000_encrypt_application_deployment_configuration_columns.php +++ b/database/migrations/2026_05_29_000000_encrypt_application_deployment_configuration_columns.php @@ -11,7 +11,7 @@ return new class extends Migration */ public function up(): void { - // SQLite (testing) uses type affinity, so json columns already accept text + // SQLite (testing) uses type affinity, so json columns already accept text. if (DB::connection()->getDriverName() !== 'pgsql') { return; } diff --git a/resources/css/utilities.css b/resources/css/utilities.css index 170e6ac16..c982f9f86 100644 --- a/resources/css/utilities.css +++ b/resources/css/utilities.css @@ -122,7 +122,11 @@ } @utility button { - @apply flex gap-2 justify-center items-center px-2 h-8 text-sm text-black normal-case rounded-sm border-2 outline-0 cursor-pointer font-medium bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-coolgray-100 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-200 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-neutral-600 disabled:border-transparent disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base; + @apply flex gap-2 justify-center items-center px-2 h-8 text-sm text-black normal-case rounded-sm border-2 outline-0 cursor-pointer font-medium bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-coolgray-100 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-200 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-neutral-600 disabled:border-neutral-200 dark:disabled:border-coolgray-300 disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base; +} + +@utility auth-tooltip { + @apply fixed z-[99] px-2.5 py-1.5 text-xs rounded-sm pointer-events-none whitespace-nowrap text-neutral-700 bg-neutral-200 dark:text-neutral-300 dark:bg-coolgray-400; } @utility alert-success { @@ -355,4 +359,8 @@ gap: 0; margin-inline: auto; } + + .sidebar-collapsed .sidebar-collapsed-label { + display: none; + } } diff --git a/resources/views/components/applications/advanced.blade.php b/resources/views/components/applications/advanced.blade.php index d97140570..780a727e1 100644 --- a/resources/views/components/applications/advanced.blade.php +++ b/resources/views/components/applications/advanced.blade.php @@ -3,50 +3,84 @@ Advanced @if ($application->status === 'running') - - - - - + @can('deploy', $application) + + + + + + @else + + @endcan @else - - - - - + @can('deploy', $application) + + + + + + @else + + @endcan @endif diff --git a/resources/views/components/database-status-info.blade.php b/resources/views/components/database-status-info.blade.php index 4a9de3ca5..24116db25 100644 --- a/resources/views/components/database-status-info.blade.php +++ b/resources/views/components/database-status-info.blade.php @@ -11,6 +11,7 @@ 'certificateValidUntil' => null, 'isExited' => false, 'showPublicUrlPlaceholder' => false, + 'isPasswordHiddenForMember' => false, ]) @php @@ -18,14 +19,19 @@ @endphp
- - @if ($dbUrlPublic) - - @elseif ($showPublicUrlPlaceholder) - + @if ($isPasswordHiddenForMember) + + + @else + + @if ($dbUrlPublic) + + @elseif ($showPublicUrlPlaceholder) + + @endif @endif @if ($supportsSsl) diff --git a/resources/views/components/forms/button.blade.php b/resources/views/components/forms/button.blade.php index 96cdb4420..f1efd8c6c 100644 --- a/resources/views/components/forms/button.blade.php +++ b/resources/views/components/forms/button.blade.php @@ -1,3 +1,24 @@ +@if ($authDisabled) + +@endif +@if ($authDisabled) +
+ You do not have permission to perform this action. +
+
+@endif diff --git a/resources/views/components/forms/copy-button.blade.php b/resources/views/components/forms/copy-button.blade.php index eb3f3d8a4..61233b2ca 100644 --- a/resources/views/components/forms/copy-button.blade.php +++ b/resources/views/components/forms/copy-button.blade.php @@ -2,24 +2,27 @@
@if ($label) - + @endif
- - +
diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index b1d7ac475..4629e3b96 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -6,6 +6,7 @@ 'buttonFullWidth' => false, 'customButton' => null, 'disabled' => false, + 'authDisabled' => false, 'dispatchAction' => false, 'submitAction' => 'delete', 'content' => null, @@ -151,11 +152,11 @@ @else @if ($disabled) @if ($buttonFullWidth) - + {{ $buttonTitle }} @else - + {{ $buttonTitle }} @endif diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php index c2ca28a2d..924147e4b 100644 --- a/resources/views/components/navbar.blade.php +++ b/resources/views/components/navbar.blade.php @@ -44,16 +44,6 @@ this.queryTheme(); this.checkZoom(); }, - setTheme(type) { - this.theme = type; - localStorage.setItem('theme', type); - this.queryTheme(); - }, - cycleTheme() { - const themes = ['light', 'system', 'dark']; - const currentIndex = themes.indexOf(this.theme || localStorage.getItem('theme') || 'dark'); - this.setTheme(themes[(currentIndex + 1) % themes.length]); - }, queryTheme() { const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)').matches; const userSettings = localStorage.getItem('theme') || 'dark'; @@ -375,45 +365,6 @@
  • -
  • - - -
  • @if (isInstanceAdmin() && !isCloud()) @persist('upgrade')
  • diff --git a/resources/views/components/server/sidebar-sentinel.blade.php b/resources/views/components/server/sidebar-sentinel.blade.php index 8125fe22c..421a1ec9d 100644 --- a/resources/views/components/server/sidebar-sentinel.blade.php +++ b/resources/views/components/server/sidebar-sentinel.blade.php @@ -1,10 +1,12 @@ diff --git a/resources/views/components/services/advanced.blade.php b/resources/views/components/services/advanced.blade.php index 831723307..1974bf221 100644 --- a/resources/views/components/services/advanced.blade.php +++ b/resources/views/components/services/advanced.blade.php @@ -3,7 +3,7 @@ Advanced @if (str($service->status)->contains('running')) - @else
    Please verify these values. You can only modify them before the initial @@ -29,8 +33,12 @@
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    @endif started_at)
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    @else
    Please verify these values. You can only modify them before the initial start. After that, you need to modify it in the database.
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    @endif
    diff --git a/resources/views/livewire/project/database/heading.blade.php b/resources/views/livewire/project/database/heading.blade.php index 61670cc72..ca4fcf1ea 100644 --- a/resources/views/livewire/project/database/heading.blade.php +++ b/resources/views/livewire/project/database/heading.blade.php @@ -216,7 +216,8 @@
    @script diff --git a/resources/views/livewire/project/database/keydb/general.blade.php b/resources/views/livewire/project/database/keydb/general.blade.php index 9b9ed55de..74c92bb61 100644 --- a/resources/views/livewire/project/database/keydb/general.blade.php +++ b/resources/views/livewire/project/database/keydb/general.blade.php @@ -18,16 +18,24 @@ @if ($database->started_at)
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    @else
    Please verify these values. You can only modify them before the initial start. After that, you need to modify it in the database.
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    @endif @if ($database->started_at)
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    @else
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif @@ -36,8 +40,12 @@
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    diff --git a/resources/views/livewire/project/database/mysql/general.blade.php b/resources/views/livewire/project/database/mysql/general.blade.php index 23280ce04..0afd9db19 100644 --- a/resources/views/livewire/project/database/mysql/general.blade.php +++ b/resources/views/livewire/project/database/mysql/general.blade.php @@ -20,12 +20,20 @@
    @if ($database->started_at)
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    @else
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif @@ -48,8 +52,12 @@
    - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    @@ -134,7 +142,8 @@
    @forelse($initScripts ?? [] as $script) - + @empty
    No initialization scripts found.
    @endforelse diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php index c153955d8..9d317f6bf 100644 --- a/resources/views/livewire/project/database/redis/general.blade.php +++ b/resources/views/livewire/project/database/redis/general.blade.php @@ -26,8 +26,12 @@ @endif - + @if ($isPasswordHiddenForMember) + + @else + + @endif
    @else
    You can only change the username and password in the database after @@ -42,13 +46,17 @@ Note: If the environment variable REDIS_USERNAME is set as a shared variable (environment, project, or team-based), this input field will become read-only." :disabled="$this->isSharedVariable('REDIS_USERNAME')" canGate="update" :canResource="$database" /> @endif - + @else + + :disabled="$this->isSharedVariable('REDIS_PASSWORD')" canGate="update" :canResource="$database" /> + @endif
    @endif
    diff --git a/resources/views/livewire/project/database/status-info.blade.php b/resources/views/livewire/project/database/status-info.blade.php index 7107b3daf..3e3c97c80 100644 --- a/resources/views/livewire/project/database/status-info.blade.php +++ b/resources/views/livewire/project/database/status-info.blade.php @@ -2,5 +2,6 @@ + :show-public-url-placeholder="$showPublicUrlPlaceholder" :is-exited="$isExited" + :is-password-hidden-for-member="$isPasswordHiddenForMember" />
    diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php index b7f68c6ec..2a14d9350 100644 --- a/resources/views/livewire/project/service/file-storage.blade.php +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -100,7 +100,7 @@ @else {{-- Read-only view --}} @if (!$fileStorage->is_directory) - @can('view', $resource) + @can('update', $resource)
    Load from server diff --git a/resources/views/livewire/project/service/heading.blade.php b/resources/views/livewire/project/service/heading.blade.php index 1071bd658..820d99080 100644 --- a/resources/views/livewire/project/service/heading.blade.php +++ b/resources/views/livewire/project/service/heading.blade.php @@ -369,7 +369,9 @@
    @if (str($service->status)->contains('running')) - + + + @@ -379,7 +381,8 @@ Restart - + + @@ -393,7 +396,8 @@ Stop @elseif (str($service->status)->contains('degraded')) - + + @@ -403,7 +407,8 @@ Restart - + + @@ -417,7 +422,8 @@ Stop @elseif (str($service->status)->contains('exited')) - + @else - + + @@ -440,7 +447,8 @@ Stop - + @endif
    @@ -522,11 +530,9 @@ ); return; } - window.dispatchEvent(new CustomEvent('startservice')); $wire.$call('start'); }); $wire.$on('forceDeployEvent', () => { - window.dispatchEvent(new CustomEvent('startservice')); $wire.$call('forceDeploy'); }); $wire.$on('restartEvent', async () => { @@ -539,12 +545,10 @@ } $wire.$dispatch('info', 'Gracefully stopping service.

    It could take a while depending on the service.'); - window.dispatchEvent(new CustomEvent('startservice')); $wire.$call('restart'); }); $wire.$on('pullAndRestartEvent', () => { $wire.$dispatch('info', 'Pulling new images and restarting service.'); - window.dispatchEvent(new CustomEvent('startservice')); $wire.$call('pullAndRestartEvent'); }); $wire.$on('cleanupEvent', () => { diff --git a/resources/views/livewire/project/service/stack-form.blade.php b/resources/views/livewire/project/service/stack-form.blade.php index 0aec7c873..37e5f5924 100644 --- a/resources/views/livewire/project/service/stack-form.blade.php +++ b/resources/views/livewire/project/service/stack-form.blade.php @@ -45,10 +45,14 @@ @endif
    - + @if ($isPasswordHiddenForMember && data_get($field, 'isPassword')) + + @else + + @endif @endforeach
    @endif diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php index cbb7afa2f..c602ba4af 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -184,6 +184,7 @@ @endif
  • @endif @@ -191,16 +192,23 @@
    - - @if ($is_shared) - + @if ($isValueHidden) +
    + +
    + @else + + @if ($is_shared) + + @endif @endif
    Healthcheck Save @if (!$healthCheckEnabled) - - + + @else Disable Healthcheck @endif diff --git a/resources/views/livewire/project/shared/resource-details.blade.php b/resources/views/livewire/project/shared/resource-details.blade.php index 3be82da12..62b04f2a0 100644 --- a/resources/views/livewire/project/shared/resource-details.blade.php +++ b/resources/views/livewire/project/shared/resource-details.blade.php @@ -1,6 +1,4 @@ -
    -
    Identifiers for this resource. Read-only
    - +

    Resource

    diff --git a/resources/views/livewire/project/shared/resource-operations.blade.php b/resources/views/livewire/project/shared/resource-operations.blade.php index 658a7cbd3..0c3c8885c 100644 --- a/resources/views/livewire/project/shared/resource-operations.blade.php +++ b/resources/views/livewire/project/shared/resource-operations.blade.php @@ -101,16 +101,10 @@
    - @else - - You don't have permission to clone resources. Contact your team administrator to request access. - - @endcan -

    Move Resource

    -
    Transfer this resource between projects and environments.
    +

    Move Resource

    +
    Transfer this resource between projects and environments.
    - @can('update', $resource) @if ($projects->count() > 0)
    @@ -160,9 +154,8 @@
    @endif @else - - You don't have permission to move resources between projects or environments. Contact your team - administrator to request access. + + You don't have permission to modify this resource. Contact your team administrator for access. @endcan
    diff --git a/resources/views/livewire/project/shared/scheduled-task/show.blade.php b/resources/views/livewire/project/shared/scheduled-task/show.blade.php index c699609c0..79e0f2604 100644 --- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php @@ -3,44 +3,47 @@

    Task {{ $task->name }}

    - + Save @if ($resource->isRunning()) - - Execute Now - + @can('update', $resource) + + Execute Now + + @endcan @endif - @if (!$isEnabled) - Enable Task - @else - Disable Task - @endif - - + @can('update', $resource) + @if (!$isEnabled) + Enable Task + @else + Disable Task + @endif + + @endcan

    Configuration

    - - + - @if ($type === 'application') - @elseif ($type === 'service') - @endif
    - +
    diff --git a/resources/views/livewire/project/shared/tags.blade.php b/resources/views/livewire/project/shared/tags.blade.php index 85208b75b..72b8214a9 100644 --- a/resources/views/livewire/project/shared/tags.blade.php +++ b/resources/views/livewire/project/shared/tags.blade.php @@ -10,8 +10,8 @@ Add @else - - You don't have permission to manage tags. Contact your team administrator to request access. + + You don't have permission to manage this resource. Contact your team administrator for access. @endcan @if (data_get($this->resource, 'tags') && count(data_get($this->resource, 'tags')) > 0) diff --git a/resources/views/livewire/project/shared/webhooks.blade.php b/resources/views/livewire/project/shared/webhooks.blade.php index 24bba525a..bccc31383 100644 --- a/resources/views/livewire/project/shared/webhooks.blade.php +++ b/resources/views/livewire/project/shared/webhooks.blade.php @@ -22,9 +22,9 @@ helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitHub." label="GitHub Webhook Secret" id="githubManualWebhookSecret"> @else - + label="GitHub Webhook Secret" value="Hidden (only admins can view)"> @endcan
    @@ -39,9 +39,9 @@ helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitLab." label="GitLab Webhook Secret" id="gitlabManualWebhookSecret">
    @else - + label="GitLab Webhook Secret" value="Hidden (only admins can view)">
    @endcan
    @@ -51,9 +51,9 @@ helper="Need to set a secret to be able to use this webhook. It should match with the secret in Bitbucket." label="Bitbucket Webhook Secret" id="bitbucketManualWebhookSecret"> @else - + label="Bitbucket Webhook Secret" value="Hidden (only admins can view)"> @endcan
    @@ -63,9 +63,9 @@ helper="Need to set a secret to be able to use this webhook. It should match with the secret in Gitea." label="Gitea Webhook Secret" id="giteaManualWebhookSecret"> @else - + label="Gitea Webhook Secret" value="Hidden (only admins can view)"> @endcan
    @can('update', $resource) diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php index 69eab3e70..dc8c97952 100644 --- a/resources/views/livewire/security/api-tokens.blade.php +++ b/resources/views/livewire/security/api-tokens.blade.php @@ -24,20 +24,23 @@ Create
    -
    - Permissions - : -
    - @if ($permissions) +
    + Permissions + + @if ($permissions) +
    @foreach ($permissions as $permission) -
    {{ $permission }}
    + + {{ $permission }} + @endforeach - @endif -
    +
    + @endif
    -
    +

    Token Permissions

    +
    @if ($canUseRootPermissions) @@ -55,13 +58,23 @@ helper="Write access requires admin or owner role" :checked="false"> @endif - - + @else + + @endif + - + @if ($canUseSensitivePermissions) + + @else + + @endif @endif
    @if (in_array('root', $permissions)) @@ -70,13 +83,36 @@ @endcan @if (session()->has('token')) -
    Please copy this token now. For your security, it won't be shown - again. +
    +
    Please copy this token now. For your security, it won't + be shown again.
    +
    + + +
    -
    {{ session('token') }}
    @endif -

    Issued Tokens

    -
    +
    +
    +

    Issued Tokens

    + @if ($tokens->count() > 1) + + @endif +
    @@ -94,13 +130,17 @@ @forelse ($tokens as $token) - + {{ $token->name }} @if ($token->abilities) -
    +
    @foreach ($token->abilities as $ability) -
    {{ $ability }}
    + + {{ $ability }} + @endforeach
    @endif diff --git a/resources/views/livewire/server/cloudflare-tunnel.blade.php b/resources/views/livewire/server/cloudflare-tunnel.blade.php index 2ebac9d41..668fae8f1 100644 --- a/resources/views/livewire/server/cloudflare-tunnel.blade.php +++ b/resources/views/livewire/server/cloudflare-tunnel.blade.php @@ -71,6 +71,11 @@ @endif @if (!$isCloudflareTunnelsEnabled && $server->isFunctional()) + @cannot('update', $server) + + You don't have permission to configure Cloudflare Tunnel for this server. + + @endcannot @script @@ -121,10 +122,6 @@ ]" confirmationText="I manually configured Cloudflare Tunnel" confirmationLabel="Please type the confirmation text to confirm that you manually configured Cloudflare Tunnel." shortConfirmationLabel="Confirmation text" /> - @else - - You don't have permission to configure Cloudflare Tunnel for this server. - @endcan
    @endif diff --git a/resources/views/livewire/server/navbar.blade.php b/resources/views/livewire/server/navbar.blade.php index ed7d59980..463ecef3b 100644 --- a/resources/views/livewire/server/navbar.blade.php +++ b/resources/views/livewire/server/navbar.blade.php @@ -78,7 +78,7 @@ @endif @endif - @if ($server->isFunctional() && !$server->isSwarm() && !$server->settings->is_build_server) + @if ($server->isFunctional() && !$server->isSwarm() && !$server->settings->is_build_server && auth()->user()?->can('viewSentinel', $server))
    @@ -114,6 +114,7 @@
    @if ($server->proxySet()) + @can('manageProxy', $server) @if ($proxyStatus === 'running')
    @@ -178,6 +179,7 @@ Start Proxy @endif + @endcan @endif @script