Svelte es un framework que compila los componentes a JavaScript muy ligero, y desde su versión 5 gestiona la reactividad mediante runes. En este tutorial lo combinaremos con Laravel e Inertia para crear un acceso sin contraseña, en el que el usuario se identifica con un código de seis cifras que recibe por email.

Tal y como ocurre en el resto de versiones de este login, no habrá formulario de registro ni de recuperación de contraseña: si el email no tiene cuenta, se crea al introducir el código correcto, y si el usuario olvida cómo entrar, le basta con pedir otro código.

Introducción

Con este artículo cerramos la serie dedicada al login sin contraseña. Ya hemos visto la versión con Blade, en donde se explica la lógica de los códigos, y las versiones con Livewire, Vue y React. La aplicación de Svelte comparte con estas dos últimas el controlador y la configuración de Inertia en el servidor, así que nos centraremos en lo que Svelte hace de forma distinta.

Para frenar a los bots usaremos Cloudflare Turnstile con el componente de Svelte del paquete Laracaptcha, que expone el token como una prop enlazable.

Te resultará más fácil seguir el tutorial si conoces Svelte 5. Si no es así, el tutorial interactivo de Svelte es el mejor punto de partida, y en especial la parte dedicada a las runes, que usaremos en todos los componentes.

El proyecto completo está en GitHub. Esta es la pantalla de acceso que tendremos al final:

Acceso sin contraseña con Inertia, Svelte y Cloudflare Turnstile

Creación del proyecto

Para empezar necesitarás PHP 8.3 o superior, composer y Node.js.

Instala Laravel con Inertia y Svelte

Crea un proyecto nuevo de Laravel e instala tanto las dependencias del servidor como las del navegador:

composer create-project laravel/laravel login-svelte
cd login-svelte
composer require inertiajs/inertia-laravel edulazaro/laracaptcha
npm install svelte @inertiajs/svelte @sveltejs/vite-plugin-svelte

El paquete @inertiajs/svelte es el adaptador de Inertia para Svelte, y @sveltejs/vite-plugin-svelte permite a Vite compilar los archivos .svelte.

Prepara el servidor

En el servidor, Inertia se configura igual que en las versiones de Vue y React: el middleware que crea el comando php artisan inertia:middleware, registrado en bootstrap/app.php, y la vista raíz resources/views/app.blade.php con las directivas @vite('resources/js/app.js'), @inertiaHead e @inertia. En el tutorial con Vue se explica cada paso en detalle.

El controlador también es el mismo: le pasa a la página del login el resultado de Captcha::props(), con el proveedor, la clave pública y el script, y valida el token que llega en el campo captcha con la regla de Laracaptcha.

Configura Vite

En vite.config.js usaremos el plugin de Svelte y definiremos el alias @laracaptcha, que apunta a los componentes que el paquete incluye como código fuente:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
    plugins: [
        laravel({ input: ['resources/js/app.js'], refresh: true }),
        svelte(),
    ],
    resolve: {
        alias: {
            '@laracaptcha': new URL('./vendor/edulazaro/laracaptcha/resources/js', import.meta.url).pathname,
        },
    },
});

Monta la aplicación

Sustituye el contenido de resources/js/app.js por este código:

import { createInertiaApp } from '@inertiajs/svelte';
import { mount } from 'svelte';

createInertiaApp({
    resolve: (name) => {
        const pages = import.meta.glob('./pages/**/*.svelte', { eager: true });

        return pages[`./pages/${name}.svelte`];
    },
    setup({ el, App, props }) {
        mount(App, { target: el, props });
    },
});

En Svelte 5 los componentes ya no se crean con new, sino con la función mount, que recibe el componente, el elemento en donde se debe montar y sus props. Las páginas se buscarán en el directorio resources/js/pages.

Inertia 3 y las runes

Antes de escribir las páginas, conviene saber que el adaptador de Svelte de Inertia 3 está construido sobre runes, lo que cambia la forma de trabajar respecto a versiones anteriores:

  • useForm: Ya no devuelve un store al que se accede con $form, sino un objeto reactivo. Los campos se leen y se modifican directamente, como form.email, y se pueden enlazar a un input con bind:value.
  • page: Los datos de la página también son un objeto reactivo que se importa desde @inertiajs/svelte, de modo que las props compartidas se leen con page.props, sin el símbolo $.

Si vienes de Inertia 2, es el cambio que más código te hará modificar. Consulta la documentación de formularios de Inertia para ver el resto de opciones.

Las páginas de Svelte

La aplicación tiene tres páginas: el formulario del email, el del código y el panel.

Crea el formulario del email

Crea el archivo resources/js/pages/auth/Login.svelte:

<script>
    import { useForm } from '@inertiajs/svelte';
    import { Captcha } from '@laracaptcha/svelte';

    let { captcha } = $props();

    const form = useForm({ email: '', captcha: '' });
    let captchaWidget;

    function submit(event) {
        event.preventDefault();

        form.post('/login', {
            onError: () => captchaWidget?.reset(),
        });
    }
</script>

<main class="card">
    <h1>Entrar</h1>

    <form onsubmit={submit}>
        <label for="email">Email</label>
        <input id="email" type="email" bind:value={form.email} required autocomplete="username">
        {#if form.errors.email}<p class="error">{form.errors.email}</p>{/if}

        <Captcha bind:this={captchaWidget} {...captcha} language="es" bind:token={form.captcha} />
        {#if form.errors.captcha}<p class="error">{form.errors.captcha}</p>{/if}

        <button type="submit" disabled={form.processing}>Enviarme el código</button>
    </form>
</main>

Estas son las claves del componente:

  • $props(): Es la rune con la que Svelte 5 recibe las props. Aquí obtenemos captcha, que el controlador rellena con Captcha::props(). Más información en la documentación de $props.
  • bind:token: El componente de Laracaptcha declara su token como una prop enlazable, por lo que podemos vincularlo directamente al campo captcha del formulario. Cuando el usuario supera la verificación se rellena solo, y si caduca o falla vuelve a quedar vacío. Puedes leer cómo funcionan estas props en la documentación de $bindable.
  • bind:this: Guarda la instancia del componente para poder llamar a su método reset. Cloudflare no acepta el mismo token dos veces, así que si el servidor rechaza el envío, pedimos uno nuevo desde onError.
  • onsubmit: En Svelte 5 los eventos se escriben como atributos normales, y el preventDefault se hace dentro de la función.

Si en el .env cambias a reCAPTCHA v3, el componente dejará de mostrar el desafío, pero bind:token seguirá recibiendo el token, que se renueva antes de caducar.

Crea el formulario del código

En resources/js/pages/auth/Code.svelte necesitamos dos formularios, uno para verificar el código y otro para reenviarlo:

<script>
    import { Link, page, useForm } from '@inertiajs/svelte';

    let { email } = $props();

    const form = useForm({ code: '' });
    const resend = useForm({});

    function submit(event) {
        event.preventDefault();

        form.post('/login/codigo');
    }

    function sendAnother() {
        resend.post('/login/codigo/reenviar', {
            onSuccess: () => form.reset('code'),
        });
    }
</script>

En la plantilla mostramos el mensaje de estado con page.props.status y el campo del código. Aquí hay que tener cuidado con un detalle propio de Svelte: las llaves dentro de un atributo se interpretan como una expresión, así que pattern="[0-9]{6}" acabaría convertido en [0-9]6. Para que el patrón llegue intacto al navegador, hay que escribirlo como una cadena dentro de una expresión:

<input id="code" type="text" bind:value={form.code} required inputmode="numeric"
    autocomplete="one-time-code" pattern={'[0-9]{6}'} maxlength="6" class="code">

Así queda el formulario:

Formulario del código de acceso con Inertia y Svelte

Crea el panel

El panel lee el usuario desde page.props.auth.user y cierra la sesión con el router de Inertia:

<script>
    import { page, router } from '@inertiajs/svelte';
</script>

<main class="card">
    <h1>Hola, {page.props.auth.user.name}</h1>
    <button type="button" onclick={() => router.post('/logout')}>Cerrar sesión</button>
</main>

Para cambiar el título de cada página usamos el elemento especial <svelte:head>, que permite añadir etiquetas al <head> del documento desde cualquier componente. Finalmente, compila con npm run build y arranca la aplicación con php artisan serve.

Tests

Como el servidor es el mismo, los tests también lo son: comprueban con las aserciones de Inertia qué componente se renderiza y con qué props, y usan Captcha::fake() para que el componente de Svelte no cargue el script de Cloudflare. Por ejemplo, este test verifica que la página del código recibe el email al que se envió:

public function test_the_code_page_shows_the_address_the_code_went_to(): void
{
    $this->askForCode('ana@example.com');

    $this->get('/login/codigo')->assertInertia(fn (Assert $page) => $page
        ->component('auth/Code')
        ->where('email', 'ana@example.com'));
}

El repositorio incluye once tests, que puedes ejecutar con php artisan test.

Finalizando

Ya tienes un login sin contraseña con Laravel, Inertia y Svelte. Antes de publicarlo, configura un servidor SMTP, tus propias claves de Turnstile y los registros SPF del dominio, y recuerda compilar los assets en cada despliegue.

Con este tutorial termina la serie. Si quieres comparar cómo se resuelve el mismo problema en cada stack, aquí tienes el resto de versiones: verás que el servidor apenas cambia y que la diferencia está en cómo cada framework recibe el token del captcha.

Tienes el código del proyecto en GitHub y todas las opciones del paquete en la documentación de Laracaptcha.

Esto ha sido todo. Espero que este tutorial te haya resultado de ayuda.