diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 35ff2632d..f02c4255d 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -25,26 +25,24 @@ class ApplicationsController extends Controller { private function removeSensitiveData($application) { - $token = auth()->user()->currentAccessToken(); $application->makeHidden([ 'id', ]); - if ($token->can('view:sensitive')) { - return serializeApiResponse($application); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $application->makeHidden([ + 'custom_labels', + 'dockerfile', + 'docker_compose', + 'docker_compose_raw', + 'manual_webhook_secret_bitbucket', + 'manual_webhook_secret_gitea', + 'manual_webhook_secret_github', + 'manual_webhook_secret_gitlab', + 'private_key_id', + 'value', + 'real_value', + ]); } - $application->makeHidden([ - 'custom_labels', - 'dockerfile', - 'docker_compose', - 'docker_compose_raw', - 'manual_webhook_secret_bitbucket', - 'manual_webhook_secret_gitea', - 'manual_webhook_secret_github', - 'manual_webhook_secret_gitlab', - 'private_key_id', - 'value', - 'real_value', - ]); return serializeApiResponse($application); } diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 98a076c49..917171e5c 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -19,26 +19,23 @@ class DatabasesController extends Controller { private function removeSensitiveData($database) { - $token = auth()->user()->currentAccessToken(); $database->makeHidden([ 'id', 'laravel_through_key', ]); - if ($token->can('view:sensitive')) { - return serializeApiResponse($database); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $database->makeHidden([ + 'internal_db_url', + 'external_db_url', + 'postgres_password', + 'dragonfly_password', + 'redis_password', + 'mongo_initdb_root_password', + 'keydb_password', + 'clickhouse_admin_password', + ]); } - $database->makeHidden([ - 'internal_db_url', - 'external_db_url', - 'postgres_password', - 'dragonfly_password', - 'redis_password', - 'mongo_initdb_root_password', - 'keydb_password', - 'clickhouse_admin_password', - ]); - return serializeApiResponse($database); } diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 666dc55a5..73b452f86 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -16,15 +16,12 @@ class DeployController extends Controller { private function removeSensitiveData($deployment) { - $token = auth()->user()->currentAccessToken(); - if ($token->can('view:sensitive')) { - return serializeApiResponse($deployment); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $deployment->makeHidden([ + 'logs', + ]); } - $deployment->makeHidden([ - 'logs', - ]); - return serializeApiResponse($deployment); } diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php index b7190ab1e..a14b0da20 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -11,13 +11,11 @@ class SecurityController extends Controller { private function removeSensitiveData($team) { - $token = auth()->user()->currentAccessToken(); - if ($token->can('view:sensitive')) { - return serializeApiResponse($team); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $team->makeHidden([ + 'private_key', + ]); } - $team->makeHidden([ - 'private_key', - ]); return serializeApiResponse($team); } diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index 8c13b1a01..f37040bdd 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -19,25 +19,22 @@ class ServersController extends Controller { private function removeSensitiveDataFromSettings($settings) { - $token = auth()->user()->currentAccessToken(); - if ($token->can('view:sensitive')) { - return serializeApiResponse($settings); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $settings = $settings->makeHidden([ + 'sentinel_token', + ]); } - $settings = $settings->makeHidden([ - 'sentinel_token', - ]); return serializeApiResponse($settings); } private function removeSensitiveData($server) { - $token = auth()->user()->currentAccessToken(); $server->makeHidden([ 'id', ]); - if ($token->can('view:sensitive')) { - return serializeApiResponse($server); + if (request()->attributes->get('can_read_sensitive', false) === false) { + // Do nothing } return serializeApiResponse($server); diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index bf90322e2..e6b7e9854 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -18,19 +18,16 @@ class ServicesController extends Controller { private function removeSensitiveData($service) { - $token = auth()->user()->currentAccessToken(); $service->makeHidden([ 'id', ]); - if ($token->can('view:sensitive')) { - return serializeApiResponse($service); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $service->makeHidden([ + 'docker_compose_raw', + 'docker_compose', + ]); } - $service->makeHidden([ - 'docker_compose_raw', - 'docker_compose', - ]); - return serializeApiResponse($service); } diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php index 3f951c6f7..d4b24d8ab 100644 --- a/app/Http/Controllers/Api/TeamController.php +++ b/app/Http/Controllers/Api/TeamController.php @@ -10,20 +10,18 @@ class TeamController extends Controller { private function removeSensitiveData($team) { - $token = auth()->user()->currentAccessToken(); $team->makeHidden([ 'custom_server_limit', 'pivot', ]); - if ($token->can('view:sensitive')) { - return serializeApiResponse($team); + if (request()->attributes->get('can_read_sensitive', false) === false) { + $team->makeHidden([ + 'smtp_username', + 'smtp_password', + 'resend_api_key', + 'telegram_token', + ]); } - $team->makeHidden([ - 'smtp_username', - 'smtp_password', - 'resend_api_key', - 'telegram_token', - ]); return serializeApiResponse($team); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 5f1731071..a1ce20295 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -69,5 +69,7 @@ class Kernel extends HttpKernel 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class, 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class, + 'api.ability' => \App\Http\Middleware\ApiAbility::class, + 'api.sensitive' => \App\Http\Middleware\ApiSensitiveData::class, ]; } diff --git a/app/Http/Middleware/ApiAbility.php b/app/Http/Middleware/ApiAbility.php new file mode 100644 index 000000000..324eeebaa --- /dev/null +++ b/app/Http/Middleware/ApiAbility.php @@ -0,0 +1,27 @@ +user()->tokenCan('root')) { + return $next($request); + } + + return parent::handle($request, $next, ...$abilities); + } catch (\Illuminate\Auth\AuthenticationException $e) { + return response()->json([ + 'message' => 'Unauthenticated.', + ], 401); + } catch (\Exception $e) { + return response()->json([ + 'message' => 'Missing required permissions: '.implode(', ', $abilities), + ], 403); + } + } +} diff --git a/app/Http/Middleware/ApiSensitiveData.php b/app/Http/Middleware/ApiSensitiveData.php new file mode 100644 index 000000000..49584ddb3 --- /dev/null +++ b/app/Http/Middleware/ApiSensitiveData.php @@ -0,0 +1,21 @@ +user()->currentAccessToken(); + + // Allow access to sensitive data if token has root or read:sensitive permission + $request->attributes->add([ + 'can_read_sensitive' => $token->can('root') || $token->can('read:sensitive'), + ]); + + return $next($request); + } +} diff --git a/app/Http/Middleware/IgnoreReadOnlyApiToken.php b/app/Http/Middleware/IgnoreReadOnlyApiToken.php deleted file mode 100644 index bd6cd1f8a..000000000 --- a/app/Http/Middleware/IgnoreReadOnlyApiToken.php +++ /dev/null @@ -1,28 +0,0 @@ -user()->currentAccessToken(); - if ($token->can('*')) { - return $next($request); - } - if ($token->can('read-only')) { - return response()->json(['message' => 'You are not allowed to perform this action.'], 403); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/OnlyRootApiToken.php b/app/Http/Middleware/OnlyRootApiToken.php deleted file mode 100644 index 8ff1fa0e5..000000000 --- a/app/Http/Middleware/OnlyRootApiToken.php +++ /dev/null @@ -1,25 +0,0 @@ -user()->currentAccessToken(); - if ($token->can('*')) { - return $next($request); - } - - return response()->json(['message' => 'You are not allowed to perform this action.'], 403); - } -} diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php index 0710e37ff..74eac7132 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php @@ -24,6 +24,14 @@ class Executions extends Component #[Locked] public ?string $serverTimezone = null; + public $currentPage = 1; + + public $logsPerPage = 100; + + public $selectedExecution = null; + + public $isPollingActive = false; + public function getListeners() { $teamId = Auth::user()->currentTeam()->id; @@ -54,16 +62,84 @@ class Executions extends Component public function refreshExecutions(): void { $this->executions = $this->task->executions()->take(20)->get(); + if ($this->selectedKey) { + $this->selectedExecution = $this->task->executions()->find($this->selectedKey); + if ($this->selectedExecution && $this->selectedExecution->status !== 'running') { + $this->isPollingActive = false; + } + } } public function selectTask($key): void { if ($key == $this->selectedKey) { $this->selectedKey = null; + $this->selectedExecution = null; + $this->currentPage = 1; + $this->isPollingActive = false; return; } $this->selectedKey = $key; + $this->selectedExecution = $this->task->executions()->find($key); + $this->currentPage = 1; + + // Start polling if task is running + if ($this->selectedExecution && $this->selectedExecution->status === 'running') { + $this->isPollingActive = true; + } + } + + public function polling() + { + if ($this->selectedExecution && $this->isPollingActive) { + $this->selectedExecution->refresh(); + if ($this->selectedExecution->status !== 'running') { + $this->isPollingActive = false; + } + } + } + + public function loadMoreLogs() + { + $this->currentPage++; + } + + public function getLogLinesProperty() + { + if (! $this->selectedExecution) { + return collect(); + } + + if (! $this->selectedExecution->message) { + return collect(['Waiting for task output...']); + } + + $lines = collect(explode("\n", $this->selectedExecution->message)); + + return $lines->take($this->currentPage * $this->logsPerPage); + } + + public function downloadLogs(int $executionId) + { + $execution = $this->executions->firstWhere('id', $executionId); + if (! $execution) { + return; + } + + return response()->streamDownload(function () use ($execution) { + echo $execution->message; + }, 'task-execution-'.$execution->id.'.log'); + } + + public function hasMoreLogs() + { + if (! $this->selectedExecution || ! $this->selectedExecution->message) { + return false; + } + $lines = collect(explode("\n", $this->selectedExecution->message)); + + return $lines->count() > ($this->currentPage * $this->logsPerPage); } public function formatDateInServerTimezone($date) diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index fe68a8ba5..72684bdc6 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -11,13 +11,7 @@ class ApiTokens extends Component public $tokens = []; - public bool $viewSensitiveData = false; - - public bool $readOnly = true; - - public bool $rootAccess = false; - - public array $permissions = ['read-only']; + public array $permissions = ['read']; public $isApiEnabled; @@ -29,51 +23,28 @@ class ApiTokens extends Component public function mount() { $this->isApiEnabled = InstanceSettings::get()->is_api_enabled; + $this->getTokens(); + } + + private function getTokens() + { $this->tokens = auth()->user()->tokens->sortByDesc('created_at'); } - public function updatedViewSensitiveData() + public function updatedPermissions($permissionToUpdate) { - if ($this->viewSensitiveData) { - $this->permissions[] = 'view:sensitive'; - $this->permissions = array_diff($this->permissions, ['*']); - $this->rootAccess = false; + if ($permissionToUpdate == 'root') { + $this->permissions = ['root']; + } elseif ($permissionToUpdate == 'read:sensitive' && ! in_array('read', $this->permissions)) { + $this->permissions[] = 'read'; + } elseif ($permissionToUpdate == 'deploy') { + $this->permissions = ['deploy']; } else { - $this->permissions = array_diff($this->permissions, ['view:sensitive']); - } - $this->makeSureOneIsSelected(); - } - - public function updatedReadOnly() - { - if ($this->readOnly) { - $this->permissions[] = 'read-only'; - $this->permissions = array_diff($this->permissions, ['*']); - $this->rootAccess = false; - } else { - $this->permissions = array_diff($this->permissions, ['read-only']); - } - $this->makeSureOneIsSelected(); - } - - public function updatedRootAccess() - { - if ($this->rootAccess) { - $this->permissions = ['*']; - $this->readOnly = false; - $this->viewSensitiveData = false; - } else { - $this->readOnly = true; - $this->permissions = ['read-only']; - } - } - - public function makeSureOneIsSelected() - { - if (count($this->permissions) == 0) { - $this->permissions = ['read-only']; - $this->readOnly = true; + if (count($this->permissions) == 0) { + $this->permissions = ['read']; + } } + sort($this->permissions); } public function addNewToken() @@ -82,8 +53,8 @@ class ApiTokens extends Component $this->validate([ 'description' => 'required|min:3|max:255', ]); - $token = auth()->user()->createToken($this->description, $this->permissions); - $this->tokens = auth()->user()->tokens; + $token = auth()->user()->createToken($this->description, array_values($this->permissions)); + $this->getTokens(); session()->flash('token', $token->plainTextToken); } catch (\Exception $e) { return handleError($e, $this); @@ -92,8 +63,12 @@ class ApiTokens extends Component public function revoke(int $id) { - $token = auth()->user()->tokens()->where('id', $id)->first(); - $token->delete(); - $this->tokens = auth()->user()->tokens; + try { + $token = auth()->user()->tokens()->where('id', $id)->firstOrFail(); + $token->delete(); + $this->getTokens(); + } catch (\Exception $e) { + return handleError($e, $this); + } } } diff --git a/app/Notifications/Channels/SendsSlack.php b/app/Notifications/Channels/SendsSlack.php index 417d4adda..ab2dd6f11 100644 --- a/app/Notifications/Channels/SendsSlack.php +++ b/app/Notifications/Channels/SendsSlack.php @@ -5,4 +5,4 @@ namespace App\Notifications\Channels; interface SendsSlack { public function routeNotificationForSlack(); -} \ No newline at end of file +} diff --git a/app/Notifications/Dto/SlackMessage.php b/app/Notifications/Dto/SlackMessage.php index 86532c65b..879bf6547 100644 --- a/app/Notifications/Dto/SlackMessage.php +++ b/app/Notifications/Dto/SlackMessage.php @@ -8,8 +8,7 @@ class SlackMessage public string $title, public string $description, public string $color = '#0099ff' - ) { - } + ) {} public static function infoColor(): string { @@ -30,4 +29,4 @@ class SlackMessage { return '#ffa500'; } -} \ No newline at end of file +} diff --git a/app/View/Components/Forms/Checkbox.php b/app/View/Components/Forms/Checkbox.php index 0bdebe7e4..e46598e8e 100644 --- a/app/View/Components/Forms/Checkbox.php +++ b/app/View/Components/Forms/Checkbox.php @@ -15,6 +15,7 @@ class Checkbox extends Component public ?string $id = null, public ?string $name = null, public ?string $value = null, + public ?string $domValue = null, public ?string $label = null, public ?string $helper = null, public string|bool|null $checked = false, diff --git a/config/constants.php b/config/constants.php index 648af735a..4ea4411b6 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.376', + 'version' => '4.0.0-beta.377', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), diff --git a/database/migrations/2024_10_30_074601_rename_token_permissions.php b/database/migrations/2024_10_30_074601_rename_token_permissions.php new file mode 100644 index 000000000..2ca98d090 --- /dev/null +++ b/database/migrations/2024_10_30_074601_rename_token_permissions.php @@ -0,0 +1,60 @@ +abilities)) { + $abilities->push('root'); + } + if (in_array('read-only', $token->abilities)) { + $abilities->push('read'); + } + if (in_array('view:sensitive', $token->abilities)) { + $abilities->push('read', 'read:sensitive'); + } + $token->abilities = $abilities->unique()->values()->all(); + $token->save(); + } + } catch (\Exception $e) { + \Log::error('Error renaming token permissions: '.$e->getMessage()); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + try { + $tokens = PersonalAccessToken::all(); + foreach ($tokens as $token) { + $abilities = collect(); + if (in_array('write', $token->abilities)) { + $abilities->push('*'); + } else { + if (in_array('read', $token->abilities)) { + $abilities->push('read-only'); + } + if (in_array('read:sensitive', $token->abilities)) { + $abilities->push('view:sensitive'); + } + } + $token->abilities = $abilities->unique()->values()->all(); + $token->save(); + } + } catch (\Exception $e) { + \Log::error('Error renaming token permissions: '.$e->getMessage()); + } + } +}; diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php index fb244962d..39704a122 100644 --- a/resources/views/components/forms/checkbox.blade.php +++ b/resources/views/components/forms/checkbox.blade.php @@ -5,8 +5,8 @@ 'disabled' => false, 'instantSave' => false, 'value' => null, + 'domValue' => null, 'checked' => false, - 'hideLabel' => false, 'fullWidth' => false, ]) @@ -14,26 +14,32 @@ 'flex flex-row items-center gap-4 pr-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100', 'w-full' => $fullWidth, ])> - @if (!$hideLabel) - - @endif + diff --git a/resources/views/livewire/project/new/select.blade.php b/resources/views/livewire/project/new/select.blade.php index 323191bfd..54a2e953e 100644 --- a/resources/views/livewire/project/new/select.blade.php +++ b/resources/views/livewire/project/new/select.blade.php @@ -12,7 +12,7 @@
Deploy resources, like Applications, Databases, Services...
@if ($current_step === 'type') -
+
@@ -137,6 +137,7 @@ return { search: '', loading: false, + isSticky: false, services: [], gitBasedApplications: [], dockerBasedApplications: [], diff --git a/resources/views/livewire/project/shared/scheduled-task/executions.blade.php b/resources/views/livewire/project/shared/scheduled-task/executions.blade.php index fcb4215de..c1400f5ec 100644 --- a/resources/views/livewire/project/shared/scheduled-task/executions.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/executions.blade.php @@ -1,4 +1,17 @@ -
+
@forelse($executions as $execution) data_get($execution, 'status') === 'failed', 'border-yellow-500' => data_get($execution, 'status') === 'running', ])> + @if (data_get($execution, 'status') === 'running')
@@ -21,11 +35,34 @@ Started At: {{ $this->formatDateInServerTimezone(data_get($execution, 'created_at', now())) }}
+ @if (strlen($execution->message) > 0) + + Download Logs + + @endif @if (data_get($execution, 'id') == $selectedKey)
- @if (data_get($execution, 'message')) + @if (data_get($execution, 'status') === 'running') +
+ Task is running... + +
+ @endif + @if ($this->logLines->isNotEmpty())
-
{{ data_get($execution, 'message') }}
+
+@foreach ($this->logLines as $line)
+{{ $line }}
+@endforeach
+
+
+ @if ($this->hasMoreLogs()) + + Load More + + @endif + +
@else
No output was recorded for this execution.
diff --git a/resources/views/livewire/security/api-tokens.blade.php b/resources/views/livewire/security/api-tokens.blade.php index 1bcd64710..5c3c4c81c 100644 --- a/resources/views/livewire/security/api-tokens.blade.php +++ b/resources/views/livewire/security/api-tokens.blade.php @@ -25,21 +25,31 @@
@if ($permissions) @foreach ($permissions as $permission) - @if ($permission === '*') -
Root access, be careful!
- @else -
{{ $permission }}
- @endif +
{{ $permission }}
@endforeach @endif
+

Token Permissions

- - - + + @if (!in_array('root', $permissions)) + + + + + @endif
+ @if (in_array('root', $permissions)) +
Root access, be careful!
+ @endif @if (session()->has('token'))
Please copy this token now. For your security, it won't be shown @@ -50,12 +60,13 @@

Issued Tokens

@forelse ($tokens as $token) -
+
Description: {{ $token->name }}
Last used: {{ $token->last_used_at ? $token->last_used_at->diffForHumans() : 'Never' }}
@if ($token->abilities) - Abilities: + Permissions: @foreach ($token->abilities as $ability)
{{ $ability }}
@endforeach diff --git a/resources/views/livewire/team/admin-view.blade.php b/resources/views/livewire/team/admin-view.blade.php index 26da967ab..7aa623122 100644 --- a/resources/views/livewire/team/admin-view.blade.php +++ b/resources/views/livewire/team/admin-view.blade.php @@ -10,7 +10,8 @@

Users

@forelse ($users as $user) -
+
{{ $user->name }}
{{ $user->email }}
diff --git a/routes/api.php b/routes/api.php index e8425aeb1..9ad64c40c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -11,8 +11,6 @@ use App\Http\Controllers\Api\ServersController; use App\Http\Controllers\Api\ServicesController; use App\Http\Controllers\Api\TeamController; use App\Http\Middleware\ApiAllowed; -use App\Http\Middleware\IgnoreReadOnlyApiToken; -use App\Http\Middleware\OnlyRootApiToken; use App\Jobs\PushServerUpdateJob; use App\Models\Server; use Illuminate\Support\Facades\Route; @@ -21,113 +19,113 @@ Route::get('/health', [OtherController::class, 'healthcheck']); Route::post('/feedback', [OtherController::class, 'feedback']); Route::group([ - 'middleware' => ['auth:sanctum', OnlyRootApiToken::class], + 'middleware' => ['auth:sanctum', 'api.ability:write'], 'prefix' => 'v1', ], function () { Route::get('/enable', [OtherController::class, 'enable_api']); Route::get('/disable', [OtherController::class, 'disable_api']); }); Route::group([ - 'middleware' => ['auth:sanctum', ApiAllowed::class], + 'middleware' => ['auth:sanctum', ApiAllowed::class, 'api.sensitive'], 'prefix' => 'v1', ], function () { - Route::get('/version', [OtherController::class, 'version']); + Route::get('/version', [OtherController::class, 'version'])->middleware(['api.ability:read']); - Route::get('/teams', [TeamController::class, 'teams']); - Route::get('/teams/current', [TeamController::class, 'current_team']); - Route::get('/teams/current/members', [TeamController::class, 'current_team_members']); - Route::get('/teams/{id}', [TeamController::class, 'team_by_id']); - Route::get('/teams/{id}/members', [TeamController::class, 'members_by_id']); + Route::get('/teams', [TeamController::class, 'teams'])->middleware(['api.ability:read']); + Route::get('/teams/current', [TeamController::class, 'current_team'])->middleware(['api.ability:read']); + Route::get('/teams/current/members', [TeamController::class, 'current_team_members'])->middleware(['api.ability:read']); + Route::get('/teams/{id}', [TeamController::class, 'team_by_id'])->middleware(['api.ability:read']); + Route::get('/teams/{id}/members', [TeamController::class, 'members_by_id'])->middleware(['api.ability:read']); - Route::get('/projects', [ProjectController::class, 'projects']); - Route::get('/projects/{uuid}', [ProjectController::class, 'project_by_uuid']); - Route::get('/projects/{uuid}/{environment_name}', [ProjectController::class, 'environment_details']); + Route::get('/projects', [ProjectController::class, 'projects'])->middleware(['api.ability:read']); + Route::get('/projects/{uuid}', [ProjectController::class, 'project_by_uuid'])->middleware(['api.ability:read']); + Route::get('/projects/{uuid}/{environment_name}', [ProjectController::class, 'environment_details'])->middleware(['api.ability:read']); - Route::post('/projects', [ProjectController::class, 'create_project']); - Route::patch('/projects/{uuid}', [ProjectController::class, 'update_project']); - Route::delete('/projects/{uuid}', [ProjectController::class, 'delete_project']); + Route::post('/projects', [ProjectController::class, 'create_project'])->middleware(['api.ability:read']); + Route::patch('/projects/{uuid}', [ProjectController::class, 'update_project'])->middleware(['api.ability:write']); + Route::delete('/projects/{uuid}', [ProjectController::class, 'delete_project'])->middleware(['api.ability:write']); - Route::get('/security/keys', [SecurityController::class, 'keys']); - Route::post('/security/keys', [SecurityController::class, 'create_key'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/security/keys', [SecurityController::class, 'keys'])->middleware(['api.ability:read']); + Route::post('/security/keys', [SecurityController::class, 'create_key'])->middleware(['api.ability:write']); - Route::get('/security/keys/{uuid}', [SecurityController::class, 'key_by_uuid']); - Route::patch('/security/keys/{uuid}', [SecurityController::class, 'update_key'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/security/keys/{uuid}', [SecurityController::class, 'delete_key'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/security/keys/{uuid}', [SecurityController::class, 'key_by_uuid'])->middleware(['api.ability:read']); + Route::patch('/security/keys/{uuid}', [SecurityController::class, 'update_key'])->middleware(['api.ability:write']); + Route::delete('/security/keys/{uuid}', [SecurityController::class, 'delete_key'])->middleware(['api.ability:write']); - Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::get('/deployments', [DeployController::class, 'deployments']); - Route::get('/deployments/{uuid}', [DeployController::class, 'deployment_by_uuid']); + Route::match(['get', 'post'], '/deploy', [DeployController::class, 'deploy'])->middleware(['api.ability:write,deploy']); + Route::get('/deployments', [DeployController::class, 'deployments'])->middleware(['api.ability:read']); + Route::get('/deployments/{uuid}', [DeployController::class, 'deployment_by_uuid'])->middleware(['api.ability:read']); - Route::get('/servers', [ServersController::class, 'servers']); - Route::get('/servers/{uuid}', [ServersController::class, 'server_by_uuid']); - Route::get('/servers/{uuid}/domains', [ServersController::class, 'domains_by_server']); - Route::get('/servers/{uuid}/resources', [ServersController::class, 'resources_by_server']); + Route::get('/servers', [ServersController::class, 'servers'])->middleware(['api.ability:read']); + Route::get('/servers/{uuid}', [ServersController::class, 'server_by_uuid'])->middleware(['api.ability:read']); + Route::get('/servers/{uuid}/domains', [ServersController::class, 'domains_by_server'])->middleware(['api.ability:read']); + Route::get('/servers/{uuid}/resources', [ServersController::class, 'resources_by_server'])->middleware(['api.ability:read']); - Route::get('/servers/{uuid}/validate', [ServersController::class, 'validate_server']); + Route::get('/servers/{uuid}/validate', [ServersController::class, 'validate_server'])->middleware(['api.ability:read']); - Route::post('/servers', [ServersController::class, 'create_server']); - Route::patch('/servers/{uuid}', [ServersController::class, 'update_server']); - Route::delete('/servers/{uuid}', [ServersController::class, 'delete_server']); + Route::post('/servers', [ServersController::class, 'create_server'])->middleware(['api.ability:read']); + Route::patch('/servers/{uuid}', [ServersController::class, 'update_server'])->middleware(['api.ability:write']); + Route::delete('/servers/{uuid}', [ServersController::class, 'delete_server'])->middleware(['api.ability:write']); - Route::get('/resources', [ResourcesController::class, 'resources']); + Route::get('/resources', [ResourcesController::class, 'resources'])->middleware(['api.ability:read']); - Route::get('/applications', [ApplicationsController::class, 'applications']); - Route::post('/applications/public', [ApplicationsController::class, 'create_public_application'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/applications/private-github-app', [ApplicationsController::class, 'create_private_gh_app_application'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/applications/private-deploy-key', [ApplicationsController::class, 'create_private_deploy_key_application'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/applications/dockerfile', [ApplicationsController::class, 'create_dockerfile_application'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/applications/dockerimage', [ApplicationsController::class, 'create_dockerimage_application'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/applications/dockercompose', [ApplicationsController::class, 'create_dockercompose_application'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/applications', [ApplicationsController::class, 'applications'])->middleware(['api.ability:read']); + Route::post('/applications/public', [ApplicationsController::class, 'create_public_application'])->middleware(['api.ability:write']); + Route::post('/applications/private-github-app', [ApplicationsController::class, 'create_private_gh_app_application'])->middleware(['api.ability:write']); + Route::post('/applications/private-deploy-key', [ApplicationsController::class, 'create_private_deploy_key_application'])->middleware(['api.ability:write']); + Route::post('/applications/dockerfile', [ApplicationsController::class, 'create_dockerfile_application'])->middleware(['api.ability:write']); + Route::post('/applications/dockerimage', [ApplicationsController::class, 'create_dockerimage_application'])->middleware(['api.ability:write']); + Route::post('/applications/dockercompose', [ApplicationsController::class, 'create_dockercompose_application'])->middleware(['api.ability:write']); - Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid']); - Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid'])->middleware(['api.ability:read']); + Route::patch('/applications/{uuid}', [ApplicationsController::class, 'update_by_uuid'])->middleware(['api.ability:write']); + Route::delete('/applications/{uuid}', [ApplicationsController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); - Route::get('/applications/{uuid}/envs', [ApplicationsController::class, 'envs']); - Route::post('/applications/{uuid}/envs', [ApplicationsController::class, 'create_env'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::patch('/applications/{uuid}/envs/bulk', [ApplicationsController::class, 'create_bulk_envs'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::patch('/applications/{uuid}/envs', [ApplicationsController::class, 'update_env_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - // Route::post('/applications/{uuid}/execute', [ApplicationsController::class, 'execute_command_by_uuid'])->middleware([OnlyRootApiToken::class]); + Route::get('/applications/{uuid}/envs', [ApplicationsController::class, 'envs'])->middleware(['api.ability:read']); + Route::post('/applications/{uuid}/envs', [ApplicationsController::class, 'create_env'])->middleware(['api.ability:write']); + Route::patch('/applications/{uuid}/envs/bulk', [ApplicationsController::class, 'create_bulk_envs'])->middleware(['api.ability:write']); + Route::patch('/applications/{uuid}/envs', [ApplicationsController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']); + Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']); + // Route::post('/applications/{uuid}/execute', [ApplicationsController::class, 'execute_command_by_uuid'])->middleware(['ability:write']); - Route::match(['get', 'post'], '/applications/{uuid}/start', [ApplicationsController::class, 'action_deploy'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::match(['get', 'post'], '/applications/{uuid}/start', [ApplicationsController::class, 'action_deploy'])->middleware(['api.ability:write']); + Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:write']); + Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:write']); - Route::get('/databases', [DatabasesController::class, 'databases']); - Route::post('/databases/postgresql', [DatabasesController::class, 'create_database_postgresql'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/mysql', [DatabasesController::class, 'create_database_mysql'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/mariadb', [DatabasesController::class, 'create_database_mariadb'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/mongodb', [DatabasesController::class, 'create_database_mongodb'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/redis', [DatabasesController::class, 'create_database_redis'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/clickhouse', [DatabasesController::class, 'create_database_clickhouse'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/dragonfly', [DatabasesController::class, 'create_database_dragonfly'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::post('/databases/keydb', [DatabasesController::class, 'create_database_keydb'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/databases', [DatabasesController::class, 'databases'])->middleware(['api.ability:read']); + Route::post('/databases/postgresql', [DatabasesController::class, 'create_database_postgresql'])->middleware(['api.ability:write']); + Route::post('/databases/mysql', [DatabasesController::class, 'create_database_mysql'])->middleware(['api.ability:write']); + Route::post('/databases/mariadb', [DatabasesController::class, 'create_database_mariadb'])->middleware(['api.ability:write']); + Route::post('/databases/mongodb', [DatabasesController::class, 'create_database_mongodb'])->middleware(['api.ability:write']); + Route::post('/databases/redis', [DatabasesController::class, 'create_database_redis'])->middleware(['api.ability:write']); + Route::post('/databases/clickhouse', [DatabasesController::class, 'create_database_clickhouse'])->middleware(['api.ability:write']); + Route::post('/databases/dragonfly', [DatabasesController::class, 'create_database_dragonfly'])->middleware(['api.ability:write']); + Route::post('/databases/keydb', [DatabasesController::class, 'create_database_keydb'])->middleware(['api.ability:write']); - Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid']); - Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']); + Route::patch('/databases/{uuid}', [DatabasesController::class, 'update_by_uuid'])->middleware(['api.ability:write']); + Route::delete('/databases/{uuid}', [DatabasesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); - Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/databases/{uuid}/stop', [DatabasesController::class, 'action_stop'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:write']); + Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:write']); + Route::match(['get', 'post'], '/databases/{uuid}/stop', [DatabasesController::class, 'action_stop'])->middleware(['api.ability:write']); - Route::get('/services', [ServicesController::class, 'services']); - Route::post('/services', [ServicesController::class, 'create_service'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/services', [ServicesController::class, 'services'])->middleware(['api.ability:read']); + Route::post('/services', [ServicesController::class, 'create_service'])->middleware(['api.ability:write']); - Route::get('/services/{uuid}', [ServicesController::class, 'service_by_uuid']); - // Route::patch('/services/{uuid}', [ServicesController::class, 'update_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/services/{uuid}', [ServicesController::class, 'delete_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/services/{uuid}', [ServicesController::class, 'service_by_uuid'])->middleware(['api.ability:read']); + // Route::patch('/services/{uuid}', [ServicesController::class, 'update_by_uuid'])->middleware(['ability:write']); + Route::delete('/services/{uuid}', [ServicesController::class, 'delete_by_uuid'])->middleware(['api.ability:write']); - Route::get('/services/{uuid}/envs', [ServicesController::class, 'envs']); - Route::post('/services/{uuid}/envs', [ServicesController::class, 'create_env'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::get('/services/{uuid}/envs', [ServicesController::class, 'envs'])->middleware(['api.ability:read']); + Route::post('/services/{uuid}/envs', [ServicesController::class, 'create_env'])->middleware(['api.ability:write']); + Route::patch('/services/{uuid}/envs/bulk', [ServicesController::class, 'create_bulk_envs'])->middleware(['api.ability:write']); + Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']); + Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']); - Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware([IgnoreReadOnlyApiToken::class]); - Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware([IgnoreReadOnlyApiToken::class]); + Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:write']); + Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:write']); + Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware(['api.ability:write']); }); Route::group([ diff --git a/routes/web.php b/routes/web.php index d8ba925f2..f7bec5be5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -12,8 +12,8 @@ use App\Livewire\Destination\Show as DestinationShow; use App\Livewire\ForcePasswordReset; use App\Livewire\Notifications\Discord as NotificationDiscord; use App\Livewire\Notifications\Email as NotificationEmail; -use App\Livewire\Notifications\Telegram as NotificationTelegram; use App\Livewire\Notifications\Slack as NotificationSlack; +use App\Livewire\Notifications\Telegram as NotificationTelegram; use App\Livewire\Profile\Index as ProfileIndex; use App\Livewire\Project\Application\Configuration as ApplicationConfiguration; use App\Livewire\Project\Application\Deployment\Index as DeploymentIndex; @@ -287,7 +287,7 @@ Route::middleware(['auth'])->group(function () { 'privateKey' => $privateKeyLocation, 'root' => '/', ]); - if (!$disk->exists($filename)) { + if (! $disk->exists($filename)) { return response()->json(['message' => 'Backup not found.'], 404); } @@ -299,7 +299,7 @@ Route::middleware(['auth'])->group(function () { if ($stream === false || is_null($stream)) { abort(500, 'Failed to open stream for the requested file.'); } - while (!feof($stream)) { + while (! feof($stream)) { echo fread($stream, 2048); flush(); } @@ -307,7 +307,7 @@ Route::middleware(['auth'])->group(function () { fclose($stream); }, 200, [ 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . basename($filename) . '"', + 'Content-Disposition' => 'attachment; filename="'.basename($filename).'"', ]); } catch (\Throwable $e) { return response()->json(['message' => $e->getMessage()], 500); diff --git a/versions.json b/versions.json index 2d1633ed6..b6f301ebc 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.376" + "version": "4.0.0-beta.377" }, "nightly": { - "version": "4.0.0-beta.377" + "version": "4.0.0-beta.378" }, "helper": { "version": "1.0.4"