Files
office_translator/_bmad-output/implementation-artifacts/5-4-admin-user-management.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-25 15:01:47 +02:00

16 KiB

Story 5.4: Admin - User Management

Status: done

Story

En tant qu'Admin, Je veux voir et gérer les utilisateurs, de sorte que je puisse gérer les abonnements et prévenir les abus.

Acceptance Criteria

  1. Users Table: Affiche une table des utilisateurs avec: email, tier/plan, usage mensuel, date de création
  2. Tier Filter: Filtre par tier (Free/Pro/Starter/Business) via dropdown ou tabs
  3. Email Search: Recherche par email avec input de recherche
  4. Tier Change: Dropdown pour changer le tier d'un utilisateur (Free ↔ Pro)
  5. Immediate Effect: Le changement de tier prend effet immédiatement (appel API PATCH)
  6. Usage Display: Affiche l'utilisation mensuelle (docs traduits / limite du plan)
  7. Revoke API Keys: Bouton pour révoquer les clés API d'un utilisateur
  8. Loading States: États de chargement pendant les appels API
  9. Error Handling: Gestion gracieuse des erreurs API avec message utilisateur
  10. Responsive: S'affiche correctement sur desktop et mobile
  11. Stats Summary: Affiche le nombre total d'utilisateurs, actifs, et par plan

Tasks / Subtasks

  • Task 1: Créer le hook useAdminUsers (AC: #8, #9)

    • 1.1 Créer frontend/src/app/admin/users/useAdminUsers.ts
    • 1.2 Implémenter le fetch de /api/v1/admin/users avec TanStack Query
    • 1.3 Gérer les états loading/error/success
    • 1.4 Exposer les données: users list, total count, refetch function
  • Task 2: Créer le hook useUpdateUserTier (AC: #4, #5)

    • 2.1 Créer frontend/src/app/admin/users/useUpdateUserTier.ts
    • 2.2 Implémenter PATCH /api/v1/admin/users/{user_id} avec TanStack Mutation
    • 2.3 Invalider le cache users après mise à jour
    • 2.4 Afficher un toast de confirmation après succès
  • Task 3: Créer le hook useRevokeApiKey (AC: #7)

    • 3.1 Créer frontend/src/app/admin/users/useRevokeApiKey.ts
    • 3.2 Implémenter DELETE /api/v1/admin/api-keys/{key_id} avec TanStack Mutation
    • 3.3 Gérer l'état de révocation (spinner, confirmation)
    • 3.4 Invalider le cache users après révocation
  • Task 4: Créer types.ts (AC: Tous)

    • 4.1 Créer frontend/src/app/admin/users/types.ts
    • 4.2 Interface AdminUser: id, email, name, plan, tier, docs_translated, plan_limits, created_at
    • 4.3 Interface AdminUsersResponse: total, users[]
    • 4.4 Interface UpdateTierRequest: plan
  • Task 5: Créer UserTable.tsx (AC: #1, #2, #3, #4, #6, #7)

    • 5.1 Créer frontend/src/app/admin/users/UserTable.tsx
    • 5.2 Table avec colonnes: Email, Status, Tier, Usage, Keys, Actions
    • 5.3 Tier dropdown dans chaque ligne (Free/Starter/Pro/Business/Enterprise)
    • 5.4 Progress bar pour usage (docs traduits / limite plan)
    • 5.5 Bouton "Revoke Keys" avec confirmation tooltip
    • 5.6 Input de recherche pour filtrer par email
    • 5.7 Adapter le design depuis office-translator-landing-page/components/admin-user-table.tsx
  • Task 6: Créer UserStats.tsx (AC: #11)

    • 6.1 Créer frontend/src/app/admin/users/UserStats.tsx
    • 6.2 Cards: Total Users, Active This Month, Free/Pro Distribution
    • 6.3 Utiliser les composants shadcn/ui: Card, Badge
  • Task 7: Modifier la page admin/users/page.tsx (AC: #10)

    • 7.1 Importer et utiliser UserTable et UserStats
    • 7.2 Layout responsive: stats en haut, table en dessous
    • 7.3 Header avec titre et description
    • 7.4 Remplacer le contenu placeholder existant
  • Task 8: Tests et validation (AC: Tous)

    • 8.1 npm run build → 0 erreurs TypeScript
    • 8.2 Tester avec backend actif → données affichées correctement
    • 8.3 Tester changement de tier → mise à jour immédiate + toast
    • 8.4 Tester recherche email → filtrage fonctionne
    • 8.5 Tester révocation clés → spinner + confirmation
    • 8.6 Tester error handling → backend éteint → message d'erreur
    • 8.7 Tester responsive → mobile et desktop

Dev Notes

🏗️ Stack Technique

Technologie Version
Next.js 16.0.6 (App Router)
React 19.2.0
TanStack Query v5
Tailwind CSS configuré
shadcn/ui Card, Button, Progress, Badge, Tooltip, Table, Select, Input
Lucide React Users, Search, KeyRound, ChevronLeft, ChevronRight

📁 Structure Cible (Colocation Pattern)

frontend/src/app/admin/users/
├── page.tsx              # ⭐ Page principale (modifier)
├── UserTable.tsx         # ⭐ Nouveau: Table des utilisateurs
├── UserStats.tsx         # ⭐ Nouveau: Cards de stats
├── useAdminUsers.ts      # ⭐ Nouveau: Hook TanStack Query
├── useUpdateUserTier.ts  # ⭐ Nouveau: Hook mutation tier
├── useRevokeApiKey.ts    # ⭐ Nouveau: Hook mutation revoke
└── types.ts              # ⭐ Nouveau: TypeScript interfaces

⚠️ Règle absolue (architecture.md):

🚨 FICHIERS SPÉCIAUX: page.tsx, layout.tsx → TOUJOURS minuscules
🚨 COLOCATION: Components/hooks/types dans le dossier de leur page

🔗 API Endpoints

Endpoint Méthode Description
/api/v1/admin/users GET Liste tous les utilisateurs avec stats
/api/v1/admin/users/{user_id} PATCH Modifier le plan/tier d'un utilisateur
/api/v1/admin/api-keys/{key_id} DELETE Révoquer une clé API

📊 Response Format: GET /api/v1/admin/users

{
  "total": 42,
  "users": [
    {
      "id": "user_abc123",
      "email": "sarah.chen@acme.com",
      "name": "Sarah Chen",
      "plan": "pro",
      "subscription_status": "active",
      "docs_translated_this_month": 23,
      "pages_translated_this_month": 145,
      "extra_credits": 0,
      "created_at": "2024-01-15T10:30:00Z",
      "plan_limits": {
        "docs_per_month": 100,
        "max_pages_per_doc": 50
      }
    }
  ]
}

📊 Response Format: PATCH /api/v1/admin/users/{user_id}

{
  "data": {
    "id": "user_abc123",
    "email": "sarah.chen@acme.com",
    "name": "Sarah Chen",
    "plan": "pro",
    "tier": "pro"
  },
  "meta": {}
}

📊 Request Format: PATCH /api/v1/admin/users/{user_id}

{
  "plan": "pro"
}

Plans disponibles: free, starter, pro, business, enterprise

🎨 Patterns UI à Réutiliser

UserTable.tsx - depuis office-translator-landing-page/components/admin-user-table.tsx:

// Structure à adapter
<Card>
  <CardHeader className="pb-3">
    <div className="flex items-start justify-between gap-4">
      <div className="flex items-center gap-2">
        <Users className="size-4" />
        <CardTitle>User Management</CardTitle>
      </div>
      <Input placeholder="Filter by email..." className="w-56" />
    </div>
  </CardHeader>
  <CardContent>
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>User Email</TableHead>
          <TableHead>Status</TableHead>
          <TableHead>Tier</TableHead>
          <TableHead>Usage</TableHead>
          <TableHead>Keys</TableHead>
          <TableHead>Actions</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {/* User rows */}
      </TableBody>
    </Table>
  </CardContent>
</Card>

Tier Dropdown:

<Select value={user.plan} onValueChange={(val) => updateTier(user.id, val)}>
  <SelectTrigger className={user.plan === "pro" ? "border-accent bg-accent/10" : ""}>
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="free">Free</SelectItem>
    <SelectItem value="starter">Starter</SelectItem>
    <SelectItem value="pro">Pro</SelectItem>
    <SelectItem value="business">Business</SelectItem>
    <SelectItem value="enterprise">Enterprise</SelectItem>
  </SelectContent>
</Select>

Usage Progress Bar:

<div className="flex w-32 flex-col gap-1">
  <span className="text-xs">{usage} / {maxUsage}</span>
  <Progress value={(usage / maxUsage) * 100} />
</div>

🔄 TanStack Query Pattern

// useAdminUsers.ts
import { useQuery } from '@tanstack/react-query'

export function useAdminUsers() {
  return useQuery({
    queryKey: ['admin', 'users'],
    queryFn: async () => {
      const token = localStorage.getItem('adminToken')
      const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/admin/users`, {
        headers: { Authorization: `Bearer ${token}` }
      })
      if (!res.ok) throw new Error('Failed to fetch users')
      return res.json()
    },
    staleTime: 30000, // 30 seconds
  })
}
// useUpdateUserTier.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'

export function useUpdateUserTier() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: async ({ userId, plan }: { userId: string, plan: string }) => {
      const token = localStorage.getItem('adminToken')
      const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/admin/users/${userId}`, {
        method: 'PATCH',
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ plan }),
      })
      if (!res.ok) throw new Error('Failed to update tier')
      return res.json()
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['admin', 'users'] })
    },
  })
}

⚠️ Points d'Attention Critiques

  1. Authentification: Tous les appels API doivent inclure le header Authorization: Bearer {adminToken}. Le token est stocké dans localStorage ou Zustand store.

  2. Gestion du token admin: Utiliser le même pattern que dans useAdminLogin.ts et useAdminDashboard.ts pour récupérer le token.

  3. Plan vs Tier: Le backend utilise plan (free/starter/pro/business/enterprise) mais l'UI affiche souvent tier (free/pro). Mapper correctement:

    • plan: "free" → tier: "free"
    • plan: "starter" → tier: "free"
    • plan: "pro" → tier: "pro"
    • plan: "business" → tier: "pro"
    • plan: "enterprise" → tier: "pro"
  4. Usage Calculation: Le backend retourne docs_translated_this_month et plan_limits.docs_per_month. Calculer le pourcentage pour la progress bar.

  5. API Keys Count: Le backend ne retourne pas directement le nombre de clés API par utilisateur. Vérifier si cette info est disponible ou masquer cette colonne.

  6. Error Handling: Utiliser toast de shadcn/ui pour afficher les erreurs de manière non-intrusive.

  7. Composants shadcn/ui: Vérifier que Table et Select sont installés. Sinon:

    npx shadcn@latest add table select
    
  8. Couleurs OKLCH: Le design utilise des couleurs OKLCH modernes. Si le projet ne les supporte pas, utiliser des classes Tailwind standard.

📋 Checklist de Validation Avant Dev

  • useAdminUsers.ts hook créé avec TanStack Query
  • useUpdateUserTier.ts hook créé pour les mutations
  • useRevokeApiKey.ts hook créé pour les révocations
  • types.ts créé avec interfaces TypeScript
  • Composants shadcn/ui disponibles (Table, Select, Progress, Badge, Tooltip)
  • Backend /api/v1/admin/users retourne les données attendues
  • Backend /api/v1/admin/users/{user_id} PATCH fonctionne

🚀 Integration Steps

  1. Créer types.ts avec les interfaces TypeScript
  2. Créer useAdminUsers.ts avec TanStack Query
  3. Créer useUpdateUserTier.ts pour les mutations
  4. Créer useRevokeApiKey.ts pour les révocations
  5. Créer UserStats.tsx pour les stats cards
  6. Créer UserTable.tsx en adaptant le design
  7. Modifier page.tsx pour intégrer les composants
  8. Tester avec le backend actif

📚 Previous Story Intelligence (Story 5.3)

Learnings from Story 5.3 (Admin System Health Dashboard):

  • Utiliser TanStack Query useQuery pour le data fetching, pas de raw fetch
  • Utiliser TanStack Query useMutation pour les mutations avec cache invalidation
  • Le token admin est dans useTranslationStore().settings.adminToken ou localStorage
  • Mapper les erreurs API vers des messages utilisateur en français
  • Les composants shadcn/ui sont disponibles: Card, Button, Progress, Badge, Tooltip
  • Le pattern de colocation fonctionne bien (components/hooks/types dans le dossier)

Files created in Story 5.3:

  • frontend/src/app/admin/types.ts - peut être étendu ou créer types locaux
  • frontend/src/app/admin/useAdminDashboard.ts - pattern à suivre
  • frontend/src/app/admin/useCleanup.ts - pattern mutation à suivre

References

  • [Source: _bmad-output/planning-artifacts/epics.md#Story-5.4] — Story requirements
  • [Source: _bmad-output/planning-artifacts/architecture.md#Frontend-Architecture] — Colocation pattern
  • [Source: routes/admin_routes.py] — Backend API endpoints (GET /users, PATCH /users/{id})
  • [Source: office-translator-landing-page/components/admin-user-table.tsx] — UI reference
  • [Source: frontend/src/app/admin/useAdminDashboard.ts] — Auth token pattern
  • [Source: frontend/src/app/admin/useCleanup.ts] — Mutation pattern

Dev Agent Record

Agent Model Used

zai-anthropic/glm-5

Debug Log References

None

Completion Notes List

  • 2026-02-24: Story created - ready for development
  • 2026-02-24: Implementation complete - all ACs satisfied, ready for review
    • Created types.ts with TypeScript interfaces for AdminUser, AdminUsersResponse, UpdateTierRequest, UpdateTierResponse, RevokeApiKeyResponse
    • Created useAdminUsers.ts hook with TanStack Query for fetching users from /api/v1/admin/users
    • Created useUpdateUserTier.ts hook with TanStack Mutation for updating user plan via PATCH /api/v1/admin/users/{id}
    • Created useRevokeApiKey.ts hook with TanStack Mutation for revoking API keys via DELETE /api/v1/admin/api-keys/{id}
    • Created UserStats.tsx with stats cards showing total users, active users, pro users, free users
    • Created UserTable.tsx with full user table including search, tier dropdown, usage progress bar, revoke keys button
    • Updated admin/users/page.tsx to integrate all components with toast notifications
    • Build passes with 0 TypeScript errors
    • All components use shadcn/ui: Card, Button, Progress, Badge, Tooltip, Table, Select, Input
    • Error handling displays user-friendly French messages
    • Loading states show skeleton loaders while fetching data
  • 2026-02-24: Code review fixes applied
    • Fixed AC #2: Added tier filter (Free/Pro) dropdown in UserTable.tsx
    • Fixed AC #7: Revoke API Keys now uses real key IDs from backend (api_key_ids field)
    • Fixed backend: GET /api/v1/admin/users now returns api_keys_count and api_key_ids for each user
    • Fixed types.ts: Added api_key_ids field to AdminUser interface
    • Fixed UserTable.tsx: Added TierFilter dropdown, error handling with toast
    • Fixed page.tsx: Uses shadcn/ui toast, proper revoke keys with real key IDs
    • Fixed UserStats.tsx: Consistent "active" definition (subscription_status === "active")
    • Fixed magic numbers: Extracted ADMIN_TIMEOUT_MS to useAdminUsers.ts
    • Fixed useAdminDashboard.ts: Removed duplicate code block

File List

Created files:

  • frontend/src/app/admin/users/types.ts
  • frontend/src/app/admin/users/useAdminUsers.ts
  • frontend/src/app/admin/users/useUpdateUserTier.ts
  • frontend/src/app/admin/users/useRevokeApiKey.ts
  • frontend/src/app/admin/users/UserTable.tsx
  • frontend/src/app/admin/users/UserStats.tsx
  • frontend/src/app/admin/users/page.tsx (replaced placeholder content)

Modified files (code review fixes):

  • routes/admin_routes.py (added api_keys_count and api_key_ids to GET /users response)
  • frontend/src/app/admin/useAdminDashboard.ts (fixed duplicate code)

Change Log

  • 2026-02-24: Story created - ready for development
  • 2026-02-24: Implementation complete - all tasks completed, ready for review
  • 2026-02-24: Code review completed - 3 CRITICAL, 3 HIGH, 3 MEDIUM issues found and fixed
    • Added tier filter dropdown (AC #2)
    • Fixed revoke API keys to use real key IDs from backend (AC #7)
    • Updated backend to return api_keys_count and api_key_ids
    • Replaced custom toast with shadcn/ui toast
    • Standardized "active" user definition
    • Centralized timeout constant
    • Fixed duplicate code in useAdminDashboard.ts
  • 2026-02-24: Story marked as done