452 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			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>
 | 
						|
``` |