{
  "id": "add-an-admin-table",
  "type": "playbooks",
  "category": "playbooks",
  "locale": "fr",
  "url": "/fr/playbooks/add-an-admin-table",
  "title": "Prompt-to-PR : Ajouter un tableau de données administrateur",
  "description": "SOP pour ajouter un tableau de données administrateur paginé côté serveur, triable et recherchable dans Next.js en utilisant TanStack Table et une requête PostgreSQL — sans magie d'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": "Les tableaux d'administration sont une tâche de codage IA courante qui produit fréquemment des implémentations entièrement côté client et non sécurisées. Ce playbook maintient la pagination et le tri côté serveur et protège explicitement la route.\n\n## 1. Exigence\n\nAjoutez une page `/admin/users` accessible uniquement aux utilisateurs ayant `role = \"admin\"`. Le tableau est paginé côté serveur (25 lignes), triable par colonne et recherchable par email. Les données proviennent d'une table PostgreSQL `users` via une requête paramétrée — pas de SQL non paramétré.\n\n## 2. Premier 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. Modifications de fichiers attendues\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. Liste de vérification\n\n- La page vérifie `session.user.role === \"admin\"` côté serveur — pas de garde côté client uniquement.\n- `getUsers` liste blanche des colonnes de tri autorisées avant interpolation dans SQL — aucune entrée utilisateur brute dans les noms de colonnes.\n- Toutes les valeurs WHERE/LIMIT/OFFSET utilisent des espaces réservés paramétrés, pas de concaténation de chaînes.\n- La recherche (`q`) utilise `ILIKE '%' || $1 || '%'` avec un paramètre, pas d'interpolation de chaîne.\n- TanStack Table est utilisé uniquement dans le composant `\"use client\"` — pas d'import `@tanstack/react-table` dans les composants serveurs.\n- Les paramètres de recherche d'URL pilotent l'état de tri/page/recherche — le bouton Retour du navigateur fonctionne correctement.\n- Le compte `total` provient d'un `SELECT COUNT(*)` dans la même fonction de requête — pas d'analyse complète de la table séparée de la requête paginée.\n\n## 5. Commandes de test\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. Échecs courants\n\n- **Injection SQL via la colonne de tri** — l'agent interpole `sort` directement : `` `ORDER BY ${sort}` ``. Doit utiliser une recherche dans une liste blanche vers une chaîne de nom de colonne sécurisée.\n- **`searchParams` est asynchrone dans Next.js 15+** — doit être attendu avec `await` : `const { page } = await searchParams`.\n- **TanStack Table s'affiche dans un composant serveur** — erreur de build car il utilise `useState`. Déplacez-le dans un composant `\"use client\"`.\n- **Le compte `total` décalé d'une page** — `Math.ceil(total / 25)` donne un nombre de pages incorrect si `total` est 0. Protection : `Math.max(1, Math.ceil(total / 25))`.\n- **Vérification de rôle manquante** — l'agent vérifie seulement une session, pas le rôle admin. Tout utilisateur connecté peut accéder au tableau.\n\n## 7. Correctif prompt\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. Description de la 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```"
}