diff --git a/.ai/lessons.md b/.ai/lessons.md new file mode 100644 index 000000000..6bd6dcbaa --- /dev/null +++ b/.ai/lessons.md @@ -0,0 +1,20 @@ +# Lessons Learned + +## Docker / Worktree Setup +- The Docker dev container mounts from `young-stork` worktree, NOT `ivory-raccoon` +- Do NOT copy files to `young-stork` or use `docker cp` — only modify files in the `ivory-raccoon` worktree +- Do NOT use `docker exec` to run tests — work entirely within the `ivory-raccoon` worktree + +## Policy Tests +- Policy methods have typed parameters (e.g., `Server $server`) — anonymous classes cause TypeError +- Must use `Mockery::mock(Model::class)->makePartial()` instead of anonymous classes for model stubs +- Use `shouldReceive('getAttribute')->with('property')->andReturn(value)` for model properties accessed via relationship chains + +## Browser Tests (Pest Browser Plugin) +- Plugin runs an in-process HTTP server (Amphp) sharing the same SQLite :memory: database as the test process +- Model boot events that call external services (e.g., `StandaloneDocker::created` runs docker commands) WILL fail in tests — use `Model::withoutEvents()` to wrap creation +- Livewire full-page components that fail during `mount()` silently redirect to the previous URL instead of showing an error page +- `Server::proxySet()` requires `isFunctional()` which requires `is_reachable=true` AND `is_usable=true` in ServerSetting — tests without a validated server won't show proxy controls +- Application/Database/Service pages require complex model chains (Application → Environment → Project → Team, with StandaloneDocker destination) that are difficult to fully set up for browser tests due to Livewire mount() redirecting on any chain failure +- The `currentTeam()` helper reads from session (`data_get(session('currentTeam'), 'id')`) — set during browser login flow +- `Project::created` auto-creates a "production" environment — don't manually create one with that name diff --git a/CLAUDE.md b/CLAUDE.md index 8e398586b..46f781710 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,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/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index eb2c15625..d8916431f 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -54,6 +54,10 @@ class ApplicationsController extends Controller ]); } + if ($application->is_shown_once ?? false) { + $application->makeHidden(['value', 'real_value']); + } + return serializeApiResponse($application); } diff --git a/app/Http/Controllers/Api/CloudProviderTokensController.php b/app/Http/Controllers/Api/CloudProviderTokensController.php index 5be82a31c..5ca7212fd 100644 --- a/app/Http/Controllers/Api/CloudProviderTokensController.php +++ b/app/Http/Controllers/Api/CloudProviderTokensController.php @@ -176,6 +176,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)); } @@ -242,6 +243,7 @@ class CloudProviderTokensController extends Controller if (is_null($teamId)) { return invalidTokenResponse(); } + $this->authorize('create', [CloudProviderToken::class]); $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { @@ -386,6 +388,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))); @@ -459,6 +462,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); diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php index f6a6b3513..b5cabcde0 100644 --- a/app/Http/Controllers/Api/GithubController.php +++ b/app/Http/Controllers/Api/GithubController.php @@ -180,6 +180,7 @@ class GithubController extends Controller if (is_null($teamId)) { return invalidTokenResponse(); } + $this->authorize('create', [GithubApp::class]); $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; @@ -555,6 +556,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 = [ @@ -721,6 +723,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 2645c2df1..761e951ea 100644 --- a/app/Http/Controllers/Api/HetznerController.php +++ b/app/Http/Controllers/Api/HetznerController.php @@ -548,6 +548,7 @@ class HetznerController extends Controller if (is_null($teamId)) { return invalidTokenResponse(); } + $this->authorize('create', [Server::class]); $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index da553a68c..33b28fc59 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -96,6 +96,7 @@ class ProjectController extends Controller if (! $project) { return response()->json(['message' => 'Project not found.'], 404); } + $this->authorize('view', $project); $project->load(['environments']); @@ -232,6 +233,7 @@ class ProjectController extends Controller if (is_null($teamId)) { return invalidTokenResponse(); } + $this->authorize('create', [Project::class]); $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { @@ -378,6 +380,7 @@ class ProjectController extends Controller if (! $project) { return response()->json(['message' => 'Project not found.'], 404); } + $this->authorize('update', $project); $project->update($request->only($allowedFields)); @@ -455,6 +458,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); } @@ -630,6 +634,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) { @@ -717,6 +722,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 e7b36cb9a..4fe738871 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -109,6 +109,7 @@ class SecurityController extends Controller 'message' => 'Private Key not found.', ], 404); } + $this->authorize('view', $key); return response()->json($this->removeSensitiveData($key)); } @@ -175,6 +176,7 @@ class SecurityController extends Controller if (is_null($teamId)) { return invalidTokenResponse(); } + $this->authorize('create', [PrivateKey::class]); $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { return $return; @@ -330,6 +332,7 @@ class SecurityController extends Controller 'message' => 'Private Key not found.', ], 404); } + $this->authorize('update', $foundKey); $foundKey->update($request->all()); return response()->json(serializeApiResponse([ @@ -406,6 +409,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/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 892457925..08271c6cb 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -145,6 +145,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 = [ @@ -465,6 +466,7 @@ class ServersController extends Controller if (is_null($teamId)) { return invalidTokenResponse(); } + $this->authorize('create', [ModelsServer::class]); $return = validateIncomingRequest($request); if ($return instanceof \Illuminate\Http\JsonResponse) { @@ -665,6 +667,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(); @@ -758,6 +761,7 @@ class ServersController extends Controller if (! $server) { return response()->json(['message' => 'Server not found.'], 404); } + $this->authorize('delete', $server); if ($server->definedResources()->count() > 0) { return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400); } @@ -836,6 +840,7 @@ class ServersController extends Controller if (! $server) { return response()->json(['message' => 'Server not found.'], 404); } + $this->authorize('update', $server); ValidateServer::dispatch($server); return response()->json(['message' => 'Validation started.'], 201); diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 98b35f63e..199e172ac 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -35,6 +35,10 @@ class ServicesController extends Controller ]); } + 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 fd0282d96..bd0f48809 100644 --- a/app/Http/Controllers/Api/TeamController.php +++ b/app/Http/Controllers/Api/TeamController.php @@ -118,6 +118,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( @@ -176,6 +177,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/Middleware/ApiAbility.php b/app/Http/Middleware/ApiAbility.php index 324eeebaa..874e63825 100644 --- a/app/Http/Middleware/ApiAbility.php +++ b/app/Http/Middleware/ApiAbility.php @@ -6,9 +6,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/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php index b5f6d2929..c68bba3e8 100644 --- a/app/Livewire/Admin/Index.php +++ b/app/Livewire/Admin/Index.php @@ -45,6 +45,9 @@ class Index extends Component public function submitSearch() { + if (Auth::id() !== 0 && ! session('impersonating')) { + return redirect()->route('dashboard'); + } if ($this->search !== '') { $this->foundUsers = User::where(function ($query) { $query->where('name', 'like', "%{$this->search}%") @@ -55,6 +58,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 0f6f45d83..289f8c181 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -8,12 +8,15 @@ use App\Models\Project; use App\Models\Server; use App\Models\Team; use App\Services\ConfigurationRepository; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Collection; use Livewire\Component; use Visus\Cuid2\Cuid2; class Index extends Component { + use AuthorizesRequests; + protected $listeners = [ 'refreshBoardingIndex' => 'validateServer', 'prerequisitesInstalled' => 'handlePrerequisitesInstalled', @@ -172,6 +175,9 @@ class Index extends Component public function skipBoarding() { + if (auth()->user()?->isMember()) { + return redirect()->route('dashboard'); + } Team::find(currentTeam()->id)->update([ 'show_boarding' => false, ]); @@ -257,6 +263,7 @@ class Index extends Component ]); try { + $this->authorize('create', PrivateKey::class); $privateKey = PrivateKey::createAndStore([ 'name' => $this->privateKeyName, 'description' => $this->privateKeyDescription, @@ -280,6 +287,12 @@ class Index extends Component 'remoteServerUser' => 'required|string', ]); + 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) { diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php index 98cf72376..448a0d1bd 100644 --- a/app/Livewire/Destination/Show.php +++ b/app/Livewire/Destination/Show.php @@ -2,7 +2,6 @@ namespace App\Livewire\Destination; -use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; @@ -32,17 +31,12 @@ class Show extends Component $destination = StandaloneDocker::whereUuid($destination_uuid)->first() ?? SwarmDocker::whereUuid($destination_uuid)->firstOrFail(); - $ownedByTeam = Server::ownedByCurrentTeam()->each(function ($server) use ($destination) { - if ($server->standaloneDockers->contains($destination) || $server->swarmDockers->contains($destination)) { - $this->destination = $destination; - $this->syncData(); - } - }); - if ($ownedByTeam === false) { - return redirect()->route('destination.index'); - } + $this->authorize('view', $destination); + $this->destination = $destination; $this->syncData(); + } catch (\Illuminate\Auth\Access\AuthorizationException) { + abort(403); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php index a8c932912..78eafbeb4 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) { - if (! verifyPasswordConfirmation($password, $this)) { - return; - } - - $currentTeam = currentTeam(); - $currentTeam->delete(); - - $currentTeam->members->each(function ($user) use ($currentTeam) { - if ($user->id === Auth::id()) { + try { + if (! verifyPasswordConfirmation($password, $this)) { 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(); + $currentTeam = currentTeam(); + $this->authorize('delete', $currentTeam); - return redirectRoute($this, 'team.index'); + $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); + } } public function render() diff --git a/app/Livewire/Project/AddEmpty.php b/app/Livewire/Project/AddEmpty.php index 974f0608a..3430c69bb 100644 --- a/app/Livewire/Project/AddEmpty.php +++ b/app/Livewire/Project/AddEmpty.php @@ -4,11 +4,14 @@ 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,6 +32,7 @@ class AddEmpty extends Component public function submit() { try { + $this->authorize('create', Project::class); $this->validate(); $project = Project::create([ 'name' => $this->name, diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index 954670582..b8f5e891a 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(); } @@ -123,6 +125,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/Heading.php b/app/Livewire/Project/Application/Heading.php index a46b2f19c..eb5b5f06c 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -65,58 +65,66 @@ 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() @@ -127,45 +135,53 @@ class Heading extends Component public function stop() { - $this->authorize('deploy', $this->application); + try { + $this->authorize('deploy', $this->application); - $this->dispatch('info', 'Gracefully stopping application.
It could take a while depending on the application.'); - StopApplication::dispatch($this->application, false, $this->docker_cleanup); + $this->dispatch('info', 'Gracefully stopping application.
It could take a while depending on the application.'); + StopApplication::dispatch($this->application, false, $this->docker_cleanup); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function restart() { - $this->authorize('deploy', $this->application); + try { + $this->authorize('deploy', $this->application); - if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) { - $this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.
More information here: documentation'); + if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) { + $this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.
More information here: documentation'); - return; + return; + } + + $this->setDeploymentUuid(); + $result = queue_application_deployment( + application: $this->application, + deployment_uuid: $this->deploymentUuid, + restart_only: true, + ); + if ($result['status'] === 'queue_full') { + $this->dispatch('error', 'Deployment queue full', $result['message']); + + return; + } + if ($result['status'] === 'skipped') { + $this->dispatch('success', 'Deployment skipped', $result['message']); + + return; + } + + return $this->redirectRoute('project.application.deployment.show', [ + 'project_uuid' => $this->parameters['project_uuid'], + 'application_uuid' => $this->parameters['application_uuid'], + 'deployment_uuid' => $this->deploymentUuid, + 'environment_uuid' => $this->parameters['environment_uuid'], + ], navigate: false); + } catch (\Throwable $e) { + return handleError($e, $this); } - - $this->setDeploymentUuid(); - $result = queue_application_deployment( - application: $this->application, - deployment_uuid: $this->deploymentUuid, - restart_only: true, - ); - if ($result['status'] === 'queue_full') { - $this->dispatch('error', 'Deployment queue full', $result['message']); - - return; - } - if ($result['status'] === 'skipped') { - $this->dispatch('success', 'Deployment skipped', $result['message']); - - return; - } - - return $this->redirectRoute('project.application.deployment.show', [ - 'project_uuid' => $this->parameters['project_uuid'], - 'application_uuid' => $this->parameters['application_uuid'], - 'deployment_uuid' => $this->deploymentUuid, - 'environment_uuid' => $this->parameters['environment_uuid'], - ], navigate: false); } public function render() diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index 41f352c14..50acf76b2 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -215,24 +215,31 @@ 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); - $this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true); + $this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null) { - $this->authorize('deploy', $this->application); + try { + $this->authorize('deploy', $this->application); - $this->add($pull_request_id, $pull_request_html_url); - $this->deploy($pull_request_id, $pull_request_html_url); + $this->add($pull_request_id, $pull_request_html_url); + $this->deploy($pull_request_id, $pull_request_html_url); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false) { - $this->authorize('deploy', $this->application); - try { + $this->authorize('deploy', $this->application); $this->setDeploymentUuid(); $found = ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found && ! is_null($pull_request_html_url)) { @@ -291,9 +298,8 @@ 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/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 3b3e42619..58a181f2f 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -11,11 +11,14 @@ 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; @@ -91,6 +94,7 @@ class CloneMe extends Component public function clone(string $type) { try { + $this->authorize('create', Project::class); $this->validate([ 'selectedDestination' => 'required', 'newName' => ValidationPatterns::nameRules(), diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 35262d7b0..8b67105cc 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -201,6 +201,18 @@ class BackupEdit extends Component } } + public function backupNow() + { + try { + $this->authorize('manageBackups', $this->backup->database); + + \App\Jobs\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 44f903fcc..5ffe6f509 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -3,12 +3,15 @@ namespace App\Livewire\Project\Database; use App\Models\ScheduledDatabaseBackup; +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 +47,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) { + try { + $this->authorize('manageBackups', $this->database); + } catch (\Throwable $e) { + return handleError($e, $this); + } + if (! verifyPasswordConfirmation($password, $this)) { return; } 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 9de75c1c5..f4dfe73f2 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -46,6 +46,8 @@ class General extends Component public bool $isLogDrainEnabled = false; + public bool $isPasswordHiddenForMember = false; + public function getListeners() { $teamId = Auth::user()->currentTeam()->id; @@ -69,6 +71,13 @@ class General extends Component } catch (\Throwable $e) { return handleError($e, $this); } + + $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false; + if ($this->isPasswordHiddenForMember) { + $this->clickhouseAdminPassword = ''; + $this->dbUrl = null; + $this->dbUrlPublic = null; + } } protected function rules(): array diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index d35e57a9d..1143ba44b 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -50,6 +50,8 @@ class General extends Component public bool $enable_ssl = false; + public bool $isPasswordHiddenForMember = false; + public function getListeners() { $userId = Auth::id(); @@ -81,6 +83,13 @@ class General extends Component } catch (\Throwable $e) { return handleError($e, $this); } + + $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false; + if ($this->isPasswordHiddenForMember) { + $this->dragonflyPassword = ''; + $this->dbUrl = null; + $this->dbUrlPublic = null; + } } 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/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index adb4ccb5f..333a5f940 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -52,6 +52,8 @@ class General extends Component public bool $enable_ssl = false; + public bool $isPasswordHiddenForMember = false; + public function getListeners() { $userId = Auth::id(); @@ -83,6 +85,13 @@ class General extends Component } catch (\Throwable $e) { return handleError($e, $this); } + + $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false; + if ($this->isPasswordHiddenForMember) { + $this->keydbPassword = ''; + $this->dbUrl = null; + $this->dbUrlPublic = null; + } } protected function rules(): array diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 14240c82d..cd07368b2 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -58,6 +58,8 @@ class General extends Component public ?Carbon $certificateValidUntil = null; + public bool $isPasswordHiddenForMember = false; + public function getListeners() { $userId = Auth::id(); @@ -143,6 +145,14 @@ class General extends Component } catch (Exception $e) { return handleError($e, $this); } + + $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false; + if ($this->isPasswordHiddenForMember) { + $this->mariadbRootPassword = ''; + $this->mariadbPassword = ''; + $this->db_url = null; + $this->db_url_public = null; + } } 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 11419ec71..c9aa253bb 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -58,6 +58,8 @@ class General extends Component public ?Carbon $certificateValidUntil = null; + public bool $isPasswordHiddenForMember = false; + public function getListeners() { $userId = Auth::id(); @@ -143,6 +145,13 @@ class General extends Component } catch (Exception $e) { return handleError($e, $this); } + + $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false; + if ($this->isPasswordHiddenForMember) { + $this->mongoInitdbRootPassword = ''; + $this->db_url = null; + $this->db_url_public = null; + } } 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 4f0f5eb19..4c8c91713 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -60,6 +60,8 @@ class General extends Component public ?Carbon $certificateValidUntil = null; + public bool $isPasswordHiddenForMember = false; + public function getListeners() { $userId = Auth::id(); @@ -148,6 +150,14 @@ class General extends Component } catch (Exception $e) { return handleError($e, $this); } + + $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false; + if ($this->isPasswordHiddenForMember) { + $this->mysqlRootPassword = ''; + $this->mysqlPassword = ''; + $this->db_url = null; + $this->db_url_public = null; + } } 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 4e044672b..e6648f97f 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -68,6 +68,8 @@ class General extends Component public ?Carbon $certificateValidUntil = null; + public bool $isPasswordHiddenForMember = false; + public function getListeners() { $userId = Auth::id(); @@ -161,6 +163,13 @@ class General extends Component } catch (Exception $e) { return handleError($e, $this); } + + $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false; + if ($this->isPasswordHiddenForMember) { + $this->postgresPassword = ''; + $this->db_url = null; + $this->db_url_public = null; + } } 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 ebe2f3ba0..b16243b84 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -56,6 +56,8 @@ class General extends Component public ?Carbon $certificateValidUntil = null; + public bool $isPasswordHiddenForMember = false; + public function getListeners() { $userId = Auth::id(); @@ -136,6 +138,13 @@ class General extends Component } catch (\Throwable $e) { return handleError($e, $this); } + + $this->isPasswordHiddenForMember = auth()->user()?->isMember() ?? false; + if ($this->isPasswordHiddenForMember) { + $this->redisPassword = ''; + $this->dbUrl = null; + $this->dbUrlPublic = null; + } } public function syncData(bool $toModel = false) diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php index 1cf5e53f6..06473c56a 100644 --- a/app/Livewire/Project/Database/ScheduledBackups.php +++ b/app/Livewire/Project/Database/ScheduledBackups.php @@ -56,22 +56,30 @@ 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 aa6e95975..28027ce5a 100644 --- a/app/Livewire/Project/DeleteEnvironment.php +++ b/app/Livewire/Project/DeleteEnvironment.php @@ -30,18 +30,22 @@ class DeleteEnvironment extends Component public function delete() { - $this->validate([ - 'environment_id' => 'required|int', - ]); - $environment = Environment::findOrFail($this->environment_id); - $this->authorize('delete', $environment); + try { + $this->validate([ + 'environment_id' => 'required|int', + ]); + $environment = Environment::findOrFail($this->environment_id); + $this->authorize('delete', $environment); - if ($environment->isEmpty()) { - $environment->delete(); + if ($environment->isEmpty()) { + $environment->delete(); - return redirectRoute($this, 'project.show', ['project_uuid' => $this->parameters['project_uuid']]); + return redirectRoute($this, 'project.show', ['project_uuid' => $this->parameters['project_uuid']]); + } + + return $this->dispatch('error', "Environment {$environment->name} has defined resources, please delete them first."); + } catch (\Throwable $e) { + return handleError($e, $this); } - - return $this->dispatch('error', "Environment {$environment->name} has defined resources, please delete them first."); } } diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php index a018046fd..0b99f57a4 100644 --- a/app/Livewire/Project/DeleteProject.php +++ b/app/Livewire/Project/DeleteProject.php @@ -26,18 +26,22 @@ class DeleteProject extends Component public function delete() { - $this->validate([ - 'project_id' => 'required|int', - ]); - $project = Project::findOrFail($this->project_id); - $this->authorize('delete', $project); + try { + $this->validate([ + 'project_id' => 'required|int', + ]); + $project = Project::findOrFail($this->project_id); + $this->authorize('delete', $project); - if ($project->isEmpty()) { - $project->delete(); + if ($project->isEmpty()) { + $project->delete(); - return redirectRoute($this, 'project.index'); + return redirectRoute($this, 'project.index'); + } + + return $this->dispatch('error', "Project {$project->name} has resources defined, please delete them first."); + } catch (\Throwable $e) { + return handleError($e, $this); } - - return $this->dispatch('error', "Project {$project->name} has resources defined, please delete them first."); } } diff --git a/app/Livewire/Project/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/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 079115bb6..596a2646c 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -101,8 +101,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 c8a08d8f9..20502393b 100644 --- a/app/Livewire/Project/Service/Heading.php +++ b/app/Livewire/Project/Service/Heading.php @@ -7,12 +7,15 @@ use App\Actions\Service\StartService; use App\Actions\Service\StopService; use App\Enums\ProcessStatus; use App\Models\Service; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; use Livewire\Component; use Spatie\Activitylog\Models\Activity; class Heading extends Component { + use AuthorizesRequests; + public Service $service; public array $parameters; @@ -99,13 +102,20 @@ class Heading extends Component public function start() { - $activity = StartService::run($this->service, pullLatestImages: true); - $this->dispatch('activityMonitor', $activity->id); + try { + $this->authorize('deploy', $this->service); + $activity = StartService::run($this->service, pullLatestImages: true); + $this->js("window.dispatchEvent(new CustomEvent('startservice'))"); + $this->dispatch('activityMonitor', $activity->id); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function forceDeploy() { try { + $this->authorize('deploy', $this->service); $activities = Activity::where('properties->type_uuid', $this->service->uuid) ->where(function ($q) { $q->where('properties->status', ProcessStatus::IN_PROGRESS->value) @@ -116,43 +126,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() { try { + $this->authorize('stop', $this->service); StopService::dispatch($this->service, false, $this->docker_cleanup); - } catch (\Exception $e) { - $this->dispatch('error', $e->getMessage()); + } catch (\Throwable $e) { + return handleError($e, $this); } } public function restart() { - $this->checkDeployments(); - if ($this->isDeploymentProgress) { - $this->dispatch('error', 'There is a deployment in progress.'); + try { + $this->authorize('deploy', $this->service); + $this->checkDeployments(); + if ($this->isDeploymentProgress) { + $this->dispatch('error', 'There is a deployment in progress.'); - return; + return; + } + $activity = StartService::run($this->service, stopBeforeStart: true); + $this->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->checkDeployments(); - if ($this->isDeploymentProgress) { - $this->dispatch('error', 'There is a deployment in progress.'); + try { + $this->authorize('deploy', $this->service); + $this->checkDeployments(); + if ($this->isDeploymentProgress) { + $this->dispatch('error', 'There is a deployment in progress.'); - return; + return; + } + $activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true); + $this->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); } public function render() 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/Destination.php b/app/Livewire/Project/Shared/Destination.php index 7ab81b7d1..1eb1dc580 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -7,12 +7,15 @@ use App\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 +62,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,6 +74,7 @@ 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'); @@ -110,15 +115,20 @@ class Destination extends Component public function promote(int $network_id, int $server_id) { - $main_destination = $this->resource->destination; - $this->resource->update([ - 'destination_id' => $network_id, - 'destination_type' => StandaloneDocker::class, - ]); - $this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]); - $this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]); - $this->refreshServers(); - $this->resource->refresh(); + try { + $this->authorize('update', $this->resource); + $main_destination = $this->resource->destination; + $this->resource->update([ + 'destination_id' => $network_id, + 'destination_type' => StandaloneDocker::class, + ]); + $this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]); + $this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]); + $this->refreshServers(); + $this->resource->refresh(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function refreshServers() @@ -130,13 +140,19 @@ class Destination extends Component public function addServer(int $network_id, int $server_id) { - $this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]); - $this->dispatch('refresh'); + try { + $this->authorize('update', $this->resource); + $this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]); + $this->dispatch('refresh'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function removeServer(int $network_id, int $server_id, $password) { try { + $this->authorize('update', $this->resource); if (! verifyPasswordConfirmation($password, $this)) { return; } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index f250a860b..a69a41d90 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -72,7 +72,7 @@ class All extends Component $query->orderBy('order'); } - return $query->get(); + return $this->nullLockedValues($query->get()); } public function getEnvironmentVariablesPreviewProperty() @@ -86,7 +86,21 @@ class All extends Component $query->orderBy('order'); } - return $query->get(); + return $this->nullLockedValues($query->get()); + } + + 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 getHardcodedEnvironmentVariablesProperty() @@ -155,7 +169,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 2a18be13c..74f3606e7 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -56,6 +56,8 @@ class Show extends Component public bool $is_redis_credential = false; + public bool $isValueHidden = false; + public array $problematicVariables = []; protected $listeners = [ @@ -142,6 +144,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/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php index 0d5d71b45..6eaf0cdd0 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 e769e4bcb..25545c4b0 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -47,224 +47,87 @@ class ResourceOperations extends Component public function cloneTo($destination_id) { - $this->authorize('update', $this->resource); + try { + $this->authorize('update', $this->resource); - $teamScope = fn ($q) => $q->where('team_id', currentTeam()->id); - $new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id); - if (! $new_destination) { - $new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id); - } - if (! $new_destination) { - return $this->addError('destination_id', 'Destination not found.'); - } - $uuid = (string) new Cuid2; - $server = $new_destination->server; - - if ($this->resource->getMorphClass() === \App\Models\Application::class) { - $new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData); - - $route = route('project.application.configuration', [ - 'project_uuid' => $this->projectUuid, - 'environment_uuid' => $this->environmentUuid, - 'application_uuid' => $new_resource->uuid, - ]).'#resource-operations'; - - return redirect()->to($route); - } elseif ( - $this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class || - $this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class || - $this->resource->getMorphClass() === \App\Models\StandaloneMysql::class || - $this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class || - $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class || - $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class || - $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class || - $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class - ) { + $teamScope = fn ($q) => $q->where('team_id', currentTeam()->id); + $new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id); + if (! $new_destination) { + $new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id); + } + if (! $new_destination) { + return $this->addError('destination_id', 'Destination not found.'); + } $uuid = (string) new Cuid2; - $new_resource = $this->resource->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'uuid' => $uuid, - 'name' => $this->resource->name.'-clone-'.$uuid, - 'status' => 'exited', - 'started_at' => null, - 'destination_id' => $new_destination->id, - ]); - $new_resource->save(); + $server = $new_destination->server; - $tags = $this->resource->tags; - foreach ($tags as $tag) { - $new_resource->tags()->attach($tag->id); - } + if ($this->resource->getMorphClass() === \App\Models\Application::class) { + $new_resource = clone_application($this->resource, $new_destination, ['uuid' => $uuid], $this->cloneVolumeData); - $new_resource->persistentStorages()->delete(); - $persistentVolumes = $this->resource->persistentStorages()->get(); - foreach ($persistentVolumes as $volume) { - $originalName = $volume->name; - $newName = ''; + $route = route('project.application.configuration', [ + 'project_uuid' => $this->projectUuid, + 'environment_uuid' => $this->environmentUuid, + 'application_uuid' => $new_resource->uuid, + ]).'#resource-operations'; - if (str_starts_with($originalName, 'postgres-data-')) { - $newName = 'postgres-data-'.$new_resource->uuid; - } elseif (str_starts_with($originalName, 'mysql-data-')) { - $newName = 'mysql-data-'.$new_resource->uuid; - } elseif (str_starts_with($originalName, 'redis-data-')) { - $newName = 'redis-data-'.$new_resource->uuid; - } elseif (str_starts_with($originalName, 'clickhouse-data-')) { - $newName = 'clickhouse-data-'.$new_resource->uuid; - } elseif (str_starts_with($originalName, 'mariadb-data-')) { - $newName = 'mariadb-data-'.$new_resource->uuid; - } elseif (str_starts_with($originalName, 'mongodb-data-')) { - $newName = 'mongodb-data-'.$new_resource->uuid; - } elseif (str_starts_with($originalName, 'keydb-data-')) { - $newName = 'keydb-data-'.$new_resource->uuid; - } elseif (str_starts_with($originalName, 'dragonfly-data-')) { - $newName = 'dragonfly-data-'.$new_resource->uuid; - } else { - if (str_starts_with($volume->name, $this->resource->uuid)) { - $newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid); - } else { - $newName = $new_resource->uuid.'-'.$volume->name; - } - } - - $newPersistentVolume = $volume->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'name' => $newName, - 'resource_id' => $new_resource->id, - ]); - $newPersistentVolume->save(); - - if ($this->cloneVolumeData) { - try { - StopDatabase::dispatch($this->resource); - $sourceVolume = $volume->name; - $targetVolume = $newPersistentVolume->name; - $sourceServer = $this->resource->destination->server; - $targetServer = $new_resource->destination->server; - - VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); - - StartDatabase::dispatch($this->resource); - } catch (\Exception $e) { - \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); - } - } - } - - $fileStorages = $this->resource->fileStorages()->get(); - foreach ($fileStorages as $storage) { - $newStorage = $storage->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'resource_id' => $new_resource->id, - ]); - $newStorage->save(); - } - - $scheduledBackups = $this->resource->scheduledBackups()->get(); - foreach ($scheduledBackups as $backup) { + return redirect()->to($route); + } elseif ( + $this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMysql::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class || + $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class || + $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class + ) { $uuid = (string) new Cuid2; - $newBackup = $backup->replicate([ + $new_resource = $this->resource->replicate([ 'id', 'created_at', 'updated_at', ])->fill([ 'uuid' => $uuid, - 'database_id' => $new_resource->id, - 'database_type' => $new_resource->getMorphClass(), - 'team_id' => currentTeam()->id, - ]); - $newBackup->save(); - } - - $environmentVaribles = $this->resource->environment_variables()->get(); - foreach ($environmentVaribles as $environmentVarible) { - $payload = [ - 'resourceable_id' => $new_resource->id, - 'resourceable_type' => $new_resource->getMorphClass(), - ]; - $newEnvironmentVariable = $environmentVarible->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill($payload); - $newEnvironmentVariable->save(); - } - - $route = route('project.database.configuration', [ - 'project_uuid' => $this->projectUuid, - 'environment_uuid' => $this->environmentUuid, - 'database_uuid' => $new_resource->uuid, - ]).'#resource-operations'; - - return redirect()->to($route); - } elseif ($this->resource->type() === 'service') { - $uuid = (string) new Cuid2; - $new_resource = $this->resource->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'uuid' => $uuid, - 'name' => $this->resource->name.'-clone-'.$uuid, - 'destination_id' => $new_destination->id, - 'destination_type' => $new_destination->getMorphClass(), - 'server_id' => $new_destination->server_id, // server_id is probably not needed anymore because of the new polymorphic relationships (here it is needed for clone to a different server to work - but maybe we can drop the column) - ]); - - $new_resource->save(); - - $tags = $this->resource->tags; - foreach ($tags as $tag) { - $new_resource->tags()->attach($tag->id); - } - - $scheduledTasks = $this->resource->scheduled_tasks()->get(); - foreach ($scheduledTasks as $task) { - $newTask = $task->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'uuid' => (string) new Cuid2, - 'service_id' => $new_resource->id, - 'team_id' => currentTeam()->id, - ]); - $newTask->save(); - } - - $environmentVariables = $this->resource->environment_variables()->get(); - foreach ($environmentVariables as $environmentVariable) { - $newEnvironmentVariable = $environmentVariable->replicate([ - 'id', - 'created_at', - 'updated_at', - ])->fill([ - 'resourceable_id' => $new_resource->id, - 'resourceable_type' => $new_resource->getMorphClass(), - ]); - $newEnvironmentVariable->save(); - } - - foreach ($new_resource->applications() as $application) { - $application->update([ + 'name' => $this->resource->name.'-clone-'.$uuid, 'status' => 'exited', + 'started_at' => null, + 'destination_id' => $new_destination->id, ]); + $new_resource->save(); - $persistentVolumes = $application->persistentStorages()->get(); + $tags = $this->resource->tags; + foreach ($tags as $tag) { + $new_resource->tags()->attach($tag->id); + } + + $new_resource->persistentStorages()->delete(); + $persistentVolumes = $this->resource->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { + $originalName = $volume->name; $newName = ''; - if (str_starts_with($volume->name, $volume->resource->uuid)) { - $newName = str($volume->name)->replace($volume->resource->uuid, $application->uuid); + + if (str_starts_with($originalName, 'postgres-data-')) { + $newName = 'postgres-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'mysql-data-')) { + $newName = 'mysql-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'redis-data-')) { + $newName = 'redis-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'clickhouse-data-')) { + $newName = 'clickhouse-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'mariadb-data-')) { + $newName = 'mariadb-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'mongodb-data-')) { + $newName = 'mongodb-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'keydb-data-')) { + $newName = 'keydb-data-'.$new_resource->uuid; + } elseif (str_starts_with($originalName, 'dragonfly-data-')) { + $newName = 'dragonfly-data-'.$new_resource->uuid; } else { - $newName = $application->uuid.'-'.str($volume->name)->afterLast('-'); + if (str_starts_with($volume->name, $this->resource->uuid)) { + $newName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid); + } else { + $newName = $new_resource->uuid.'-'.$volume->name; + } } $newPersistentVolume = $volume->replicate([ @@ -273,79 +136,220 @@ class ResourceOperations extends Component 'updated_at', ])->fill([ 'name' => $newName, - 'resource_id' => $application->id, + 'resource_id' => $new_resource->id, ]); $newPersistentVolume->save(); if ($this->cloneVolumeData) { try { - StopService::dispatch($application); + StopDatabase::dispatch($this->resource); $sourceVolume = $volume->name; $targetVolume = $newPersistentVolume->name; - $sourceServer = $application->service->destination->server; + $sourceServer = $this->resource->destination->server; $targetServer = $new_resource->destination->server; VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); - StartService::dispatch($application); + StartDatabase::dispatch($this->resource); } catch (\Exception $e) { \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); } } } - } - foreach ($new_resource->databases() as $database) { - $database->update([ - 'status' => 'exited', - ]); - - $persistentVolumes = $database->persistentStorages()->get(); - foreach ($persistentVolumes as $volume) { - $newName = ''; - if (str_starts_with($volume->name, $volume->resource->uuid)) { - $newName = str($volume->name)->replace($volume->resource->uuid, $database->uuid); - } else { - $newName = $database->uuid.'-'.str($volume->name)->afterLast('-'); - } - - $newPersistentVolume = $volume->replicate([ + $fileStorages = $this->resource->fileStorages()->get(); + foreach ($fileStorages as $storage) { + $newStorage = $storage->replicate([ 'id', 'created_at', 'updated_at', ])->fill([ - 'name' => $newName, - 'resource_id' => $database->id, + 'resource_id' => $new_resource->id, ]); - $newPersistentVolume->save(); + $newStorage->save(); + } - if ($this->cloneVolumeData) { - try { - StopService::dispatch($database->service); - $sourceVolume = $volume->name; - $targetVolume = $newPersistentVolume->name; - $sourceServer = $database->service->destination->server; - $targetServer = $new_resource->destination->server; + $scheduledBackups = $this->resource->scheduledBackups()->get(); + foreach ($scheduledBackups as $backup) { + $uuid = (string) new Cuid2; + $newBackup = $backup->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => $uuid, + 'database_id' => $new_resource->id, + 'database_type' => $new_resource->getMorphClass(), + 'team_id' => currentTeam()->id, + ]); + $newBackup->save(); + } - VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + $environmentVaribles = $this->resource->environment_variables()->get(); + foreach ($environmentVaribles as $environmentVarible) { + $payload = [ + 'resourceable_id' => $new_resource->id, + 'resourceable_type' => $new_resource->getMorphClass(), + ]; + $newEnvironmentVariable = $environmentVarible->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill($payload); + $newEnvironmentVariable->save(); + } - StartService::dispatch($database->service); - } catch (\Exception $e) { - \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + $route = route('project.database.configuration', [ + 'project_uuid' => $this->projectUuid, + 'environment_uuid' => $this->environmentUuid, + 'database_uuid' => $new_resource->uuid, + ]).'#resource-operations'; + + return redirect()->to($route); + } elseif ($this->resource->type() === 'service') { + $uuid = (string) new Cuid2; + $new_resource = $this->resource->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => $uuid, + 'name' => $this->resource->name.'-clone-'.$uuid, + 'destination_id' => $new_destination->id, + 'destination_type' => $new_destination->getMorphClass(), + 'server_id' => $new_destination->server_id, // server_id is probably not needed anymore because of the new polymorphic relationships (here it is needed for clone to a different server to work - but maybe we can drop the column) + ]); + + $new_resource->save(); + + $tags = $this->resource->tags; + foreach ($tags as $tag) { + $new_resource->tags()->attach($tag->id); + } + + $scheduledTasks = $this->resource->scheduled_tasks()->get(); + foreach ($scheduledTasks as $task) { + $newTask = $task->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'uuid' => (string) new Cuid2, + 'service_id' => $new_resource->id, + 'team_id' => currentTeam()->id, + ]); + $newTask->save(); + } + + $environmentVariables = $this->resource->environment_variables()->get(); + foreach ($environmentVariables as $environmentVariable) { + $newEnvironmentVariable = $environmentVariable->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'resourceable_id' => $new_resource->id, + 'resourceable_type' => $new_resource->getMorphClass(), + ]); + $newEnvironmentVariable->save(); + } + + foreach ($new_resource->applications() as $application) { + $application->update([ + 'status' => 'exited', + ]); + + $persistentVolumes = $application->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $newName = ''; + if (str_starts_with($volume->name, $volume->resource->uuid)) { + $newName = str($volume->name)->replace($volume->resource->uuid, $application->uuid); + } else { + $newName = $application->uuid.'-'.str($volume->name)->afterLast('-'); + } + + $newPersistentVolume = $volume->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'name' => $newName, + 'resource_id' => $application->id, + ]); + $newPersistentVolume->save(); + + if ($this->cloneVolumeData) { + try { + StopService::dispatch($application); + $sourceVolume = $volume->name; + $targetVolume = $newPersistentVolume->name; + $sourceServer = $application->service->destination->server; + $targetServer = $new_resource->destination->server; + + VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + + StartService::dispatch($application); + } catch (\Exception $e) { + \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } } } } + + foreach ($new_resource->databases() as $database) { + $database->update([ + 'status' => 'exited', + ]); + + $persistentVolumes = $database->persistentStorages()->get(); + foreach ($persistentVolumes as $volume) { + $newName = ''; + if (str_starts_with($volume->name, $volume->resource->uuid)) { + $newName = str($volume->name)->replace($volume->resource->uuid, $database->uuid); + } else { + $newName = $database->uuid.'-'.str($volume->name)->afterLast('-'); + } + + $newPersistentVolume = $volume->replicate([ + 'id', + 'created_at', + 'updated_at', + ])->fill([ + 'name' => $newName, + 'resource_id' => $database->id, + ]); + $newPersistentVolume->save(); + + if ($this->cloneVolumeData) { + try { + StopService::dispatch($database->service); + $sourceVolume = $volume->name; + $targetVolume = $newPersistentVolume->name; + $sourceServer = $database->service->destination->server; + $targetServer = $new_resource->destination->server; + + VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume); + + StartService::dispatch($database->service); + } catch (\Exception $e) { + \Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage()); + } + } + } + } + + $new_resource->parse(); + + $route = route('project.service.configuration', [ + 'project_uuid' => $this->projectUuid, + 'environment_uuid' => $this->environmentUuid, + 'service_uuid' => $new_resource->uuid, + ]).'#resource-operations'; + + return redirect()->to($route); } - - $new_resource->parse(); - - $route = route('project.service.configuration', [ - 'project_uuid' => $this->projectUuid, - 'environment_uuid' => $this->environmentUuid, - 'service_uuid' => $new_resource->uuid, - ]).'#resource-operations'; - - return redirect()->to($route); + } catch (\Throwable $e) { + return handleError($e, $this); } } diff --git a/app/Livewire/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/Show.php b/app/Livewire/Project/Show.php index e884abb4e..c86fa377d 100644 --- a/app/Livewire/Project/Show.php +++ b/app/Livewire/Project/Show.php @@ -5,11 +5,14 @@ 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,6 +44,7 @@ class Show extends Component public function submit() { try { + $this->authorize('create', Environment::class); $this->validate(); $environment = Environment::create([ 'name' => $this->name, diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index a263acedf..97d2bfb44 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -23,6 +23,10 @@ class ApiTokens extends Component public bool $canUseWritePermissions = false; + public bool $canUseDeployPermissions = false; + + public bool $canUseSensitivePermissions = false; + public function render() { return view('livewire.security.api-tokens'); @@ -33,6 +37,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(); } @@ -43,23 +49,35 @@ class ApiTokens extends Component public function updatedPermissions($permissionToUpdate) { - // Check if user is trying to use restricted permissions - if ($permissionToUpdate == 'root' && ! $this->canUseRootPermissions) { + // 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; } - if (in_array($permissionToUpdate, ['write', 'write:sensitive']) && ! $this->canUseWritePermissions) { + if (in_array($permissionToUpdate, ['write', 'write:sensitive']) && ! 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)) { @@ -79,20 +97,31 @@ class ApiTokens extends Component try { $this->authorize('create', PersonalAccessToken::class); - // Validate permissions based on user role - if (in_array('root', $this->permissions) && ! $this->canUseRootPermissions) { + // 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) && ! auth()->user()->can('useRootPermissions', PersonalAccessToken::class)) { throw new \Exception('You do not have permission to create tokens with root permissions.'); } - if (array_intersect(['write', 'write:sensitive'], $this->permissions) && ! $this->canUseWritePermissions) { + if (array_intersect(['write', 'write:sensitive'], $this->permissions) && ! auth()->user()->can('useWritePermissions', PersonalAccessToken::class)) { throw new \Exception('You do not have permission to create tokens with write permissions.'); } + if (in_array('deploy', $this->permissions) && ! 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) && ! 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', ]); $token = auth()->user()->createToken($this->description, array_values($this->permissions)); $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..5e4ca9853 100644 --- a/app/Livewire/Security/CloudInitScriptForm.php +++ b/app/Livewire/Security/CloudInitScriptForm.php @@ -20,15 +20,19 @@ class CloudInitScriptForm extends Component public function mount(?int $scriptId = null) { - if ($scriptId) { - $this->scriptId = $scriptId; - $cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId); - $this->authorize('update', $cloudInitScript); + try { + if ($scriptId) { + $this->scriptId = $scriptId; + $cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId); + $this->authorize('update', $cloudInitScript); - $this->name = $cloudInitScript->name; - $this->script = $cloudInitScript->script; - } else { - $this->authorize('create', CloudInitScript::class); + $this->name = $cloudInitScript->name; + $this->script = $cloudInitScript->script; + } else { + $this->authorize('create', CloudInitScript::class); + } + } catch (\Throwable $e) { + return handleError($e, $this); } } diff --git a/app/Livewire/Security/CloudProviderTokenForm.php b/app/Livewire/Security/CloudProviderTokenForm.php index 7affb1531..ec4513ff3 100644 --- a/app/Livewire/Security/CloudProviderTokenForm.php +++ b/app/Livewire/Security/CloudProviderTokenForm.php @@ -21,7 +21,11 @@ class CloudProviderTokenForm extends Component public function mount() { - $this->authorize('create', CloudProviderToken::class); + try { + $this->authorize('create', CloudProviderToken::class); + } catch (\Throwable $e) { + return handleError($e, $this); + } } protected function rules(): array diff --git a/app/Livewire/Security/CloudProviderTokens.php b/app/Livewire/Security/CloudProviderTokens.php index cfef30772..b7f389534 100644 --- a/app/Livewire/Security/CloudProviderTokens.php +++ b/app/Livewire/Security/CloudProviderTokens.php @@ -14,8 +14,12 @@ class CloudProviderTokens extends Component public function mount() { - $this->authorize('viewAny', CloudProviderToken::class); - $this->loadTokens(); + try { + $this->authorize('viewAny', CloudProviderToken::class); + $this->loadTokens(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function getListeners() diff --git a/app/Livewire/Security/PrivateKey/Index.php b/app/Livewire/Security/PrivateKey/Index.php index 1eb66ae3e..0362b65fa 100644 --- a/app/Livewire/Security/PrivateKey/Index.php +++ b/app/Livewire/Security/PrivateKey/Index.php @@ -21,8 +21,12 @@ 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/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 117b43ad6..622bc1d1e 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 f1ffa60f2..e8df99d65 100644 --- a/app/Livewire/Server/New/ByHetzner.php +++ b/app/Livewire/Server/New/ByHetzner.php @@ -78,14 +78,18 @@ class ByHetzner extends Component public function mount() { - $this->authorize('viewAny', CloudProviderToken::class); - $this->loadTokens(); - $this->loadSavedCloudInitScripts(); - $this->server_name = generate_random_name(); - $this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get(); + try { + $this->authorize('viewAny', CloudProviderToken::class); + $this->loadTokens(); + $this->loadSavedCloudInitScripts(); + $this->server_name = generate_random_name(); + $this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get(); - if ($this->private_keys->count() > 0) { - $this->private_key_id = $this->private_keys->first()->id; + if ($this->private_keys->count() > 0) { + $this->private_key_id = $this->private_keys->first()->id; + } + } catch (\Throwable $e) { + return handleError($e, $this); } } diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 1a14baf89..6c163a112 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -96,11 +96,15 @@ 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 c67591cf5..f7db1257c 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php @@ -22,34 +22,38 @@ class DynamicConfigurationNavbar extends Component public function delete(string $fileName) { - $this->authorize('update', $this->server); - $proxy_path = $this->server->proxyPath(); - $proxy_type = $this->server->proxyType(); + try { + $this->authorize('update', $this->server); + $proxy_path = $this->server->proxyPath(); + $proxy_type = $this->server->proxyType(); - // Decode filename: pipes are used to encode dots for Livewire property binding - // (e.g., 'my|service.yaml' -> 'my.service.yaml') - // This must happen BEFORE validation because validateShellSafePath() correctly - // rejects pipe characters as dangerous shell metacharacters - $file = str_replace('|', '.', $fileName); + // Decode filename: pipes are used to encode dots for Livewire property binding + // (e.g., 'my|service.yaml' -> 'my.service.yaml') + // This must happen BEFORE validation because validateShellSafePath() correctly + // rejects pipe characters as dangerous shell metacharacters + $file = str_replace('|', '.', $fileName); - // Validate filename to prevent command injection - validateShellSafePath($file, 'proxy configuration filename'); + // Validate filename to prevent command injection + validateShellSafePath($file, 'proxy configuration filename'); - if ($proxy_type === 'CADDY' && $file === 'Caddyfile') { - $this->dispatch('error', 'Cannot delete Caddyfile.'); + if ($proxy_type === 'CADDY' && $file === 'Caddyfile') { + $this->dispatch('error', 'Cannot delete Caddyfile.'); - return; + return; + } + + $fullPath = "{$proxy_path}/dynamic/{$file}"; + $escapedPath = escapeshellarg($fullPath); + instant_remote_process(["rm -f {$escapedPath}"], $this->server); + if ($proxy_type === 'CADDY') { + $this->server->reloadCaddy(); + } + $this->dispatch('success', 'File deleted.'); + $this->dispatch('loadDynamicConfigurations'); + $this->dispatch('refresh'); + } catch (\Throwable $e) { + return handleError($e, $this); } - - $fullPath = "{$proxy_path}/dynamic/{$file}"; - $escapedPath = escapeshellarg($fullPath); - instant_remote_process(["rm -f {$escapedPath}"], $this->server); - if ($proxy_type === 'CADDY') { - $this->server->reloadCaddy(); - } - $this->dispatch('success', 'File deleted.'); - $this->dispatch('loadDynamicConfigurations'); - $this->dispatch('refresh'); } public function render() diff --git a/app/Livewire/Server/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 a21b0372b..31e57b301 100644 --- a/app/Livewire/Server/Resources.php +++ b/app/Livewire/Server/Resources.php @@ -29,23 +29,38 @@ class Resources extends Component public function startUnmanaged($id) { - $this->server->startUnmanaged($id); - $this->dispatch('success', 'Container started.'); - $this->loadUnmanagedContainers(); + try { + $this->authorize('update', $this->server); + $this->server->startUnmanaged($id); + $this->dispatch('success', 'Container started.'); + $this->loadUnmanagedContainers(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function restartUnmanaged($id) { - $this->server->restartUnmanaged($id); - $this->dispatch('success', 'Container restarted.'); - $this->loadUnmanagedContainers(); + try { + $this->authorize('update', $this->server); + $this->server->restartUnmanaged($id); + $this->dispatch('success', 'Container restarted.'); + $this->loadUnmanagedContainers(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function stopUnmanaged($id) { - $this->server->stopUnmanaged($id); - $this->dispatch('success', 'Container stopped.'); - $this->loadUnmanagedContainers(); + try { + $this->authorize('update', $this->server); + $this->server->stopUnmanaged($id); + $this->dispatch('success', 'Container stopped.'); + $this->loadUnmanagedContainers(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function refreshStatus() diff --git a/app/Livewire/Server/Security/Patches.php b/app/Livewire/Server/Security/Patches.php index b4d151424..087836da3 100644 --- a/app/Livewire/Server/Security/Patches.php +++ b/app/Livewire/Server/Security/Patches.php @@ -41,7 +41,11 @@ 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/Show.php b/app/Livewire/Server/Show.php index edc17004c..21703c4aa 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -287,18 +287,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); } } @@ -401,6 +405,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.'); @@ -465,6 +470,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 1a5bd381b..9b0f02573 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -72,31 +72,39 @@ 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) { - $this->error = 'Server is not reachable. Please validate your configuration and connection.
Check this documentation for further help.

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

Error: '.$error.'
'; + $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 ad478273f..c63e9b2f7 100644 --- a/app/Livewire/Settings/Advanced.php +++ b/app/Livewire/Settings/Advanced.php @@ -4,11 +4,14 @@ namespace App\Livewire\Settings; use App\Models\InstanceSettings; 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')] @@ -72,6 +75,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(); @@ -141,6 +145,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; @@ -159,6 +164,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 9a51d107d..5a7c74172 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; @@ -86,6 +89,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; @@ -103,6 +107,7 @@ class Index extends Component public function confirmDomainUsage() { + $this->authorize('update', $this->settings); $this->forceSaveDomains = true; $this->showDomainConflictModal = false; $this->submit(); @@ -111,6 +116,7 @@ class Index extends Component public function submit() { try { + $this->authorize('update', $this->settings); $error_show = false; $this->resetErrorBag(); @@ -172,6 +178,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 01a67c38c..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; @@ -25,6 +28,9 @@ class Updates extends Component public function mount() { + if (! isInstanceAdmin()) { + return redirect()->route('dashboard'); + } if (! isCloud()) { $this->server = Server::findOrFail(0); } @@ -38,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'], @@ -56,6 +63,7 @@ class Updates extends Component public function submit() { try { + $this->authorize('update', $this->settings); $this->resetErrorBag(); $this->validate(); @@ -88,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 84f5c6081..cfd188b40 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -7,12 +7,15 @@ use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; 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; @@ -76,6 +79,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); @@ -120,14 +124,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 ca48e9b16..5ed012b37 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 e1b230218..503fc3ead 100644 --- a/app/Livewire/SharedVariables/Environment/Show.php +++ b/app/Livewire/SharedVariables/Environment/Show.php @@ -73,7 +73,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 1f304b543..00646fdbe 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 75fd415e1..9ad37dea1 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 4dc0b6ae2..bc1726f10 100644 --- a/app/Livewire/Storage/Form.php +++ b/app/Livewire/Storage/Form.php @@ -31,6 +31,8 @@ class Form extends Component public ?bool $isUsable = null; + public bool $isPasswordHiddenForMember = false; + protected function rules(): array { return [ @@ -109,6 +111,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/Show.php b/app/Livewire/Storage/Show.php index fdf3d0d28..fd8c12292 100644 --- a/app/Livewire/Storage/Show.php +++ b/app/Livewire/Storage/Show.php @@ -18,7 +18,11 @@ class Show extends Component if (! $this->storage) { abort(404); } - $this->authorize('view', $this->storage); + try { + $this->authorize('view', $this->storage); + } catch (\Illuminate\Auth\Access\AuthorizationException) { + return $this->redirectRoute('storage.index', navigate: true); + } } public function render() 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 c8d44d42b..cbc8f67b6 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/Upgrade.php b/app/Livewire/Upgrade.php index 7948ad6a9..be4ea1cf0 100644 --- a/app/Livewire/Upgrade.php +++ b/app/Livewire/Upgrade.php @@ -44,6 +44,9 @@ class Upgrade extends Component public function upgrade() { try { + if (! isInstanceAdmin()) { + abort(403); + } if ($this->updateInProgress) { return; } diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php index 3aae55966..5d1c3b8b5 100644 --- a/app/Models/S3Storage.php +++ b/app/Models/S3Storage.php @@ -127,21 +127,25 @@ class S3Storage extends BaseModel } catch (\Throwable $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' => $e->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' => $e->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 $e; diff --git a/app/Models/Team.php b/app/Models/Team.php index e32526169..92fed1128 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -59,7 +59,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 4561cddb2..68ecc6b31 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -316,6 +316,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..7a992f2fd 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,7 @@ 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; } /** @@ -109,11 +83,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 +93,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 +103,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 +113,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..f62ffdde2 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,9 @@ 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); } /** @@ -87,8 +92,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 +102,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..32436987c 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,7 @@ 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); } /** @@ -89,8 +84,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 +92,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..e5cbe91a0 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,14 @@ 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); } } diff --git a/app/Policies/ServicePolicy.php b/app/Policies/ServicePolicy.php index 7ab0fe7d0..d48728cdf 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,9 @@ 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); } /** @@ -111,18 +94,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/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/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 217c82929..cb9f3bcab 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -183,6 +183,11 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d $application = Application::find(data_get($application_deployment_queue, 'application_id')); $is_debug_enabled = data_get($application, 'settings.is_debug_enabled'); + // 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 3e993dbf3..d51a647aa 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -186,6 +186,14 @@ function refreshSession(?Team $team = null): void $team = User::find(Auth::id())->teams->first(); } } + + if (! $team) { + session()->forget('currentTeam'); + Cache::forget('team:'.Auth::id()); + + return; + } + // Clear old cache key format for backwards compatibility Cache::forget('team:'.Auth::id()); // Use new cache key format that includes team ID diff --git a/resources/css/utilities.css b/resources/css/utilities.css index 02be0c0c4..349a12ed0 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 { diff --git a/resources/views/components/applications/advanced.blade.php b/resources/views/components/applications/advanced.blade.php index e36583741..5964abb4e 100644 --- a/resources/views/components/applications/advanced.blade.php +++ b/resources/views/components/applications/advanced.blade.php @@ -3,7 +3,7 @@ Advanced @if ($application->status === 'running') - - - @if ($db_url_public) - + @else + + type="password" readonly wire:model="db_url" canGate="update" :canResource="$database" /> + @endif + @if ($isPasswordHiddenForMember) + + @else + @if ($db_url_public) + + @endif @endif diff --git a/resources/views/livewire/project/database/mysql/general.blade.php b/resources/views/livewire/project/database/mysql/general.blade.php index b1a75c455..d3169c9b6 100644 --- a/resources/views/livewire/project/database/mysql/general.blade.php +++ b/resources/views/livewire/project/database/mysql/general.blade.php @@ -17,12 +17,20 @@ @if ($database->started_at)
- + @if ($isPasswordHiddenForMember) + + @else + + @endif - + @if ($isPasswordHiddenForMember) + + @else + + @endif
@else
- + @if ($isPasswordHiddenForMember) + + @else + + @endif - + @if ($isPasswordHiddenForMember) + + @else + + @endif
- - @if ($db_url_public) - + @else + + type="password" readonly wire:model="db_url" /> + @endif + @if ($isPasswordHiddenForMember) + + @else + @if ($db_url_public) + + @endif @endif
diff --git a/resources/views/livewire/project/database/postgresql/general.blade.php b/resources/views/livewire/project/database/postgresql/general.blade.php index 74b1a03a8..2282d090f 100644 --- a/resources/views/livewire/project/database/postgresql/general.blade.php +++ b/resources/views/livewire/project/database/postgresql/general.blade.php @@ -34,9 +34,13 @@ - + @if ($isPasswordHiddenForMember) + + @else + + @endif @@ -45,8 +49,12 @@
- + @if ($isPasswordHiddenForMember) + + @else + + @endif
@@ -69,13 +77,21 @@ canGate="update" :canResource="$database" /> - - @if ($db_url_public) - + @else + + type="password" readonly wire:model="db_url" /> + @endif + @if ($isPasswordHiddenForMember) + + @else + @if ($db_url_public) + + @endif @endif
diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php index 11ffddd81..8c5456ad2 100644 --- a/resources/views/livewire/project/database/redis/general.blade.php +++ b/resources/views/livewire/project/database/redis/general.blade.php @@ -23,8 +23,12 @@ @endif - + @if ($isPasswordHiddenForMember) + + @else + + @endif
@else
You can only change the username and password in the database after @@ -39,13 +43,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 @@ -60,13 +68,21 @@ helper="A comma separated list of ports you would like to map to the host system.
Example3000:5432,3002:5433" canGate="update" :canResource="$database" /> - - @if ($dbUrlPublic) - + @else + + type="password" readonly wire:model="dbUrl" canGate="update" :canResource="$database" /> + @endif + @if ($isPasswordHiddenForMember) + + @else + @if ($dbUrlPublic) + + @endif @endif
diff --git a/resources/views/livewire/project/service/file-storage.blade.php b/resources/views/livewire/project/service/file-storage.blade.php index 1dd58fe17..472398db8 100644 --- a/resources/views/livewire/project/service/file-storage.blade.php +++ b/resources/views/livewire/project/service/file-storage.blade.php @@ -87,7 +87,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 c33ebc279..db21f3317 100644 --- a/resources/views/livewire/project/service/heading.blade.php +++ b/resources/views/livewire/project/service/heading.blade.php @@ -30,7 +30,7 @@
@if (str($service->status)->contains('running')) - + @@ -40,7 +40,7 @@ Restart - @@ -58,7 +58,7 @@ @elseif (str($service->status)->contains('degraded')) - + @@ -68,7 +68,7 @@ Restart - @@ -86,7 +86,7 @@ @elseif (str($service->status)->contains('exited')) - + @else - @@ -113,7 +113,7 @@ Stop - + @endif
@else @@ -149,11 +149,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 () => { @@ -166,12 +164,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('imagePulled', () => { diff --git a/resources/views/livewire/project/service/stack-form.blade.php b/resources/views/livewire/project/service/stack-form.blade.php index 8972345bc..2cdf57266 100644 --- a/resources/views/livewire/project/service/stack-form.blade.php +++ b/resources/views/livewire/project/service/stack-form.blade.php @@ -39,10 +39,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 86faeeeb4..9ca92602c 100644 --- a/resources/views/livewire/project/shared/environment-variable/show.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php @@ -158,24 +158,45 @@ @else - + @endif @if ($is_shared) @endif - + @if (!$isMagicVariable) + + @endif @endif @else
- - @if ($is_shared) - + @if ($isValueHidden) +
+ +
+ @else + + @if ($is_shared) + + @endif @endif
@if (!$isMagicVariable) diff --git a/resources/views/livewire/project/shared/health-checks.blade.php b/resources/views/livewire/project/shared/health-checks.blade.php index 8662b0b50..01f42dace 100644 --- a/resources/views/livewire/project/shared/health-checks.blade.php +++ b/resources/views/livewire/project/shared/health-checks.blade.php @@ -3,12 +3,12 @@

Healthchecks

Save @if (!$healthCheckEnabled) - - + + @else Disable Healthcheck @endif 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 f312c0bf3..6777e96a0 100644 --- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php @@ -13,36 +13,43 @@

Scheduled Task

- + Save @if ($resource->isRunning()) - - Execute Now - + @can('update', $resource) + + Execute Now + + @endcan @endif - - + @can('update', $resource) + + @endcan
- + @can('update', $resource) + + @else + + @endcan
- - - - + + + @if ($type === 'application') - @elseif ($type === 'service') - @endif diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php index 23f0e263e..bf04aad2a 100644 --- a/resources/views/livewire/security/api-tokens.blade.php +++ b/resources/views/livewire/security/api-tokens.blade.php @@ -18,21 +18,23 @@ Create
-
- Permissions - : -
- @if ($permissions) +
+ Permissions + + @if ($permissions) +
@foreach ($permissions as $permission) -
{{ $permission }}
+ + {{ $permission }} + @endforeach - @endif -
+
+ @endif

Token Permissions

-
+
@if ($canUseRootPermissions) @@ -50,13 +52,23 @@ helper="Write access requires admin or owner role" :checked="false"> @endif - + @if ($canUseDeployPermissions) + + @else + + @endif - + @if ($canUseSensitivePermissions) + + @else + + @endif @endif
@if (in_array('root', $permissions)) @@ -65,44 +77,74 @@ @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

-
- @forelse ($tokens as $token) -
-
Description: {{ $token->name }}
-
Last used: {{ $token->last_used_at ? $token->last_used_at->diffForHumans() : 'Never' }}
-
- @if ($token->abilities) - Permissions: - @foreach ($token->abilities as $ability) -
{{ $ability }}
- @endforeach +
+
+

Issued Tokens

+ @if ($tokens->count() > 1) + + @endif +
+
+ @forelse ($tokens as $token) +
+
+
+ {{ $token->name }} + @if ($token->abilities) + @foreach ($token->abilities as $ability) + + {{ $ability }} + + @endforeach + @endif +
+
+ Last used: {{ $token->last_used_at ? $token->last_used_at->diffForHumans() : 'Never' }} +
+
+ @if (auth()->id() === $token->tokenable_id) + @endif
- - @if (auth()->id() === $token->tokenable_id) - - @endif -
- @empty -
-
No API tokens found.
-
- @endforelse + @empty +
No API tokens found.
+ @endforelse +
@endif
diff --git a/resources/views/livewire/server/navbar.blade.php b/resources/views/livewire/server/navbar.blade.php index 4be11f51a..413f0ee57 100644 --- a/resources/views/livewire/server/navbar.blade.php +++ b/resources/views/livewire/server/navbar.blade.php @@ -62,7 +62,7 @@
{{ data_get($server, 'name') }}