This commit is contained in:
Andras Bacsai 2026-03-10 23:01:51 +01:00 committed by GitHub
commit 8181553449
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
239 changed files with 9323 additions and 2239 deletions

20
.ai/lessons.md Normal file
View file

@ -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

View file

@ -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/)

View file

@ -54,6 +54,10 @@ class ApplicationsController extends Controller
]);
}
if ($application->is_shown_once ?? false) {
$application->makeHidden(['value', 'real_value']);
}
return serializeApiResponse($application);
}

View file

@ -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);

View file

@ -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()) {

View file

@ -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) {

View file

@ -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);

View file

@ -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([

View file

@ -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);

View file

@ -35,6 +35,10 @@ class ServicesController extends Controller
]);
}
if ($service->is_shown_once ?? false) {
$service->makeHidden(['value', 'real_value']);
}
return serializeApiResponse($service);
}

View file

@ -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',

View file

@ -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);
}

View file

@ -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);

View file

@ -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();
}

View file

@ -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) {

View file

@ -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);
}

View file

@ -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()

View file

@ -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,

View file

@ -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 = '';

View file

@ -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;

View file

@ -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.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/build-server">documentation</a>');
return;
}
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
return;
}
$this->setDeploymentUuid();
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild,
);
if ($result['status'] === 'queue_full') {
$this->dispatch('error', 'Deployment queue full', $result['message']);
return;
}
if ($result['status'] === 'skipped') {
$this->dispatch('error', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: false);
} catch (\Throwable $e) {
return handleError($e, $this);
}
if ($this->application->destination->server->isSwarm() && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'To deploy to a Swarm cluster you must set a Docker image name first.');
return;
}
if (data_get($this->application, 'settings.is_build_server_enabled') && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'To use a build server, you must first set a Docker image.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/build-server">documentation</a>');
return;
}
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy.', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
return;
}
$this->setDeploymentUuid();
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild,
);
if ($result['status'] === 'queue_full') {
$this->dispatch('error', 'Deployment queue full', $result['message']);
return;
}
if ($result['status'] === 'skipped') {
$this->dispatch('error', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: false);
}
protected function setDeploymentUuid()
@ -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.<br/>It could take a while depending on the application.');
StopApplication::dispatch($this->application, false, $this->docker_cleanup);
$this->dispatch('info', 'Gracefully stopping application.<br/>It could take a while depending on the application.');
StopApplication::dispatch($this->application, false, $this->docker_cleanup);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function restart()
{
$this->authorize('deploy', $this->application);
try {
$this->authorize('deploy', $this->application);
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
if ($this->application->additional_servers->count() > 0 && str($this->application->docker_registry_image_name)->isEmpty()) {
$this->dispatch('error', 'Failed to deploy', 'Before deploying to multiple servers, you must first set a Docker image in the General tab.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
return;
return;
}
$this->setDeploymentUuid();
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);
if ($result['status'] === 'queue_full') {
$this->dispatch('error', 'Deployment queue full', $result['message']);
return;
}
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: false);
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->setDeploymentUuid();
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);
if ($result['status'] === 'queue_full') {
$this->dispatch('error', 'Deployment queue full', $result['message']);
return;
}
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: false);
}
public function render()

View file

@ -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()) {

View file

@ -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) {

View file

@ -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(),

View file

@ -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 {

View file

@ -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;
}

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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', "<strong>Environment {$environment->name}</strong> has defined resources, please delete them first.");
} catch (\Throwable $e) {
return handleError($e, $this);
}
return $this->dispatch('error', "<strong>Environment {$environment->name}</strong> has defined resources, please delete them first.");
}
}

View file

@ -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', "<strong>Project {$project->name}</strong> has resources defined, please delete them first.");
} catch (\Throwable $e) {
return handleError($e, $this);
}
return $this->dispatch('error', "<strong>Project {$project->name}</strong> has resources defined, please delete them first.");
}
}

View file

@ -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) {

View file

@ -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,

View file

@ -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()

View file

@ -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();

View file

@ -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()

View file

@ -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);

View file

@ -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.<br>More information here: <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/multiple-servers">documentation</a>');
@ -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;
}

View file

@ -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)";
}

View file

@ -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;
}
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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

View file

@ -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()

View file

@ -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);
}
}
}

View file

@ -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()

View file

@ -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 {

View file

@ -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);
}
}

View file

@ -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)

View file

@ -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()

View file

@ -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));

View file

@ -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()

View file

@ -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);
}
}

View file

@ -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.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);
try {
$this->syncData(true);
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
if ($uptime) {
$this->dispatch('success', 'Server is reachable.');
$this->server->settings->is_reachable = $this->isReachable = true;
$this->server->settings->is_usable = $this->isUsable = true;
$this->server->settings->save();
ServerReachabilityChanged::dispatch($this->server);
} else {
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error);
return;
return;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
@ -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.');

View file

@ -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.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$error.'</div>';
$this->server->update([
'validation_logs' => $this->error,
]);
try {
$this->authorize('update', $this->server);
['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection();
if (! $this->uptime) {
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$error.'</div>';
$this->server->update([
'validation_logs' => $this->error,
]);
return;
return;
}
$this->dispatch('validateOS');
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->dispatch('validateOS');
}
public function validateOS()

View file

@ -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;
}

View file

@ -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.');

View file

@ -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();

View file

@ -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);
}
}
}

View file

@ -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',
], [

View file

@ -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);
}
}
}

View file

@ -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)";
}

View file

@ -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)";
}

View file

@ -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)";
}

View file

@ -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()

View file

@ -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()

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -44,6 +44,9 @@ class Upgrade extends Component
public function upgrade()
{
try {
if (! isInstanceAdmin()) {
abort(403);
}
if ($this->updateInProgress) {
return;
}

View file

@ -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;

View file

@ -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.');
}

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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.<br/><br/>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;
}
}

View file

@ -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.<br/><br/>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;
}
}

View file

@ -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;
}
}

View file

@ -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.<br/><br/>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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
/**

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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();
}
/**

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

Some files were not shown because too many files have changed in this diff Show more