Autenticación de miembros

Autentica a los miembros del sitio mediante las mutaciones GraphQL de backend siteMember llamadas a través del gateway - registro, inicio de sesión, refresco, cierre de sesión, restablecimiento de contraseña y verificación de email. El SDK slim no incluye helpers de auth; tú gestionas la sesión.

24 de julio de 2026

Resumen

Los miembros del sitio son los usuarios finales de tu sitio publicado - las personas que se registran, inician sesión y tienen una cuenta. Un miembro es un registro de un modelo de contenido en tu workspace, identificado por su modelSlug (por ejemplo members) más una identity (normalmente un email). No hay una tabla de usuarios especial - los miembros son registros.

La autenticación de miembros es una función de backend activa, expuesta a través del namespace de mutaciones GraphQL siteMember. La llamas a través del gateway con createCmssyClient(cmssy).query(). El SDK slim no incluye ningún helper de auth - ninguna ruta de auth, ningún middleware, ningún lector de sesión. El flujo y la sesión son tuyos.

Tú gestionas la sesión. login y refresh devuelven los accessToken y refreshToken en crudo - el backend no establece ninguna cookie. Tu app decide dónde viven (una cookie httpOnly que establece tu ruta es la opción segura por defecto) y adjunta Authorization: Bearer <accessToken> en las llamadas autenticadas.


1. El cliente del gateway

Crea una instancia de createCmssyClient y mantén cada documento de mutación a su lado. Cada mutación vive bajo el namespace siteMember.

// lib/cmssy-members.ts
import { createCmssyClient } from "@cmssy/react";
import { cmssy } from "@/cmssy.config";

// One gateway client. Every siteMember mutation goes through client.query().
export const client = createCmssyClient(cmssy);

export const REGISTER = `mutation Register($input: SiteMemberRegisterInput!) {
  siteMember { register(input: $input) { success message } }
}`;

export const LOGIN = `mutation Login($input: SiteMemberLoginInput!) {
  siteMember {
    login(input: $input) {
      success
      message
      accessToken
      refreshToken
      accessTokenExpiresIn
    }
  }
}`;

export const REFRESH = `mutation Refresh($refreshToken: String!) {
  siteMember {
    refresh(refreshToken: $refreshToken) {
      success
      message
      accessToken
      refreshToken
      accessTokenExpiresIn
    }
  }
}`;

export const LOGOUT = `mutation Logout($refreshToken: String!) {
  siteMember { logout(refreshToken: $refreshToken) { success message } }
}`;

export const LOGOUT_EVERYWHERE = `mutation LogoutEverywhere {
  siteMember { logoutEverywhere { success message } }
}`;

export const FORGOT_PASSWORD = `mutation ForgotPassword($modelSlug: String!, $identity: String!) {
  siteMember { forgotPassword(modelSlug: $modelSlug, identity: $identity) { success message } }
}`;

export const RESET_PASSWORD = `mutation ResetPassword($token: String!, $newPassword: String!) {
  siteMember { resetPassword(token: $token, newPassword: $newPassword) { success message } }
}`;

export const VERIFY_EMAIL = `mutation VerifyEmail($token: String!) {
  siteMember { verifyEmail(token: $token) { success message } }
}`;

2. Registro

register crea un registro de miembro a partir de modelSlug, identity y password; los campos de perfil adicionales van en fields. Devuelve { success, message } y, cuando se requiere verificación de email, envía un email de verificación.

// app/api/auth/register/route.ts
import { client, REGISTER } from "@/lib/cmssy-members";

export async function POST(request: Request) {
  const { email, password, name } = await request.json();
  const { siteMember } = await client.query(REGISTER, {
    input: {
      modelSlug: "members",
      identity: email,
      password,
      fields: { name },
    },
  });
  return Response.json(siteMember.register); // { success, message }
}

3. Inicia sesión y guarda la sesión

login devuelve { success, message, accessToken, refreshToken, accessTokenExpiresIn }. El backend no establece cookie - es tu ruta la que guarda los tokens. Abajo van a cookies httpOnly que tu app controla.

// app/api/auth/login/route.ts
import { cookies } from "next/headers";
import { client, LOGIN } from "@/lib/cmssy-members";

export async function POST(request: Request) {
  const { email, password } = await request.json();
  const { siteMember } = await client.query(LOGIN, {
    input: { modelSlug: "members", identity: email, password },
  });
  const res = siteMember.login;
  if (!res.success || !res.accessToken) {
    return Response.json({ ok: false, message: res.message }, { status: 401 });
  }
  // You own the session - store the raw tokens yourself. The backend sets no cookie.
  const jar = await cookies();
  jar.set("member_access", res.accessToken, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
    maxAge: res.accessTokenExpiresIn ?? 900,
  });
  jar.set("member_refresh", res.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
  });
  return Response.json({ ok: true });
}

4. Peticiones autenticadas

Cualquier operación con ámbito de miembro necesita el token de acceso. Léelo de tu almacén y envíalo como Authorization: Bearer <accessToken>; el backend lo resuelve al miembro conectado. logoutEverywhere es una de esas llamadas - revoca todas las sesiones del miembro.

// app/api/auth/logout-everywhere/route.ts
import { cookies } from "next/headers";
import { client, LOGOUT_EVERYWHERE } from "@/lib/cmssy-members";

export async function POST() {
  const accessToken = (await cookies()).get("member_access")?.value;
  if (!accessToken) return Response.json({ ok: false }, { status: 401 });
  // Authenticated call: attach the access token as a Bearer header.
  const { siteMember } = await client.query(LOGOUT_EVERYWHERE, {}, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  return Response.json(siteMember.logoutEverywhere);
}

5. Refresco

refresh intercambia un refreshToken por un par de tokens nuevo (la misma forma que login). Rota ambas cookies con los nuevos valores; si falla, borra la sesión.

// app/api/auth/refresh/route.ts
import { cookies } from "next/headers";
import { client, REFRESH } from "@/lib/cmssy-members";

export async function POST() {
  const jar = await cookies();
  const refreshToken = jar.get("member_refresh")?.value;
  if (!refreshToken) return Response.json({ ok: false }, { status: 401 });
  const { siteMember } = await client.query(REFRESH, { refreshToken });
  const res = siteMember.refresh;
  if (!res.success || !res.accessToken) {
    jar.delete("member_access");
    jar.delete("member_refresh");
    return Response.json({ ok: false }, { status: 401 });
  }
  // refresh rotates the pair - overwrite both cookies with the new tokens.
  jar.set("member_access", res.accessToken, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
    maxAge: res.accessTokenExpiresIn ?? 900,
  });
  jar.set("member_refresh", res.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
  });
  return Response.json({ ok: true });
}

6. Cerrar sesión

logout revoca un único refreshToken. Llámalo y luego borra tus propias cookies.

// app/api/auth/logout/route.ts
import { cookies } from "next/headers";
import { client, LOGOUT } from "@/lib/cmssy-members";

export async function POST() {
  const jar = await cookies();
  const refreshToken = jar.get("member_refresh")?.value;
  if (refreshToken) {
    await client.query(LOGOUT, { refreshToken });
  }
  jar.delete("member_access");
  jar.delete("member_refresh");
  return Response.json({ ok: true });
}

7. Restablecimiento de contraseña

forgotPassword(modelSlug, identity) envía por email un enlace de restablecimiento y siempre informa éxito, para que nadie pueda sondear qué cuentas existen. resetPassword(token, newPassword) consume el token de ese enlace y establece la nueva contraseña.

// forgot password - always reports success (no account enumeration)
await client.query(FORGOT_PASSWORD, { modelSlug: "members", identity: email });

// reset password - token comes from the emailed link (/reset-password?token=...)
await client.query(RESET_PASSWORD, { token, newPassword });

8. Verificación de email

verifyEmail(token) consume el token del enlace de verificación. Cuando un workspace exige verificación, el inicio de sesión permanece bloqueado hasta que el miembro esté verificado.

// verify email - token comes from the verification link (/verify-email?token=...)
await client.query(VERIFY_EMAIL, { token });

Referencia de mutaciones

Cada mutación vive bajo el namespace siteMember. login y refresh devuelven tokens; el resto devuelve { success, message }.

MutaciónArgumentosDevuelve
registerinput: { modelSlug, identity, password, fields }{ success, message }
logininput: { modelSlug, identity, password }{ success, message, accessToken, refreshToken, accessTokenExpiresIn }
refreshrefreshToken{ success, message, accessToken, refreshToken, accessTokenExpiresIn }
logoutrefreshToken{ success, message }
logoutEverywhere- (necesita Bearer){ success, message }
forgotPasswordmodelSlug, identity{ success, message }
resetPasswordtoken, newPassword{ success, message }
verifyEmailtoken{ success, message }