{
  "id": "add-an-admin-table",
  "type": "playbooks",
  "category": "playbooks",
  "locale": "pt",
  "url": "/pt/playbooks/add-an-admin-table",
  "title": "Prompt-to-PR: Adicionar Tabela de Dados Administrativa",
  "description": "POP para adicionar uma tabela de dados administrativa paginada no servidor, ordenável e pesquisável em Next.js usando TanStack Table e consulta PostgreSQL — sem mágica 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": "Tabelas administrativas são uma tarefa comum de codificação com IA que frequentemente produz implementações inseguras e totalmente no lado do cliente. Este guia mantém a paginação e a ordenação no servidor e protege explicitamente a rota.\n\n## 1. Requisito\n\nAdicionar uma página `/admin/users` acessível apenas a usuários com `role = \"admin\"`. A tabela é paginada no servidor (25 linhas), ordenável por coluna e pesquisável por email. Os dados vêm de uma tabela `users` do PostgreSQL via consulta parametrizada — sem SQL não parametrizado.\n\n## 2. Primeiro 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. Mudanças Esperadas nos Arquivos\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 Verificação\n\n- A página verifica `session.user.role === \"admin\"` no servidor — sem proteção apenas no cliente.\n- `getUsers` usa lista branca de colunas de ordenação permitidas antes de interpolá-las no SQL — sem entrada bruta do usuário em nomes de colunas.\n- Todos os valores WHERE/LIMIT/OFFSET usam placeholders parametrizados, sem concatenação de strings.\n- Pesquisa (`q`) usa `ILIKE '%' || $1 || '%'` com um parâmetro, sem interpolação de strings.\n- TanStack Table é usado apenas em componente `\"use client\"` — sem importação de `@tanstack/react-table` em Componentes Servidor.\n- Parâmetros de busca da URL controlam ordenação/página/pesquisa — o botão Voltar do navegador funciona corretamente.\n- O total `total` vem de um `SELECT COUNT(*)` na mesma função de consulta — não de uma varredura completa da tabela separada da consulta paginada.\n\n## 5. Comandos de Teste\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. Falhas Comuns\n\n- **Injeção SQL via coluna de ordenação** — o agente interpola `sort` diretamente: `` `ORDER BY ${sort}` ``. Deve usar uma lista branca para obter um nome de coluna seguro.\n- **`searchParams` é assíncrono no Next.js 15+** — deve ser usado com `await`: `const { page } = await searchParams`.\n- **TanStack Table renderizado em um Componente Servidor** — erro de build porque usa `useState`. Mova para um componente `\"use client\"`.\n- **Contagem `total` com erro de página** — `Math.ceil(total / 25)` dá número errado de páginas se `total` for 0. Proteja: `Math.max(1, Math.ceil(total / 25))`.\n- **Verificação de role ausente** — o agente verifica apenas a sessão, não a role de administrador. Qualquer usuário logado pode acessar a tabela.\n\n## 7. Prompt de Correção\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. Descrição do 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```"
}