## Secure Delete Pattern - Alpine.js Modal

### MANDATORY: Use HTTP DELETE Routes for Resource Deletion

All resource deletion must go through proper HTTP DELETE routes to ensure security middlewares (CSRF, authentication, authorization) are properly applied.

**⚠️ CRITICAL SECURITY ISSUE:**
Direct Livewire controller method calls (e.g., `wire:click="deleteFlowConfirmed"`) bypass HTTP routes and ALL security middlewares, including:
- CSRF protection
- Route middleware (auth, tenant context)
- Request validation
- Proper authorization checks

#### ❌ INSECURE Pattern (Direct Livewire Calls)

```php
// ❌ BAD: Livewire component directly calls controller
class IndexFlows extends Component
{
    public function deleteFlowConfirmed()
    {
        // This bypasses HTTP routes and security middlewares!
        app(FlowsController::class)->destroy($this->modalData['id']);
    }
}
```

```blade
{{-- ❌ BAD: Blade triggers Livewire method --}}
<button wire:click="confirmDelete({{ $flow->id }}, '{{ $flow->name }}')">
    Delete
</button>
```

**Why this is insecure:**
1. ❌ No CSRF token validation
2. ❌ Bypasses route middleware stack
3. ❌ No FormRequest validation
4. ❌ Inconsistent with Laravel standards
5. ❌ Cannot leverage HTTP security features

#### ✅ SECURE Pattern (Alpine.js + HTTP DELETE)

**Step 1: Create Reusable Alpine.js Modal Component**

```blade
{{-- resources/views/components/ipaas/delete-confirmation-modal.blade.php --}}

{{--
    Reusable Delete Confirmation Modal Component

    This component provides a secure delete confirmation modal using Alpine.js
    and HTTP DELETE requests. It replaces Livewire direct controller calls
    with proper RESTful HTTP requests that go through all security middlewares.

    Security Benefits:
    - CSRF protection via Laravel's CSRF token
    - Proper authentication/authorization via route middleware
    - Consistent with Laravel RESTful conventions
    - All security middlewares are engaged

    Parameters:
    - entity: Type of entity being deleted ('flow', 'connector', etc.)
    - route-prefix: Laravel route name prefix (e.g., 'ipaas.flows')
    - refresh-event: (Optional) Livewire event to dispatch after successful delete
                     If provided, only the Livewire component refreshes (no page reload)
                     If omitted, the entire page will reload

    Usage:
    <x-ipaas.delete-confirmation-modal 
        entity="flow" 
        route-prefix="ipaas.flows"
        refresh-event="refresh-flows-index" 
    />

    Trigger from Blade:
    <button onclick="confirmDelete({{ $flow->id }}, '{{ $flow->name }}', 'flow')">
        Delete
    </button>
--}}

@props([
    'entity' => 'item',
    'routePrefix' => '',
    'refreshEvent' => '',
])

<div x-data="deleteConfirmationModal('{{ $entity }}', '{{ $routePrefix }}', '{{ $refreshEvent }}')"
     x-show="showModal"
     x-cloak
     @open-delete-modal.window="handleOpenModal($event.detail)"
     class="fixed inset-0 flex items-center justify-center bg-gray-500 bg-opacity-75 z-50"
     style="display: none;">
    
    <!-- Modal Content -->
    <div @click.away="showModal = false" 
         class="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
        
        <!-- Header -->
        <div class="flex items-center justify-between mb-4">
            <h3 class="text-lg font-semibold text-gray-900">
                Confirm Deletion
            </h3>
            <button @click="showModal = false" class="text-gray-400 hover:text-gray-600">
                <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
                </svg>
            </button>
        </div>

        <!-- Body -->
        <div class="mb-6">
            <p class="text-sm text-gray-700">
                Are you sure you want to delete 
                <strong x-text="entityName"></strong>?
            </p>
            <p class="text-sm text-gray-500 mt-2">
                This action cannot be undone.
            </p>
        </div>

        <!-- Loading State -->
        <div x-show="isDeleting" class="mb-4">
            <div class="flex items-center text-blue-600">
                <svg class="animate-spin h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24">
                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                </svg>
                <span>Deleting...</span>
            </div>
        </div>

        <!-- Footer Actions -->
        <div class="flex justify-end gap-3">
            <button @click="showModal = false"
                    :disabled="isDeleting"
                    class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50">
                Cancel
            </button>
            <button @click="confirmDelete"
                    :disabled="isDeleting"
                    class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 disabled:opacity-50">
                Delete
            </button>
        </div>
    </div>
</div>

<script>
    /**
     * Global function to trigger delete confirmation modal
     * 
     * @param {number} id - Entity ID to delete
     * @param {string} name - Entity name for display
     * @param {string} entityType - Type of entity ('flow', 'connector', etc.)
     */
    function confirmDelete(id, name, entityType) {
        window.dispatchEvent(new CustomEvent('open-delete-modal', {
            detail: { id: id, name: name, entityType: entityType }
        }));
    }

    /**
     * Alpine.js component for delete confirmation modal
     * 
     * Handles:
     * - Modal state management
     * - HTTP DELETE request with CSRF token
     * - Loading states
     * - Success/error notifications
     * - Livewire component refresh (optional)
     */
    function deleteConfirmationModal(entityType, routePrefix, refreshEvent) {
        return {
            showModal: false,
            entityId: null,
            entityName: '',
            entityType: entityType,
            isDeleting: false,
            routePrefix: routePrefix,
            refreshEvent: refreshEvent,

            /**
             * Handle modal open event
             */
            handleOpenModal(detail) {
                if (detail.entityType === this.entityType) {
                    this.entityId = detail.id;
                    this.entityName = detail.name;
                    this.showModal = true;
                }
            },

            /**
             * Perform DELETE request via HTTP
             * Uses fetch API with CSRF token for security
             */
            async confirmDelete() {
                if (this.isDeleting) return;

                this.isDeleting = true;

                try {
                    // Build DELETE route URL
                    const deleteUrl = `/${this.routePrefix.replace('.', '/')}/destroy/${this.entityId}`;
                    
                    // Perform HTTP DELETE with CSRF token (Laravel security)
                    const response = await fetch(deleteUrl, {
                        method: 'DELETE',
                        headers: {
                            'Content-Type': 'application/json',
                            'Accept': 'application/json',
                            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
                            'X-Requested-With': 'XMLHttpRequest'
                        }
                    });

                    const data = await response.json();

                    if (response.ok) {
                        this.showModal = false;
                        this.showNotification('success', `${this.capitalizeFirst(this.entityType)} deleted successfully`);

                        // Refresh Livewire component if refresh event provided
                        if (this.refreshEvent) {
                            setTimeout(() => {
                                window.Livewire.dispatch(this.refreshEvent);
                            }, 500);
                        } else {
                            // Fallback: reload page if no refresh event
                            setTimeout(() => {
                                window.location.reload();
                            }, 1500);
                        }
                    } else {
                        const errorMessage = data.message || 'An error occurred while deleting';
                        this.showNotification('error', this.escapeHtml(errorMessage));
                    }
                } catch (error) {
                    console.error('Delete error:', error);
                    this.showNotification('error', 'Failed to delete. Please try again.');
                } finally {
                    this.isDeleting = false;
                }
            },

            /**
             * Show notification using existing alert system
             */
            showNotification(type, message) {
                window.dispatchEvent(new CustomEvent('showalert', {
                    detail: {
                        response: {
                            type: type,
                            message: message
                        }
                    }
                }));
            },

            /**
             * Escape HTML for XSS protection
             */
            escapeHtml(text) {
                const div = document.createElement('div');
                div.textContent = text;
                return div.innerHTML;
            },

            /**
             * Capitalize first letter of string
             */
            capitalizeFirst(str) {
                return str.charAt(0).toUpperCase() + str.slice(1);
            }
        }
    }
</script>
```

**Step 2: Update Controller to Support Both AJAX and Redirect**

```php
// src/App/Http/Controllers/Ipaas/flows/FlowsController.php

public function destroy($id)
{
    try {
        $flow = Flow::findOrFail($id);
        
        Log::info('Deleting flow', [
            'flow_id' => $flow->id,
            'flow_name' => $flow->name,
            'user_id' => auth()->id(),
        ]);
        
        DeleteFlow::delete($flow);
        
        Log::info('Flow deleted successfully', [
            'flow_id' => $id,
            'user_id' => auth()->id(),
        ]);

        // Return JSON for AJAX requests (Alpine.js modal)
        if (request()->expectsJson()) {
            return response()->json([
                'success' => true,
                'message' => 'Flow deleted successfully'
            ], 200);
        }

        // Fallback: redirect for traditional HTML forms
        return redirect()->route('ipaas.flows.index')
            ->with('success', 'Flow deleted successfully');

    } catch (ModelNotFoundException $e) {
        Log::warning('Attempt to delete non-existent flow', [
            'flow_id' => $id,
            'user_id' => auth()->id(),
        ]);

        if (request()->expectsJson()) {
            return response()->json([
                'success' => false,
                'message' => 'Flow not found'
            ], 404);
        }

        abort(404, 'Flow not found');

    } catch (\Exception $e) {
        Log::error('Failed to delete flow', [
            'flow_id' => $id,
            'error' => $e->getMessage(),
            'user_id' => auth()->id(),
        ]);

        if (request()->expectsJson()) {
            return response()->json([
                'success' => false,
                'message' => 'Failed to delete flow'
            ], 500);
        }

        return redirect()->route('ipaas.flows.index')
            ->with('error', 'Failed to delete flow');
    }
}
```

**Step 3: Ensure DELETE Route Exists**

```php
// routes/tenant.php

Route::prefix('ipaas')->group(function () {
    Route::paramResource('flows', FlowsController::class);

    // ⚠️ MANUAL ROUTE: paramResource doesn't generate destroy with {id} correctly
    // This explicit route ensures DELETE /flows/destroy/{id} works properly
    Route::delete('/flows/destroy/{id}', [FlowsController::class, 'destroy'])
        ->name('flows.destroy');
});
```

**Step 4: Update Blade View**

```blade
{{-- resources/views/livewire/ipaas/flows/index-flows.blade.php --}}

<div>
    {{-- Flows list --}}
    @foreach($flows as $flow)
        <tr>
            <td>{{ $flow->name }}</td>
            <td>
                {{-- ✅ SECURE: Triggers HTTP DELETE via Alpine.js --}}
                <button onclick="confirmDelete({{ $flow->id }}, '{{ $flow->name }}', 'flow')"
                        class="text-red-600 hover:text-red-800">
                    Delete
                </button>
            </td>
        </tr>
    @endforeach

    {{-- Secure delete modal component --}}
    <x-ipaas.delete-confirmation-modal 
        entity="flow" 
        route-prefix="ipaas.flows"
        refresh-event="refresh-flows-index" 
    />
</div>
```

**Step 5: Simplify Livewire Component**

```php
// src/App/Livewire/Ipaas/Flows/IndexFlows.php

class IndexFlows extends Component
{
    // ✅ Remove all delete-related methods
    // No more: deleteFlowConfirmed(), confirmDelete(), cancel(), etc.
    // Alpine.js handles everything via HTTP DELETE

    protected $listeners = [
        'refresh-flows-index' => '$refresh', // Listen for Alpine.js refresh event
    ];

    public function render()
    {
        return view('livewire.ipaas.flows.index-flows', [
            'flows' => Flow::all(),
        ]);
    }
}
```

#### Pattern Benefits

**✅ Security:**
1. CSRF protection automatically applied
2. All route middlewares engaged (auth, tenant context, etc.)
3. Proper authorization checks via policies if configured
4. Cannot bypass security via direct method calls

**✅ Laravel Standards:**
1. Uses standard RESTful DELETE routes
2. Controller methods handle HTTP concerns
3. Consistent with framework conventions
4. Easy to understand for new developers

**✅ User Experience:**
1. No page reload (AJAX + Livewire refresh)
2. Instant visual feedback
3. Consistent modal design across application
4. Loading states during deletion

**✅ Maintainability:**
1. Reusable component across all resources
2. Single source of truth for delete confirmation UI
3. Easy to test HTTP DELETE routes
4. Clear separation of concerns

#### Refactoring Checklist

When migrating from Livewire direct calls to secure HTTP DELETE:

- [ ] Create reusable Alpine.js delete modal component
- [ ] Update controller `destroy()` method to support JSON responses
- [ ] Ensure explicit DELETE route exists in `routes/tenant.php`
- [ ] Update Blade view to use `onclick="confirmDelete(...)"`
- [ ] Remove Livewire delete methods from component class
- [ ] Add `$listeners` for refresh event in Livewire component
- [ ] Test CSRF protection is working
- [ ] Test delete functionality in browser
- [ ] Verify Livewire component refreshes without page reload
- [ ] Check logs for proper security context (user_id, IP, etc.)

#### Testing Requirements

```php
// tests/Feature/Http/Controllers/Ipaas/FlowsControllerTest.php

test('can delete a flow successfully via HTTP DELETE', function () {
    // Arrange: Create flow
    $flow = Flow::create(['name' => 'Test Flow']);
    
    // Act: Send DELETE request (mimics Alpine.js fetch)
    $response = $this->deleteTenantJson("/ipaas/flows/destroy/{$flow->id}");
    
    // Assert: JSON response
    $response->assertStatus(200)
        ->assertJson([
            'success' => true,
            'message' => 'Flow deleted successfully'
        ]);
    
    // Assert: Flow was deleted
    expect(Flow::find($flow->id))->toBeNull();
});

test('returns 404 when deleting non-existent flow', function () {
    $response = $this->deleteTenantJson("/ipaas/flows/destroy/999999");
    
    $response->assertStatus(404)
        ->assertJson([
            'success' => false,
            'message' => 'Flow not found'
        ]);
});

test('CSRF protection is enforced on DELETE', function () {
    // This test verifies that without CSRF token, the request fails
    // Laravel's TestCase automatically includes CSRF, so we test by removing it
    
    $flow = Flow::create(['name' => 'Test Flow']);
    
    // Make raw request without CSRF
    $response = $this->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class)
        ->deleteTenantJson("/ipaas/flows/destroy/{$flow->id}");
    
    // Without CSRF middleware, this would fail in production
    // This test documents the security requirement
});
```

#### Why This Pattern Exists

**Root Cause of the anti-pattern:** Livewire components calling controller methods directly bypass the HTTP middleware stack entirely — CSRF validation, route middleware (`auth`, tenant context), FormRequest validation, and authorization checks are all skipped. The call looks legitimate from PHP's perspective but carries no HTTP-layer security guarantees.

**The rule:** Any action that mutates or deletes a resource MUST be triggered via an HTTP route, not via a direct method call from a Livewire component. The HTTP route is the security boundary. Everything behind it (CSRF, auth, middleware, policies) depends on the request going through it.

---

