Guías
Ver como MarkdownNext.js · Pages Router
El mismo SDK en el Pages Router: `_app`, rutas de API y `getServerSideProps`.
npm install @andco/sdk @andco/sdk-reactProvider en _app
import { AndcoProvider } from "@andco/sdk-react";
import type { AndcoBrowserOptions } from "@andco/sdk/browser";
import type { AppProps } from "next/app";
const clientOptions: AndcoBrowserOptions = {
clientId: process.env.NEXT_PUBLIC_ANDCO_CLIENT_ID!,
redirectTo: process.env.NEXT_PUBLIC_APP_ORIGIN!,
initialScopes: ["openid", "email", "profile"],
};
export default function App({ Component, pageProps }: AppProps) {
return (
<AndcoProvider clientOptions={clientOptions}>
<Component {...pageProps} />
</AndcoProvider>
);
}No hace falta "use client": en el Pages Router todo es cliente. Tampoco hay que proteger la construcción, porque es inerte y no toca window hasta que alguien lee la sesión.
Leer la sesión
import { useAndcoSession } from "@andco/sdk-react";
export default function Account() {
const session = useAndcoSession();
if (session === undefined) return <p>Cargando…</p>;
return session ? <p>{session.user.email}</p> : <p>Sin sesión</p>;
}Autorización conducida por el servidor
import type { NextApiRequest, NextApiResponse } from "next";
import { andco } from "~/lib/andco";
import { storeTransaction } from "~/lib/session-cookie";
export default async function handler(_req: NextApiRequest, res: NextApiResponse) {
const { data: request, error } = await andco.oauth.createAuthorizationRequest({
redirectTo: `${process.env.APP_ORIGIN}/api/andco/callback`,
scopes: ["openid", "email", "profile"],
});
if (error) return res.status(500).json({ error: error.code });
storeTransaction(res, request);
res.redirect(request.authorizationUrl.href);
}import type { NextApiRequest, NextApiResponse } from "next";
import { andco } from "~/lib/andco";
import { readTransaction, writeSessionCookie } from "~/lib/session-cookie";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const request = readTransaction(req);
if (!request) return res.status(400).json({ error: "missing transaction" });
const { data: session, error } = await andco.oauth.exchangeCallback({
callbackUrl: new URL(req.url!, process.env.APP_ORIGIN),
request,
});
if (error) return res.status(400).json({ error: error.code });
writeSessionCookie(res, session);
res.redirect("/");
}Datos en getServerSideProps
andco.with(session) no renueva por sí solo — es una consulta directa al token de la sesión, igual que en el flujo genérico del servidor. Un ejemplo completo tiene que resolver la cookie, renovar cuando esté vencida y persistir la sesión renovada:
import type { NextApiRequest, NextApiResponse } from "next";
import type { AndcoAuthorizationRequest, AndcoSession } from "@andco/sdk";
const SESSION_COOKIE = "andco_session";
const TRANSACTION_COOKIE = "andco_tx";
function cookieHeader(name: string, value: string, maxAge: number): string {
return `${name}=${encodeURIComponent(value)}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax; Secure`;
}
export function storeTransaction(res: NextApiResponse, request: AndcoAuthorizationRequest): void {
const serialized = JSON.stringify({ ...request, authorizationUrl: request.authorizationUrl.href });
res.setHeader("Set-Cookie", cookieHeader(TRANSACTION_COOKIE, serialized, 600));
}
export function readTransaction(req: NextApiRequest): AndcoAuthorizationRequest | null {
const raw = req.cookies[TRANSACTION_COOKIE];
if (!raw) return null;
const parsed = JSON.parse(raw);
return { ...parsed, authorizationUrl: new URL(parsed.authorizationUrl) };
}
export function sessionFromCookie(req: NextApiRequest): AndcoSession | null {
const raw = req.cookies[SESSION_COOKIE];
if (!raw) return null;
try {
return JSON.parse(raw) as AndcoSession;
} catch {
return null;
}
}
export function writeSessionCookie(res: NextApiResponse, session: AndcoSession): void {
const maxAge = Math.max(0, session.expiresAt - Math.floor(Date.now() / 1000));
res.setHeader("Set-Cookie", cookieHeader(SESSION_COOKIE, JSON.stringify(session), maxAge));
}No está cifrada: solo la sirve HttpOnly y Secure. Para un dato tan sensible como un refresh token, considera cifrarla o guardar la sesión en tu propio almacén y dejar en la cookie solo un identificador.
import type { GetServerSideProps, InferGetServerSidePropsType } from "next";
import { andco } from "~/lib/andco";
import { sessionFromCookie, writeSessionCookie } from "~/lib/session-cookie";
import { isExpired } from "@andco/sdk";
export const getServerSideProps = (async ({ req, res }) => {
let session = sessionFromCookie(req);
if (!session) return { redirect: { destination: "/api/andco/authorize", permanent: false } };
if (isExpired(session) && session.refreshToken) {
const refreshed = await andco.oauth.refresh(session.refreshToken);
if (refreshed.error) return { redirect: { destination: "/api/andco/authorize", permanent: false } };
session = refreshed.data;
writeSessionCookie(res, session);
}
const { data, error } = await andco.with(session).rest.http.GET("/accounts");
return { props: { accounts: error ? [] : data.data } };
}) satisfies GetServerSideProps;
export default function Accounts({ accounts }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<ul>
{accounts.map((account) => (
<li key={account.id}>{account.name}</li>
))}
</ul>
);
}Los tipos de accounts vienen del contrato OpenAPI a través de data.data, no de una interfaz que escribas a mano; InferGetServerSidePropsType es lo que le pasa ese tipo al componente sin declararlo dos veces.
Nunca devuelvas el token como prop: viaja al navegador dentro del HTML. Devuelve los datos que la página necesita y nada más — aquí, accounts, no la sesión.
Diferencias con el App Router
| Pages Router | App Router | |
|---|---|---|
| Provider | pages/_app.tsx |
Client Component desde el layout |
| Rutas de servidor | pages/api/* |
app/api/*/route.ts |
| Datos del servidor | getServerSideProps |
Server Components |
| Aislar el secreto | convención lib/ |
import "server-only" |
El SDK es el mismo; lo que cambia es dónde vive cada pieza.
