Files
coolify/.cursor/rules/form-components.mdc

452 lines
15 KiB
Plaintext

---
description: Enhanced form components with built-in authorization system
globs: resources/views/**/*.blade.php, app/View/Components/Forms/*.php
alwaysApply: true
---
# Enhanced Form Components with Authorization
## Overview
Coolify's form components now feature **built-in authorization** that automatically handles permission-based UI control, dramatically reducing code duplication and improving security consistency.
## Enhanced Components
All form components now support the `canGate` authorization system:
- **[Input.php](mdc:app/View/Components/Forms/Input.php)** - Text, password, and other input fields
- **[Select.php](mdc:app/View/Components/Forms/Select.php)** - Dropdown selection components
- **[Textarea.php](mdc:app/View/Components/Forms/Textarea.php)** - Multi-line text areas
- **[Checkbox.php](mdc:app/View/Components/Forms/Checkbox.php)** - Boolean toggle components
- **[Button.php](mdc:app/View/Components/Forms/Button.php)** - Action buttons
## Authorization Parameters
### Core Parameters
```php
public ?string $canGate = null; // Gate name: 'update', 'view', 'deploy', 'delete'
public mixed $canResource = null; // Resource model instance to check against
public bool $autoDisable = true; // Automatically disable if no permission
```
### How It Works
```php
// Automatic authorization logic in each component
if ($this->canGate && $this->canResource && $this->autoDisable) {
$hasPermission = Gate::allows($this->canGate, $this->canResource);
if (! $hasPermission) {
$this->disabled = true;
// For Checkbox: also sets $this->instantSave = false;
}
}
```
## Usage Patterns
### ✅ Recommended: Single Line Pattern
**Before (Verbose, 6+ lines per element):**
```html
@can('update', $application)
<x-forms.input id="application.name" label="Name" />
<x-forms.checkbox instantSave id="application.settings.is_static" label="Static Site" />
<x-forms.button type="submit">Save</x-forms.button>
@else
<x-forms.input disabled id="application.name" label="Name" />
<x-forms.checkbox disabled id="application.settings.is_static" label="Static Site" />
@endcan
```
**After (Clean, 1 line per element):**
```html
<x-forms.input canGate="update" :canResource="$application" id="application.name" label="Name" />
<x-forms.checkbox instantSave canGate="update" :canResource="$application" id="application.settings.is_static" label="Static Site" />
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
```
**Result: 90% code reduction!**
### Component-Specific Examples
#### Input Fields
```html
<!-- Basic input with authorization -->
<x-forms.input
canGate="update"
:canResource="$application"
id="application.name"
label="Application Name" />
<!-- Password input with authorization -->
<x-forms.input
type="password"
canGate="update"
:canResource="$application"
id="application.database_password"
label="Database Password" />
<!-- Required input with authorization -->
<x-forms.input
required
canGate="update"
:canResource="$application"
id="application.fqdn"
label="Domain" />
```
#### Select Dropdowns
```html
<!-- Build pack selection -->
<x-forms.select
canGate="update"
:canResource="$application"
id="application.build_pack"
label="Build Pack"
required>
<option value="nixpacks">Nixpacks</option>
<option value="static">Static</option>
<option value="dockerfile">Dockerfile</option>
</x-forms.select>
<!-- Server selection -->
<x-forms.select
canGate="createAnyResource"
:canResource="auth()->user()->currentTeam"
id="server_id"
label="Target Server">
@foreach($servers as $server)
<option value="{{ $server->id }}">{{ $server->name }}</option>
@endforeach
</x-forms.select>
```
#### Checkboxes with InstantSave
```html
<!-- Static site toggle -->
<x-forms.checkbox
instantSave
canGate="update"
:canResource="$application"
id="application.settings.is_static"
label="Is it a static site?"
helper="Enable if your application serves static files" />
<!-- Debug mode toggle -->
<x-forms.checkbox
instantSave
canGate="update"
:canResource="$application"
id="application.settings.is_debug_enabled"
label="Debug Mode"
helper="Enable debug logging for troubleshooting" />
<!-- Build server toggle -->
<x-forms.checkbox
instantSave
canGate="update"
:canResource="$application"
id="application.settings.is_build_server_enabled"
label="Use Build Server"
helper="Use a dedicated build server for compilation" />
```
#### Textareas
```html
<!-- Configuration textarea -->
<x-forms.textarea
canGate="update"
:canResource="$application"
id="application.docker_compose_raw"
label="Docker Compose Configuration"
rows="10"
monacoEditorLanguage="yaml"
useMonacoEditor />
<!-- Custom commands -->
<x-forms.textarea
canGate="update"
:canResource="$application"
id="application.post_deployment_command"
label="Post-Deployment Commands"
placeholder="php artisan migrate"
helper="Commands to run after deployment" />
```
#### Buttons
```html
<!-- Save button -->
<x-forms.button
canGate="update"
:canResource="$application"
type="submit">
Save Configuration
</x-forms.button>
<!-- Deploy button -->
<x-forms.button
canGate="deploy"
:canResource="$application"
wire:click="deploy">
Deploy Application
</x-forms.button>
<!-- Delete button -->
<x-forms.button
canGate="delete"
:canResource="$application"
wire:click="confirmDelete"
class="button-danger">
Delete Application
</x-forms.button>
```
## Advanced Usage
### Custom Authorization Logic
```html
<!-- Disable auto-control for complex permissions -->
<x-forms.input
canGate="update"
:canResource="$application"
autoDisable="false"
:disabled="$application->is_deployed || !$application->canModifySettings()"
id="deployment.setting"
label="Advanced Setting" />
```
### Multiple Permission Checks
```html
<!-- Combine multiple authorization requirements -->
<x-forms.checkbox
canGate="deploy"
:canResource="$application"
autoDisable="false"
:disabled="!$application->hasDockerfile() || !Gate::allows('deploy', $application)"
id="docker.setting"
label="Docker-Specific Setting" />
```
### Conditional Resources
```html
<!-- Different resources based on context -->
<x-forms.button
:canGate="$isEditing ? 'update' : 'view'"
:canResource="$resource"
type="submit">
{{ $isEditing ? 'Save Changes' : 'View Details' }}
</x-forms.button>
```
## Supported Gates
### Resource-Level Gates
- `view` - Read access to resource details
- `update` - Modify resource configuration and settings
- `deploy` - Deploy, restart, or manage resource state
- `delete` - Remove or destroy resource
- `clone` - Duplicate resource to another location
### Global Gates
- `createAnyResource` - Create new resources of any type
- `manageTeam` - Team administration permissions
- `accessServer` - Server-level access permissions
## Supported Resources
### Primary Resources
- `$application` - Application instances and configurations
- `$service` - Docker Compose services and components
- `$database` - Database instances (PostgreSQL, MySQL, etc.)
- `$server` - Physical or virtual server instances
### Container Resources
- `$project` - Project containers and environments
- `$environment` - Environment-specific configurations
- `$team` - Team and organization contexts
### Infrastructure Resources
- `$privateKey` - SSH private keys and certificates
- `$source` - Git sources and repositories
- `$destination` - Deployment destinations and targets
## Component Behavior
### Input Components (Input, Select, Textarea)
When authorization fails:
- **disabled = true** - Field becomes non-editable
- **Visual styling** - Opacity reduction and disabled cursor
- **Form submission** - Values are ignored in forms
- **User feedback** - Clear visual indication of restricted access
### Checkbox Components
When authorization fails:
- **disabled = true** - Checkbox becomes non-clickable
- **instantSave = false** - Automatic saving is disabled
- **State preservation** - Current value is maintained but read-only
- **Visual styling** - Disabled appearance with reduced opacity
### Button Components
When authorization fails:
- **disabled = true** - Button becomes non-clickable
- **Event blocking** - Click handlers are ignored
- **Visual styling** - Disabled appearance and cursor
- **Loading states** - Loading indicators are disabled
## Migration Guide
### Converting Existing Forms
**Old Pattern:**
```html
<form wire:submit='submit'>
@can('update', $application)
<x-forms.input id="name" label="Name" />
<x-forms.select id="type" label="Type">...</x-forms.select>
<x-forms.checkbox instantSave id="enabled" label="Enabled" />
<x-forms.button type="submit">Save</x-forms.button>
@else
<x-forms.input disabled id="name" label="Name" />
<x-forms.select disabled id="type" label="Type">...</x-forms.select>
<x-forms.checkbox disabled id="enabled" label="Enabled" />
@endcan
</form>
```
**New Pattern:**
```html
<form wire:submit='submit'>
<x-forms.input canGate="update" :canResource="$application" id="name" label="Name" />
<x-forms.select canGate="update" :canResource="$application" id="type" label="Type">...</x-forms.select>
<x-forms.checkbox instantSave canGate="update" :canResource="$application" id="enabled" label="Enabled" />
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
</form>
```
### Gradual Migration Strategy
1. **Start with new forms** - Use the new pattern for all new components
2. **Convert high-traffic areas** - Migrate frequently used forms first
3. **Batch convert similar forms** - Group similar authorization patterns
4. **Test thoroughly** - Verify authorization behavior matches expectations
5. **Remove old patterns** - Clean up legacy @can/@else blocks
## Testing Patterns
### Component Authorization Tests
```php
// Test authorization integration in components
test('input component respects authorization', function () {
$user = User::factory()->member()->create();
$application = Application::factory()->create();
// Member should see disabled input
$component = Livewire::actingAs($user)
->test(TestComponent::class, [
'canGate' => 'update',
'canResource' => $application
]);
expect($component->get('disabled'))->toBeTrue();
});
test('checkbox disables instantSave for unauthorized users', function () {
$user = User::factory()->member()->create();
$application = Application::factory()->create();
$component = Livewire::actingAs($user)
->test(CheckboxComponent::class, [
'instantSave' => true,
'canGate' => 'update',
'canResource' => $application
]);
expect($component->get('disabled'))->toBeTrue();
expect($component->get('instantSave'))->toBeFalse();
});
```
### Integration Tests
```php
// Test full form authorization behavior
test('application form respects member permissions', function () {
$member = User::factory()->member()->create();
$application = Application::factory()->create();
$this->actingAs($member)
->get(route('application.edit', $application))
->assertSee('disabled')
->assertDontSee('Save Configuration');
});
```
## Best Practices
### Consistent Gate Usage
- Use `update` for configuration changes
- Use `deploy` for operational actions
- Use `view` for read-only access
- Use `delete` for destructive actions
### Resource Context
- Always pass the specific resource being acted upon
- Use team context for creation permissions
- Consider nested resource relationships
### Error Handling
- Provide clear feedback for disabled components
- Use helper text to explain permission requirements
- Consider tooltips for disabled buttons
### Performance
- Authorization checks are cached per request
- Use eager loading for resource relationships
- Consider query optimization for complex permissions
## Common Patterns
### Application Configuration Forms
```html
<!-- Application settings with consistent authorization -->
<x-forms.input canGate="update" :canResource="$application" id="application.name" label="Name" />
<x-forms.select canGate="update" :canResource="$application" id="application.build_pack" label="Build Pack">...</x-forms.select>
<x-forms.checkbox instantSave canGate="update" :canResource="$application" id="application.settings.is_static" label="Static Site" />
<x-forms.button canGate="update" :canResource="$application" type="submit">Save</x-forms.button>
```
### Service Configuration Forms
```html
<!-- Service stack configuration with authorization -->
<x-forms.input canGate="update" :canResource="$service" id="service.name" label="Service Name" />
<x-forms.input canGate="update" :canResource="$service" id="service.description" label="Description" />
<x-forms.checkbox canGate="update" :canResource="$service" instantSave id="service.connect_to_docker_network" label="Connect To Predefined Network" />
<x-forms.button canGate="update" :canResource="$service" type="submit">Save</x-forms.button>
<!-- Service-specific fields -->
<x-forms.input canGate="update" :canResource="$service" type="{{ data_get($field, 'isPassword') ? 'password' : 'text' }}"
required="{{ str(data_get($field, 'rules'))?->contains('required') }}"
id="fields.{{ $serviceName }}.value"></x-forms.input>
<!-- Service restart modal - wrapped with @can -->
@can('update', $service)
<x-modal-confirmation title="Confirm Service Application Restart?"
buttonTitle="Restart"
submitAction="restartApplication({{ $application->id }})" />
@endcan
```
### Server Management Forms
```html
<!-- Server configuration with appropriate gates -->
<x-forms.input canGate="update" :canResource="$server" id="server.name" label="Server Name" />
<x-forms.select canGate="update" :canResource="$server" id="server.type" label="Server Type">...</x-forms.select>
<x-forms.button canGate="delete" :canResource="$server" wire:click="deleteServer">Delete Server</x-forms.button>
```
### Resource Creation Forms
```html
<!-- New resource creation -->
<x-forms.input canGate="createAnyResource" :canResource="auth()->user()->currentTeam" id="name" label="Name" />
<x-forms.select canGate="createAnyResource" :canResource="auth()->user()->currentTeam" id="server_id" label="Server">...</x-forms.select>
<x-forms.button canGate="createAnyResource" :canResource="auth()->user()->currentTeam" type="submit">Create Application</x-forms.button>
```