mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-17 17:21:04 +00:00
feat: add cloud-init scripts management UI in Security section
Add comprehensive cloud-init script management interface in the Security section, allowing users to create, edit, delete, and reuse cloud-init scripts across their team. New Components: - CloudInitScripts: Main listing page with grid view of scripts - CloudInitScriptForm: Modal form for create/edit operations Features: - Create new cloud-init scripts with name and content - Edit existing scripts - Delete scripts with confirmation (requires typing script name) - View script preview (first 200 characters) - Scripts are encrypted in database - Full authorization using CloudInitScriptPolicy - Real-time updates via Livewire events UI Location: - Added to Security section nav: /security/cloud-init-scripts - Positioned between Cloud Tokens and API Tokens - Follows existing security UI patterns Files Created: - app/Livewire/Security/CloudInitScripts.php - app/Livewire/Security/CloudInitScriptForm.php - resources/views/livewire/security/cloud-init-scripts.blade.php - resources/views/livewire/security/cloud-init-script-form.blade.php Files Modified: - routes/web.php - Added route - resources/views/components/security/navbar.blade.php - Added nav link 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
97
app/Livewire/Security/CloudInitScriptForm.php
Normal file
97
app/Livewire/Security/CloudInitScriptForm.php
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Security;
|
||||||
|
|
||||||
|
use App\Models\CloudInitScript;
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class CloudInitScriptForm extends Component
|
||||||
|
{
|
||||||
|
use AuthorizesRequests;
|
||||||
|
|
||||||
|
public bool $modal_mode = true;
|
||||||
|
|
||||||
|
public ?int $scriptId = null;
|
||||||
|
|
||||||
|
public string $name = '';
|
||||||
|
|
||||||
|
public string $script = '';
|
||||||
|
|
||||||
|
public function mount(?int $scriptId = null)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'script' => 'required|string',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.required' => 'Script name is required.',
|
||||||
|
'name.max' => 'Script name cannot exceed 255 characters.',
|
||||||
|
'script.required' => 'Cloud-init script content is required.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($this->scriptId) {
|
||||||
|
// Update existing script
|
||||||
|
$cloudInitScript = CloudInitScript::ownedByCurrentTeam()->findOrFail($this->scriptId);
|
||||||
|
$this->authorize('update', $cloudInitScript);
|
||||||
|
|
||||||
|
$cloudInitScript->update([
|
||||||
|
'name' => $this->name,
|
||||||
|
'script' => $this->script,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$message = 'Cloud-init script updated successfully.';
|
||||||
|
} else {
|
||||||
|
// Create new script
|
||||||
|
$this->authorize('create', CloudInitScript::class);
|
||||||
|
|
||||||
|
CloudInitScript::create([
|
||||||
|
'team_id' => currentTeam()->id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'script' => $this->script,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$message = 'Cloud-init script created successfully.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->reset(['name', 'script', 'scriptId']);
|
||||||
|
$this->dispatch('scriptSaved');
|
||||||
|
$this->dispatch('success', $message);
|
||||||
|
|
||||||
|
if ($this->modal_mode) {
|
||||||
|
$this->dispatch('closeModal');
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.security.cloud-init-script-form');
|
||||||
|
}
|
||||||
|
}
|
||||||
52
app/Livewire/Security/CloudInitScripts.php
Normal file
52
app/Livewire/Security/CloudInitScripts.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Security;
|
||||||
|
|
||||||
|
use App\Models\CloudInitScript;
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class CloudInitScripts extends Component
|
||||||
|
{
|
||||||
|
use AuthorizesRequests;
|
||||||
|
|
||||||
|
public $scripts;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->authorize('viewAny', CloudInitScript::class);
|
||||||
|
$this->loadScripts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getListeners()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'scriptSaved' => 'loadScripts',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadScripts()
|
||||||
|
{
|
||||||
|
$this->scripts = CloudInitScript::ownedByCurrentTeam()->orderBy('created_at', 'desc')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteScript(int $scriptId)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$script = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId);
|
||||||
|
$this->authorize('delete', $script);
|
||||||
|
|
||||||
|
$script->delete();
|
||||||
|
$this->loadScripts();
|
||||||
|
|
||||||
|
$this->dispatch('success', 'Cloud-init script deleted successfully.');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return handleError($e, $this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.security.cloud-init-scripts');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,11 @@
|
|||||||
<button>Cloud Tokens</button>
|
<button>Cloud Tokens</button>
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
|
@can('viewAny', App\Models\CloudInitScript::class)
|
||||||
|
<a href="{{ route('security.cloud-init-scripts') }}">
|
||||||
|
<button>Cloud-Init Scripts</button>
|
||||||
|
</a>
|
||||||
|
@endcan
|
||||||
<a href="{{ route('security.api-tokens') }}">
|
<a href="{{ route('security.api-tokens') }}">
|
||||||
<button>API Tokens</button>
|
<button>API Tokens</button>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<form wire:submit='save' class="flex flex-col gap-4">
|
||||||
|
<x-forms.input id="name" label="Script Name" helper="A descriptive name for this cloud-init script." required />
|
||||||
|
|
||||||
|
<x-forms.textarea id="script" label="Script Content" rows="12"
|
||||||
|
helper="Enter your cloud-init script. Supports both bash scripts and cloud-config YAML format." required />
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
@if ($modal_mode)
|
||||||
|
<x-forms.button type="button" @click="$dispatch('closeModal')">
|
||||||
|
Cancel
|
||||||
|
</x-forms.button>
|
||||||
|
@endif
|
||||||
|
<x-forms.button type="submit" isHighlighted>
|
||||||
|
{{ $scriptId ? 'Update Script' : 'Create Script' }}
|
||||||
|
</x-forms.button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<div>
|
||||||
|
<x-security.navbar />
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<h2 class="pb-4">Cloud-Init Scripts</h2>
|
||||||
|
@can('create', App\Models\CloudInitScript::class)
|
||||||
|
<x-modal-input buttonTitle="+ Add" title="New Cloud-Init Script">
|
||||||
|
<livewire:security.cloud-init-script-form />
|
||||||
|
</x-modal-input>
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
<div class="pb-4 text-sm">Manage reusable cloud-init scripts for server initialization.</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
|
@forelse ($scripts as $script)
|
||||||
|
<div wire:key="script-{{ $script->id }}" class="box group">
|
||||||
|
<div class="flex flex-col gap-2 mx-6">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="box-title">{{ $script->name }}</div>
|
||||||
|
<div class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
|
Created {{ $script->created_at->diffForHumans() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="text-xs text-neutral-500 dark:text-neutral-400 mb-1">Script Preview:</div>
|
||||||
|
<pre
|
||||||
|
class="p-2 text-xs rounded bg-neutral-100 dark:bg-coolgray-100 overflow-x-auto max-h-32 overflow-y-auto">{{ Str::limit($script->script, 200) }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-2">
|
||||||
|
@can('update', $script)
|
||||||
|
<x-modal-input buttonTitle="Edit" title="Edit Cloud-Init Script">
|
||||||
|
<livewire:security.cloud-init-script-form :scriptId="$script->id"
|
||||||
|
wire:key="edit-{{ $script->id }}" />
|
||||||
|
</x-modal-input>
|
||||||
|
@endcan
|
||||||
|
|
||||||
|
@can('delete', $script)
|
||||||
|
<x-modal-confirmation title="Confirm Script Deletion?" isErrorButton buttonTitle="Delete"
|
||||||
|
submitAction="deleteScript({{ $script->id }})" :actions="[
|
||||||
|
'This cloud-init script will be permanently deleted.',
|
||||||
|
'This action cannot be undone.',
|
||||||
|
]" confirmationText="{{ $script->name }}"
|
||||||
|
confirmationLabel="Please confirm the deletion by entering the script name below"
|
||||||
|
shortConfirmationLabel="Script Name" :confirmWithPassword="false"
|
||||||
|
step2ButtonText="Delete Script" />
|
||||||
|
@endcan
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="text-neutral-500">No cloud-init scripts found. Create one to get started.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -34,6 +34,7 @@ use App\Livewire\Project\Shared\Logs;
|
|||||||
use App\Livewire\Project\Shared\ScheduledTask\Show as ScheduledTaskShow;
|
use App\Livewire\Project\Shared\ScheduledTask\Show as ScheduledTaskShow;
|
||||||
use App\Livewire\Project\Show as ProjectShow;
|
use App\Livewire\Project\Show as ProjectShow;
|
||||||
use App\Livewire\Security\ApiTokens;
|
use App\Livewire\Security\ApiTokens;
|
||||||
|
use App\Livewire\Security\CloudInitScripts;
|
||||||
use App\Livewire\Security\CloudTokens;
|
use App\Livewire\Security\CloudTokens;
|
||||||
use App\Livewire\Security\PrivateKey\Index as SecurityPrivateKeyIndex;
|
use App\Livewire\Security\PrivateKey\Index as SecurityPrivateKeyIndex;
|
||||||
use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow;
|
use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow;
|
||||||
@@ -275,6 +276,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||||||
Route::get('/security/private-key/{private_key_uuid}', SecurityPrivateKeyShow::class)->name('security.private-key.show');
|
Route::get('/security/private-key/{private_key_uuid}', SecurityPrivateKeyShow::class)->name('security.private-key.show');
|
||||||
|
|
||||||
Route::get('/security/cloud-tokens', CloudTokens::class)->name('security.cloud-tokens');
|
Route::get('/security/cloud-tokens', CloudTokens::class)->name('security.cloud-tokens');
|
||||||
|
Route::get('/security/cloud-init-scripts', CloudInitScripts::class)->name('security.cloud-init-scripts');
|
||||||
Route::get('/security/api-tokens', ApiTokens::class)->name('security.api-tokens');
|
Route::get('/security/api-tokens', ApiTokens::class)->name('security.api-tokens');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user