Notificaciones Toast Seguras en Laravel: Guía Completa para Desarrolladores

Como desarrolladores fullstack, sabemos que las notificaciones toast son fundamentales para la experiencia de usuario. Sin embargo, implementarlas correctamente va más allá de simplemente mostrar mensajes. En esta guía, exploraremos cómo crear un sistema robusto de notificaciones toast en Laravel, considerando aspectos de seguridad, rendimiento y mejores prácticas de desarrollo.

¿Por qué importa la implementación correcta?

Las notificaciones toast mal implementadas pueden:

  • Exponer información sensible en el frontend
  • Crear vulnerabilidades XSS
  • Degradar el rendimiento de la aplicación
  • Generar una experiencia de usuario inconsistente
Configuración Base Segura Paso 1: Estructura del Proyecto

Primero, creemos un nuevo proyecto Laravel con las dependencias necesarias:

composer create-project laravel/laravel toast-notifications
cd toast-notifications
composer require pusher/pusher-php-server
npm install toastr
Paso 2: Service Provider Personalizado

Creemos un service provider dedicado para manejar las notificaciones:

php artisan make:provider ToastServiceProvider
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\View;
use App\Services\ToastService;

class ToastServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton('toast', function ($app) {
            return new ToastService();
        });
    }

    public function boot()
    {
        View::composer('*', function ($view) {
            $toasts = session()->get('toasts', []);
            $view->with('toasts', $toasts);
        });
    }
}
Paso 3: Service Class con Validación
<?php

namespace App\Services;

class ToastService
{
    private const ALLOWED_TYPES = ['success', 'error', 'warning', 'info'];
    private const MAX_MESSAGE_LENGTH = 255;

    public function add(string $message, string $type = 'info', array $options = []): void
    {
        // Sanitización y validación
        $message = $this->sanitizeMessage($message);
        $type = $this->validateType($type);
        
        $toast = [
            'message' => $message,
            'type' => $type,
            'timestamp' => now()->timestamp,
            'options' => $this->sanitizeOptions($options)
        ];

        $toasts = session()->get('toasts', []);
        $toasts[] = $toast;
        
        // Limitar número de toasts por sesión
        if (count($toasts) > 10) {
            array_shift($toasts);
        }

        session()->flash('toasts', $toasts);
    }

    private function sanitizeMessage(string $message): string
    {
        $message = strip_tags($message);
        $message = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
        
        if (strlen($message) > self::MAX_MESSAGE_LENGTH) {
            $message = substr($message, 0, self::MAX_MESSAGE_LENGTH) . '...';
        }
        
        return $message;
    }

    private function validateType(string $type): string
    {
        return in_array($type, self::ALLOWED_TYPES) ? $type : 'info';
    }

    private function sanitizeOptions(array $options): array
    {
        $allowedOptions = ['timeout', 'position', 'showProgress'];
        return array_intersect_key($options, array_flip($allowedOptions));
    }
}
Paso 4: Middleware de Rate Limiting

Para prevenir spam de notificaciones, creemos un middleware:

php artisan make:middleware ThrottleToastNotifications
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

class ThrottleToastNotifications
{
    public function handle(Request $request, Closure $next)
    {
        $key = $request->ip() . ':toast-notifications';
        
        if (RateLimiter::tooManyAttempts($key, 10)) {
            return response()->json([
                'error' => 'Too many notifications'
            ], 429);
        }

        RateLimiter::hit($key, 60); // 10 notificaciones por minuto

        return $next($request);
    }
}
Paso 5: Blade Component Reutilizable

Creemos un componente Blade para las notificaciones:

php artisan make:component ToastNotifications
<?php

namespace App\View\Components;

use Illuminate\View\Component;

class ToastNotifications extends Component
{
    public $toasts;

    public function __construct()
    {
        $this->toasts = session('toasts', []);
    }

    public function render()
    {
        return view('components.toast-notifications');
    }
}
{{-- resources/views/components/toast-notifications.blade.php --}}
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2">
    @if(!empty($toasts))
        <script>
            document.addEventListener('DOMContentLoaded', function() {
                const toasts = @json($toasts);
                
                toasts.forEach(function(toast) {
                    // Configuración de seguridad para Toastr
                    toastr.options = {
                        closeButton: true,
                        debug: false,
                        newestOnTop: true,
                        progressBar: true,
                        positionClass: "toast-top-right",
                        preventDuplicates: true,
                        onclick: null,
                        showDuration: "300",
                        hideDuration: "1000",
                        timeOut: toast.options?.timeout || "5000",
                        extendedTimeOut: "1000",
                        showEasing: "swing",
                        hideEasing: "linear",
                        showMethod: "fadeIn",
                        hideMethod: "fadeOut",
                        escapeHtml: true, // Importante para seguridad
                        tapToDismiss: true
                    };

                    // Mostrar notificación según tipo
                    switch(toast.type) {
                        case 'success':
                            toastr.success(toast.message);
                            break;
                        case 'error':
                            toastr.error(toast.message);
                            break;
                        case 'warning':
                            toastr.warning(toast.message);
                            break;
                        case 'info':
                        default:
                            toastr.info(toast.message);
                            break;
                    }
                });
            });
        </script>
    @endif
</div>
Paso 6: Facade para Facilitar el Uso
<?php

namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class Toast extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'toast';
    }
}
Paso 7: Implementación en Controladores
<?php

namespace App\Http\Controllers;

use App\Facades\Toast;
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function update(Request $request, User $user)
    {
        try {
            $validated = $request->validate([
                'name' => 'required|string|max:255',
                'email' => 'required|email|unique:users,email,' . $user->id,
            ]);

            $user->update($validated);

            Toast::add(
                'Usuario actualizado correctamente',
                'success',
                ['timeout' => 3000]
            );

            return redirect()->back();

        } catch (\Exception $e) {
            Toast::add(
                'Error al actualizar el usuario',
                'error'
            );

            return redirect()->back();
        }
    }
}
Mejoras de Seguridad Adicionales Content Security Policy (CSP)

Añade estas directivas a tu CSP para mayor seguridad:

// En app/Http/Middleware/ContentSecurityPolicy.php
public function handle($request, Closure $next)
{
    $response = $next($request);
    
    $response->headers->set('Content-Security-Policy', 
        "script-src 'self' 'unsafe-inline' cdnjs.cloudflare.com; " .
        "style-src 'self' 'unsafe-inline' cdnjs.cloudflare.com;"
    );
    
    return $response;
}
Logging de Seguridad
// En ToastService.php
private function logSuspiciousActivity(string $message): void
{
    if ($this->containsSuspiciousContent($message)) {
        Log::warning('Suspicious toast message detected', [
            'message' => $message,
            'ip' => request()->ip(),
            'user_id' => auth()->id(),
            'timestamp' => now()
        ]);
    }
}

private function containsSuspiciousContent(string $message): bool
{
    $suspiciousPatterns = [
        '/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi',
        '/javascript:/i',
        '/onload\s*=/i',
        '/onerror\s*=/i'
    ];
    
    foreach ($suspiciousPatterns as $pattern) {
        if (preg_match($pattern, $message)) {
            return true;
        }
    }
    
    return false;
}
Testing

Crea tests para asegurar la funcionalidad:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Facades\Toast;
use Illuminate\Foundation\Testing\RefreshDatabase;

class ToastNotificationTest extends TestCase
{
    public function test_toast_sanitizes_html_content()
    {
        $maliciousMessage = '<script>alert("XSS")</script>Hello';
        
        Toast::add($maliciousMessage, 'info');
        
        $toasts = session('toasts');
        $this->assertStringNotContainsString('<script>', $toasts[0]['message']);
        $this->assertEquals('Hello', $toasts[0]['message']);
    }

    public function test_toast_limits_message_length()
    {
        $longMessage = str_repeat('a', 300);
        
        Toast::add($longMessage, 'info');
        
        $toasts = session('toasts');
        $this->assertLessThanOrEqual(255, strlen($toasts[0]['message']));
    }
}
Optimización de Rendimiento Lazy Loading del JavaScript
{{-- En tu layout principal --}}
@push('scripts')
    <script>
        // Cargar Toastr solo cuando sea necesario
        @if(!empty(session('toasts')))
            if (!window.toastr) {
                const script = document.createElement('script');
                script.src = 'https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js';
                script.onload = function() {
                    // Inicializar toasts después de cargar la librería
                    initializeToasts();
                };
                document.head.appendChild(script);
            } else {
                initializeToasts();
            }
        @endif
    </script>
@endpush

Las notificaciones toast son un detalle aparentemente pequeño, pero su correcta implementación marca la diferencia entre una aplicación amateur y una profesional. Al considerar aspectos de seguridad y rendimiento desde el principio, creamos una base sólida para el crecimiento de nuestra aplicación.

¿Qué opinas de esta implementación? ¿Has encontrado otros desafíos al implementar notificaciones en tus proyectos?