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.
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
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. getUserspermite 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) usaILIKE '%' || $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-tableen 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
totalproviene de unSELECT COUNT(*)en la misma función de consulta — no un escaneo completo de tabla separado de la consulta paginada.
5. Comandos de prueba
bun dev
# Hit the page as an unauthenticated usercurl -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 throughgrep -n "sort\|order\|column" src/lib/queries/users.ts
bun tsc --noEmitbun test6. Fallos comunes
- Inyección SQL a través de la columna de ordenamiento — el agente interpola
sortdirectamente:`ORDER BY ${sort}`. Debe usar una lista blanca para obtener un nombre de columna seguro. searchParamses asíncrono en Next.js 15+ — debe usarse conawait: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
totaldesviado por una página —Math.ceil(total / 25)da un número de página incorrecto sitotales 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
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 bevalidated: only accept "asc" or "desc", default to "desc".8. Descripción del PR
## 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