P PasteCode
Guía de trabajo

Prompt-to-PR: Agregar una tabla de datos de administración

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.

CursorClaude CodeCodexWindsurf Next.jsTypeScriptPostgreSQLTailwind
.md .json Dificultad: Medio Actualizado 8 jun 2026

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.

1. Requisito

Agregar 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.

2. Primer Prompt

First Prompt
Add an admin users table to this Next.js 15 App Router project.
Requirements:
1. Create `src/app/admin/users/page.tsx` — an async Server Component.
- Protect it: check the session (using the existing auth helper at
src/lib/auth.ts). If no session or session.user.role !== "admin",
call notFound() or redirect to /.
- Read `page` (default 1), `sort` (default "createdAt"), `order`
("asc"|"desc", default "desc"), and `q` (search string) from
`searchParams`.
- Fetch data by calling a function `getUsers({ page, sort, order, q })`
from `src/lib/queries/users.ts`.
- Pass results to `<UsersTable>` client component.
2. Create `src/lib/queries/users.ts` exporting `getUsers`.
- Use parameterized SQL via the existing db client (Drizzle or postgres.js,
whichever is in this project).
- Allowed sort columns whitelist: ["email","name","createdAt","role"].
Reject any other sort value by defaulting to "createdAt".
- Return `{ rows: User[], total: number }`.
- Page size: 25.
3. Create `src/components/admin/UsersTable.tsx` — a Client Component using
TanStack Table v8.
- Columns: email, name, role, createdAt (formatted), actions (Edit link).
- Column headers are clickable to sort — update URL search params via
useRouter/useSearchParams (do not manage sort state locally).
- Render a search input that debounces 300 ms before updating the URL.
- Render a pagination row: Previous / Next and "Page N of M".
4. Install `@tanstack/react-table` if not already present.
5. Do not implement the Edit action — leave it as a placeholder link.

3. Cambios de archivos esperados

package.json (@tanstack/react-table if missing)
src/app/admin/users/page.tsx (new — protected Server Component)
src/lib/queries/users.ts (new — parameterized query)
src/components/admin/UsersTable.tsx (new — TanStack Table client component)

4. Lista de verificación

  • La página verifica session.user.role === "admin" del lado del servidor — sin protección solo del lado del cliente.
  • getUsers permite solo columnas de ordenamiento autorizadas antes de interpolar en SQL — sin entrada de usuario sin procesar en nombres de columna.
  • Todos los valores WHERE/LIMIT/OFFSET usan marcadores parametrizados, no concatenación de cadenas.
  • La búsqueda (q) usa ILIKE '%' || $1 || '%' con un parámetro, no interpolación de cadenas.
  • TanStack Table se usa solo en el componente con "use client" — sin importar @tanstack/react-table en Server Components.
  • 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.
  • 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.

5. Comandos de prueba

Terminal window
bun dev
# Hit the page as an unauthenticated user
curl -I http://localhost:3000/admin/users
# Expect: 404 or redirect, not 200
# Log in as an admin user in the browser, visit /admin/users
# Confirm: 25 rows, sortable columns, search, pagination
# SQL audit — confirm no dynamic column names slip through
grep -n "sort\|order\|column" src/lib/queries/users.ts
bun tsc --noEmit
bun test

6. Fallos comunes

  • 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.
  • searchParams es asíncrono en Next.js 15+ — debe usarse con await: const { page } = await searchParams.
  • TanStack Table se renderiza en un Server Component — error de compilación porque usa useState. Muévalo a un componente "use client".
  • Conteo total desviado por una páginaMath.ceil(total / 25) da un número de página incorrecto si total es 0. Protección: Math.max(1, Math.ceil(total / 25)).
  • 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.

7. Prompt de corrección

Fix Prompt
The sort column in getUsers is interpolated directly into SQL:
`ORDER BY ${sort} ${order}`
This is a SQL injection risk.
Fix: create an allowlist object:
const ALLOWED_SORT: Record<string, string> = {
email: "users.email",
name: "users.name",
createdAt: "users.created_at",
role: "users.role",
};
const safeSort = ALLOWED_SORT[sort] ?? "users.created_at";
Then use `safeSort` in the query. The order direction must also be
validated: only accept "asc" or "desc", default to "desc".

8. Descripción del PR

PR description
## Feature: Admin users table at /admin/users
- Server-side pagination (25 rows/page), sorting, and email search
- Role guard: redirects non-admins server-side (no client-only check)
- Parameterized SQL with sort-column whitelist — no injection surface
- TanStack Table v8 client component; sort/page/search state lives in URL
- `getUsers` returns `{ rows, total }` from a single query function