Imagen destacada del post -WordPress Headless: Autenticación con Next.js 3-

WordPress Headless: Autenticación con Next.js 3


¿Hola que tal? 🙋‍♂️. Como ya comenté en el anterior post donde vimos como hacer una autenticación con NextJS y WordPress pero con el App Router de Next, en esta ocasión haremos lo mismo pero con la nueva versión de next-auth que viene preparada para hacer nuestro login con Server Actions.

Y como la mayor diferencia entre la marera del anterior post y esta son las ya mencionadas Server Actions vamos a hacer una breve explicación. Al igual que los Server Components que vimos en la entrada anterior, se trata de una funcionalidad nueva de React para la versión 19 que a día de hoy está en un release experimental (Canary) pero NextJS los ha añadido al framework y a partir de la versión 14 los trata como estables (documentación). En su utilidad original, según la documentación de React, son funciones asíncronas que se pueden llamar desde Client Components pero que se ejecutan en el servidor. NextJS extiende su uso también para Server Components y aparte se pueden, no como muchos puedan pensar por la mayoría de tutoriales, llamar desde el atributo action de un formulario, useEffect, Event Handlers o desde el mismo botón del formulario. Esto en muchos casos nos puede aliviar trabajo sobre todo para aplicaciones como las que estamos haciendo WordPress Headless, donde la fuente de datos es la WordPress API y no queremos hacer las peticiones desde el cliente por seguridad u opacidad de nuestro host o tokens de acceso, y aparte como veremos nos podemos ahorrar un paso validando y controlando errores una vez sola y con menos código ya que Next lo hace por nosotros.


La forma de instalar nuestro proyecto es exactamente igual a la entrada anterior pero con una diferencia que ya comentamos y es que para instalar la última versión de nuestra librería de autenticación next-auth lo hacemos a día de hoy con npm install next-auth@beta, de todas formas asegúrate por si acaso en la documentación official. También como la vez anterior te dejo el enlace al repositorio para que te vayas copiando los archivos. En cuanto a configuración si que hay un par de cambios.

Crea el archivo .env.local y añade las siguientes variables de entorno:

AUTH_SECRET=secret
AUTH_URL=http://localhost:3000
NEXTAUTH_URL=http://localhost:3000
WP_BACK_URL=http://localhost:8000

Recuerda que puedes crear tu secret con npx auth secret y también una cosa que se me pasó en el anterior post es que valor de la variable WP_BACK_URL corresponde a mi instalación de WordPress así que acuérdate de cambiarla al host de la tuya.

Lo siguiente es revisar los archivos que han cambiado respecto al proyecto anterior que son:

src/app/server/auth.js Este primero es el archivo de configuración principal y como puedes comprobar a diferencia del anterior exporta una serie de funciones nuevas que puedes utilizar desde el servidor. Aparte añadimos los loggers para controlar mejor lo errores que ocurran.

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { AUTH_SECRET } from '@/utils/constants';
import { wpAuth } from '@/services/wp-auth';

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      async authorize(credentials) {
        return await wpAuth(credentials);
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.accessToken = user.accessToken;
      }
      return token;
    },
  },
  jwt: {
    maxAge: 60 * 60 * 24 * 7,
  },
  pages: {
    signIn: '/signin',
    error: '/signin',
  },
  secret: AUTH_SECRET,
  logger: {
    error(error) {
      console.error('error-type', error.type);
      console.error('error-message', error.message);
    },
    warn(code, ...message) {
      console.log('log-warn', { code, message });
    },
    debug(code, ...message) {
      console.log('log-debug', { code, message });
    },
  },
});

src/app/api/auth/[...nextauth]/route.js | En este archivo next-auth exporta de forma dinámica las rutas api para por ejemplo hacer logout desde Client Components.

import { handlers } from '@/server/auth';

export const { GET, POST } = handlers;

Como novedad añadimos un archivo nuevo que contendrá nuestra función Server Action que es donde llamamos a nuestra nueva función signIn que habíamos exportado desde la configuración:

// src/app/actions/auth.js
    
import { AuthError } from 'next-auth';
import { signIn as logIn } from '@/server/auth';

export const signIn = async (state, form) => {
  try {
    await logIn('credentials', form);
  } catch (error) {
    if (error instanceof AuthError) {
      return {
        ...state,
        status: 'error',
        message: 'Incorrect email or password',
      };
    }

    throw error;
  }
};

En cuanto a nuestros archivos de páginas la única diferencia significativa es la forma de importar a nuestra sesión en el servidor que la vez anterior llamábamos a la función getServerAuthSession que habíamos creado en nuestro archivo de configuración y ahora llamamos a la función auth.

import Signin from '@/components/page/Signin';
import { cookies } from 'next/headers';
import { auth } from '@/server/auth'; // <<-------
import { redirect } from 'next/navigation';

export default async function SigninPage() {
  const session = await auth();

  if (session?.user) {
    return redirect('/dashboard');
  }

  const cookieStore = cookies();
  const csrfToken = cookieStore.get('authjs.csrf-token');

  return <Signin csrfToken={csrfToken?.value} />;
}

Ahora vamos al archivo donde ocurre toda la magia que es src/components/page/Signin.jsx y vamos a fijarnos en un punto muy importante. Como puedes darte cuenta si echamos un vistazo al comienzo de nuestro componente estamos llamando a un hook llamado useFormState al que le pasamos nuestra server action y a su vez este le pasa otra función formAction al atributo action de nuestro formulario. Esto nos permite controlar el estado del formulario devolviendo un objeto de estado desde nuestra acción con mensajes de error en la validación o cualquier otro contratiempo. Como ya comenté anteriormente a mi juicio esta forma puede permite ahorrarte un paso extra de validación en el cliente además de peticiones fetch a api routes que eran muy comunes en el pasado.

'use client';

import { useFormState } from 'react-dom';
import { signIn } from '@/app/actions/auth';

const initialState = {
  form: 'signin',
  status: 'initial',
  message: '',
};

export default function Signin({ csrfToken }) {
  const [state, formAction] = useFormState(signIn, initialState);

  return (
    <>
      <div className="flex min-h-full flex-1 flex-col justify-center px-6 py-12 lg:px-8">
        <div className="sm:mx-auto sm:w-full sm:max-w-sm">
          <img
            className="mx-auto h-10 w-auto"
            src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
            alt="Your Company"
          />
          <h2 className="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
            Sign in to your account
          </h2>
        </div>

        <div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
          <form action={formAction} className="space-y-6">
            <input name="csrfToken" type="hidden" defaultValue={csrfToken} />

            <div>
              <label
                htmlFor="email"
                className="block text-sm font-medium leading-6 text-gray-900"
              >
                Email address
              </label>
              <div className="mt-2">
                <input
                  id="email"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                />
              </div>
            </div>

            <div>
              <div className="flex items-center justify-between">
                <label
                  htmlFor="password"
                  className="block text-sm font-medium leading-6 text-gray-900"
                >
                  Password
                </label>
                <div className="text-sm">
                  <a
                    href="#"
                    className="font-semibold text-indigo-600 hover:text-indigo-500"
                  >
                    Forgot password?
                  </a>
                </div>
              </div>
              <div className="mt-2">
                <input
                  id="password"
                  name="password"
                  type="password"
                  autoComplete="current-password"
                  required
                  className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                />
              </div>
            </div>

            <div>
              <button
                type="submit"
                className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
              >
                Sign in
              </button>
            </div>
            <div>{state?.message}</div>
          </form>
        </div>
      </div>
    </>
  );
}

Para terminar ya con la serie de autenticación vamos a dar la pincelada final que será poder traernos recursos con rutas protegidas de la api de WordPress, que no se si te has dado cuenta pero ya lo hacemos en nuestro servicio wp-auth en el archivo src/services/wp-auth.js. La clave está en el accessToken que seteamos en la sesión a la hora de autenticarnos, que enviaremos por la cabecera de autorización en cada petición a un endpoint protegido de WordPress. Crea el archivo src/services/wp-post.js e intenta hacerlo, aunque de todas formas te dejo mi versión aquí abajo.

import { WP_BACK_URL } from '@/utils/constants';

export const wpPosts = async ({ accessToken }) => {
    const resp = await fetch(`${WP_BACK_URL}/wp-json/wp/v2/posts`, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    return await resp.json();
}

Teniendo en cuenta la inmensa cantidad de plugins que te ayudan a crear contenido en WordPress y que exponen rutas api para que las consumas, te puedes hacer una idea de la potencialidad que tiene este sistema para crear aplicaciones de todo tipo. Hay empresas que llevan años haciendo aplicaciones de esta forma con resultados excelentes.

¡Y esto sería todo! 🥳 Gracias por llegar hasta aquí, saludos y nos vemos en siguientes posts.