{
  "id": "add-an-admin-table",
  "type": "playbooks",
  "category": "playbooks",
  "locale": "es",
  "url": "/es/playbooks/add-an-admin-table",
  "title": "Prompt-to-PR: Agregar una tabla de datos de administración",
  "description": "Procedimiento estándar para agregar una tabla de datos de administración paginada, ordenable y con búsqueda del lado del servidor en Next.js usando TanStack Table y una consulta PostgreSQL — sin magia de ORM.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "TypeScript",
    "PostgreSQL",
    "Tailwind"
  ],
  "tags": [
    "nextjs",
    "postgres",
    "typescript",
    "tailwind",
    "auth"
  ],
  "difficulty": "medium",
  "updated": "2026-06-08",
  "markdown": "Las tablas de administración son una tarea común de codificación con IA que frecuentemente produce implementaciones inseguras, completamente del lado del cliente. Este playbook mantiene la paginación y ordenamiento del lado del servidor y protege explícitamente la ruta.\n\n## 1. Requisito\n\nAgregar una página `/admin/users` accesible solo para usuarios con `role = \"admin\"`. La tabla está paginada del lado del servidor (25 filas), ordenable por columna y con búsqueda por correo electrónico. Los datos provienen de una tabla `users` de PostgreSQL mediante una consulta parametrizada — sin SQL no parametrizado.\n\n## 2. Primer Prompt\n\n```txt title=\"First Prompt\"\nAdd an admin users table to this Next.js 15 App Router project.\n\nRequirements:\n1. Create `src/app/admin/users/page.tsx` — an async Server Component.\n   - Protect it: check the session (using the existing auth helper at\n     src/lib/auth.ts). If no session or session.user.role !== \"admin\",\n     call notFound() or redirect to /.\n   - Read `page` (default 1), `sort` (default \"createdAt\"), `order`\n     (\"asc\"|\"desc\", default \"desc\"), and `q` (search string) from\n     `searchParams`.\n   - Fetch data by calling a function `getUsers({ page, sort, order, q })`\n     from `src/lib/queries/users.ts`.\n   - Pass results to `<UsersTable>` client component.\n2. Create `src/lib/queries/users.ts` exporting `getUsers`.\n   - Use parameterized SQL via the existing db client (Drizzle or postgres.js,\n     whichever is in this project).\n   - Allowed sort columns whitelist: [\"email\",\"name\",\"createdAt\",\"role\"].\n     Reject any other sort value by defaulting to \"createdAt\".\n   - Return `{ rows: User[], total: number }`.\n   - Page size: 25.\n3. Create `src/components/admin/UsersTable.tsx` — a Client Component using\n   TanStack Table v8.\n   - Columns: email, name, role, createdAt (formatted), actions (Edit link).\n   - Column headers are clickable to sort — update URL search params via\n     useRouter/useSearchParams (do not manage sort state locally).\n   - Render a search input that debounces 300 ms before updating the URL.\n   - Render a pagination row: Previous / Next and \"Page N of M\".\n4. Install `@tanstack/react-table` if not already present.\n5. Do not implement the Edit action — leave it as a placeholder link.\n```\n\n## 3. Cambios de archivos esperados\n\n```txt\npackage.json                                   (@tanstack/react-table if missing)\nsrc/app/admin/users/page.tsx                   (new — protected Server Component)\nsrc/lib/queries/users.ts                       (new — parameterized query)\nsrc/components/admin/UsersTable.tsx            (new — TanStack Table client component)\n```\n\n## 4. Lista de verificación\n\n- La página verifica `session.user.role === \"admin\"` del lado del servidor — sin protección solo del lado del cliente.\n- `getUsers` permite solo columnas de ordenamiento autorizadas antes de interpolar en SQL — sin entrada de usuario sin procesar en nombres de columna.\n- Todos los valores WHERE/LIMIT/OFFSET usan marcadores parametrizados, no concatenación de cadenas.\n- La búsqueda (`q`) usa `ILIKE '%' || $1 || '%'` con un parámetro, no interpolación de cadenas.\n- TanStack Table se usa solo en el componente con `\"use client\"` — sin importar `@tanstack/react-table` en Server Components.\n- Los parámetros de búsqueda de la URL manejan el estado de orden/página/búsqueda — el botón Atrás del navegador funciona correctamente.\n- El conteo `total` proviene de un `SELECT COUNT(*)` en la misma función de consulta — no un escaneo completo de tabla separado de la consulta paginada.\n\n## 5. Comandos de prueba\n\n```bash\nbun dev\n\n# Hit the page as an unauthenticated user\ncurl -I http://localhost:3000/admin/users\n# Expect: 404 or redirect, not 200\n\n# Log in as an admin user in the browser, visit /admin/users\n# Confirm: 25 rows, sortable columns, search, pagination\n\n# SQL audit — confirm no dynamic column names slip through\ngrep -n \"sort\\|order\\|column\" src/lib/queries/users.ts\n\nbun tsc --noEmit\nbun test\n```\n\n## 6. Fallos comunes\n\n- **Inyección SQL a través de la columna de ordenamiento** — el agente interpola `sort` directamente: `` `ORDER BY ${sort}` ``. Debe usar una lista blanca para obtener un nombre de columna seguro.\n- **`searchParams` es asíncrono en Next.js 15+** — debe usarse con `await`: `const { page } = await searchParams`.\n- **TanStack Table se renderiza en un Server Component** — error de compilación porque usa `useState`. Muévalo a un componente `\"use client\"`.\n- **Conteo `total` desviado por una página** — `Math.ceil(total / 25)` da un número de página incorrecto si `total` es 0. Protección: `Math.max(1, Math.ceil(total / 25))`.\n- **Falta verificación de rol** — el agente solo verifica una sesión, no el rol de administrador. Cualquier usuario autenticado puede acceder a la tabla.\n\n## 7. Prompt de corrección\n\n```txt title=\"Fix Prompt\"\nThe sort column in getUsers is interpolated directly into SQL:\n  `ORDER BY ${sort} ${order}`\nThis is a SQL injection risk.\n\nFix: create an allowlist object:\n  const ALLOWED_SORT: Record<string, string> = {\n    email: \"users.email\",\n    name: \"users.name\",\n    createdAt: \"users.created_at\",\n    role: \"users.role\",\n  };\n  const safeSort = ALLOWED_SORT[sort] ?? \"users.created_at\";\n\nThen use `safeSort` in the query. The order direction must also be\nvalidated: only accept \"asc\" or \"desc\", default to \"desc\".\n```\n\n## 8. Descripción del PR\n\n```md title=\"PR description\"\n## Feature: Admin users table at /admin/users\n\n- Server-side pagination (25 rows/page), sorting, and email search\n- Role guard: redirects non-admins server-side (no client-only check)\n- Parameterized SQL with sort-column whitelist — no injection surface\n- TanStack Table v8 client component; sort/page/search state lives in URL\n- `getUsers` returns `{ rows, total }` from a single query function\n```"
}