{
  "id": "add-an-admin-table",
  "type": "playbooks",
  "category": "playbooks",
  "locale": "de",
  "url": "/de/playbooks/add-an-admin-table",
  "title": "Prompt-to-PR: Admin-Datentabelle hinzufügen",
  "description": "SOP zum Hinzufügen einer serverseitig paginierten, sortierbaren, durchsuchbaren Admin-Datentabelle in Next.js mit TanStack Table und einer PostgreSQL-Abfrage — kein ORM-Zauber.",
  "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": "Admin-Tabellen sind eine häufige KI-Codierungsaufgabe, die oft unsichere, rein clientseitige Implementierungen hervorbringt. Dieses Playbook hält Paginierung und Sortierung serverseitig und schützt die Route explizit.\n\n## 1. Anforderung\n\nFüge eine `/admin/users`-Seite hinzu, die nur für Benutzer mit `role = \"admin\"` zugänglich ist. Die Tabelle ist serverseitig paginiert (25 Zeilen), nach Spalten sortierbar und nach E-Mail durchsuchbar. Die Daten stammen aus einer PostgreSQL-`users`-Tabelle über eine parametrisierte Abfrage — kein unparametrisiertes SQL.\n\n## 2. Erster 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. Erwartete Dateiänderungen\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. Review-Checkliste\n\n- Die Seite prüft serverseitig `session.user.role === \"admin\"` — kein reiner Client-Guard.\n- `getUsers` verwendet eine Whitelist zulässiger Sortierspalten, bevor sie in SQL eingefügt werden — keine rohen Benutzereingaben in Spaltennamen.\n- Alle WHERE/LIMIT/OFFSET-Werte verwenden parametrisierte Platzhalter, keine String-Verkettung.\n- Die Suche (`q`) verwendet `ILIKE '%' || $1 || '%'` mit einem Parameter, keine String-Interpolation.\n- TanStack Table wird nur in der `\"use client\"`-Komponente verwendet — kein `@tanstack/react-table`-Import in Server Components.\n- URL-Suchparameter steuern den Sortier-/Seiten-/Suchstatus — der Zurück-Button des Browsers funktioniert korrekt.\n- Der `total`-Wert stammt aus einem `SELECT COUNT(*)` in derselben Abfragefunktion — kein vollständiger Tabellen-Scan getrennt von der paginierten Abfrage.\n\n## 5. Test-Befehle\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. Häufige Fehler\n\n- **SQL-Injection über Sortierspalte** — der Agent interpoliert `sort` direkt: `` `ORDER BY ${sort}` ``. Es muss eine Whitelist-Lookup auf einen sicheren Spaltennamen-String verwendet werden.\n- **`searchParams` ist asynchron in Next.js 15+** — muss mit `await` aufgerufen werden: `const { page } = await searchParams`.\n- **TanStack Table rendert in einer Server-Komponente** — Build-Fehler, weil es `useState` verwendet. Verschiebe es in eine `\"use client\"`-Komponente.\n- **`total` ist um eine Seite verschoben** — `Math.ceil(total / 25)` ergibt eine falsche Seitenzahl, wenn `total` 0 ist. Absicherung: `Math.max(1, Math.ceil(total / 25))`.\n- **Rollenprüfung fehlt** — der Agent prüft nur auf eine Session, nicht auf die Admin-Rolle. Jeder eingeloggte Benutzer kann auf die Tabelle zugreifen.\n\n## 7. Fix-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. PR-Beschreibung\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```"
}