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>
414 lines
16 KiB
Markdown
414 lines
16 KiB
Markdown
# 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
|
|
|
|
- [x] **Task 1: Créer le hook useAdminUsers** (AC: #8, #9)
|
|
- [x] 1.1 Créer `frontend/src/app/admin/users/useAdminUsers.ts`
|
|
- [x] 1.2 Implémenter le fetch de `/api/v1/admin/users` avec TanStack Query
|
|
- [x] 1.3 Gérer les états loading/error/success
|
|
- [x] 1.4 Exposer les données: users list, total count, refetch function
|
|
|
|
- [x] **Task 2: Créer le hook useUpdateUserTier** (AC: #4, #5)
|
|
- [x] 2.1 Créer `frontend/src/app/admin/users/useUpdateUserTier.ts`
|
|
- [x] 2.2 Implémenter PATCH `/api/v1/admin/users/{user_id}` avec TanStack Mutation
|
|
- [x] 2.3 Invalider le cache users après mise à jour
|
|
- [x] 2.4 Afficher un toast de confirmation après succès
|
|
|
|
- [x] **Task 3: Créer le hook useRevokeApiKey** (AC: #7)
|
|
- [x] 3.1 Créer `frontend/src/app/admin/users/useRevokeApiKey.ts`
|
|
- [x] 3.2 Implémenter DELETE `/api/v1/admin/api-keys/{key_id}` avec TanStack Mutation
|
|
- [x] 3.3 Gérer l'état de révocation (spinner, confirmation)
|
|
- [x] 3.4 Invalider le cache users après révocation
|
|
|
|
- [x] **Task 4: Créer types.ts** (AC: Tous)
|
|
- [x] 4.1 Créer `frontend/src/app/admin/users/types.ts`
|
|
- [x] 4.2 Interface AdminUser: id, email, name, plan, tier, docs_translated, plan_limits, created_at
|
|
- [x] 4.3 Interface AdminUsersResponse: total, users[]
|
|
- [x] 4.4 Interface UpdateTierRequest: plan
|
|
|
|
- [x] **Task 5: Créer UserTable.tsx** (AC: #1, #2, #3, #4, #6, #7)
|
|
- [x] 5.1 Créer `frontend/src/app/admin/users/UserTable.tsx`
|
|
- [x] 5.2 Table avec colonnes: Email, Status, Tier, Usage, Keys, Actions
|
|
- [x] 5.3 Tier dropdown dans chaque ligne (Free/Starter/Pro/Business/Enterprise)
|
|
- [x] 5.4 Progress bar pour usage (docs traduits / limite plan)
|
|
- [x] 5.5 Bouton "Revoke Keys" avec confirmation tooltip
|
|
- [x] 5.6 Input de recherche pour filtrer par email
|
|
- [x] 5.7 Adapter le design depuis `office-translator-landing-page/components/admin-user-table.tsx`
|
|
|
|
- [x] **Task 6: Créer UserStats.tsx** (AC: #11)
|
|
- [x] 6.1 Créer `frontend/src/app/admin/users/UserStats.tsx`
|
|
- [x] 6.2 Cards: Total Users, Active This Month, Free/Pro Distribution
|
|
- [x] 6.3 Utiliser les composants shadcn/ui: Card, Badge
|
|
|
|
- [x] **Task 7: Modifier la page admin/users/page.tsx** (AC: #10)
|
|
- [x] 7.1 Importer et utiliser UserTable et UserStats
|
|
- [x] 7.2 Layout responsive: stats en haut, table en dessous
|
|
- [x] 7.3 Header avec titre et description
|
|
- [x] 7.4 Remplacer le contenu placeholder existant
|
|
|
|
- [x] **Task 8: Tests et validation** (AC: Tous)
|
|
- [x] 8.1 `npm run build` → 0 erreurs TypeScript
|
|
- [x] 8.2 Tester avec backend actif → données affichées correctement
|
|
- [x] 8.3 Tester changement de tier → mise à jour immédiate + toast
|
|
- [x] 8.4 Tester recherche email → filtrage fonctionne
|
|
- [x] 8.5 Tester révocation clés → spinner + confirmation
|
|
- [x] 8.6 Tester error handling → backend éteint → message d'erreur
|
|
- [x] 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
|
|
|
|
```json
|
|
{
|
|
"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}
|
|
|
|
```json
|
|
{
|
|
"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}
|
|
|
|
```json
|
|
{
|
|
"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`:
|
|
```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:**
|
|
```tsx
|
|
<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:**
|
|
```tsx
|
|
<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
|
|
|
|
```tsx
|
|
// 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
|
|
})
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
// 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:
|
|
```bash
|
|
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
|
|
|
|
- [x] `useAdminUsers.ts` hook créé avec TanStack Query
|
|
- [x] `useUpdateUserTier.ts` hook créé pour les mutations
|
|
- [x] `useRevokeApiKey.ts` hook créé pour les révocations
|
|
- [x] `types.ts` créé avec interfaces TypeScript
|
|
- [x] Composants shadcn/ui disponibles (Table, Select, Progress, Badge, Tooltip)
|
|
- [x] Backend `/api/v1/admin/users` retourne les données attendues
|
|
- [x] 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
|