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>
This commit is contained in:
Sepehr Ramezani
2026-04-25 15:01:47 +02:00
parent 2ba4fedfc8
commit 26bd096a06
1178 changed files with 136435 additions and 3047 deletions

View File

@@ -1,102 +1,79 @@
'use client';
import Link from 'next/link';
import { FileText, Key, BookText, ChevronRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Loader2 } from 'lucide-react';
import { useUser } from './useUser';
import { API_BASE } from '@/lib/config';
/**
* /dashboard — point d'entrée après connexion.
*
* Unique rôle : synchroniser le paiement Stripe si `session_id` est présent
* dans l'URL (retour depuis Stripe Checkout), puis rediriger vers /dashboard/translate.
*/
export default function DashboardPage() {
const { data: user, isLoading } = useUser();
const router = useRouter();
const searchParams = useSearchParams();
const checkoutSessionId = searchParams.get('session_id');
const [syncError, setSyncError] = useState<string | null>(null);
const { refetch } = useUser();
if (isLoading) {
useEffect(() => {
if (!checkoutSessionId) {
router.replace('/dashboard/translate');
return;
}
const token = localStorage.getItem('token');
if (!token) {
router.replace('/dashboard/translate');
return;
}
let cancelled = false;
const runSync = async () => {
setSyncError(null);
try {
const res = await fetch(
`${API_BASE}/api/v1/auth/checkout/sync?session_id=${encodeURIComponent(checkoutSessionId)}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
if (!cancelled) {
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
setSyncError(errData.message || 'Erreur lors de la synchronisation du paiement.');
} else {
await refetch();
router.replace('/dashboard/translate');
}
}
} catch {
if (!cancelled) setSyncError('Erreur réseau. Veuillez rafraîchir la page.');
}
};
runSync();
return () => { cancelled = true; };
}, [checkoutSessionId, refetch, router]);
if (syncError) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-4 border-muted border-t-foreground"></div>
<div className="flex flex-col items-center justify-center gap-4 py-20">
<p className="text-sm text-destructive">{syncError}</p>
<button
onClick={() => router.replace('/dashboard/translate')}
className="text-xs text-muted-foreground underline"
>
Continuer vers la traduction
</button>
</div>
);
}
const firstName = user?.name?.split(' ')[0] || 'User';
return (
<div className="mx-auto max-w-5xl px-4 py-6 lg:px-8 lg:py-8">
{/* Page heading */}
<div className="mb-6">
<h2 className="text-xl font-semibold tracking-tight text-foreground">
Welcome back, {firstName}!
</h2>
<p className="mt-1 text-sm text-muted-foreground">
Monitor your usage, manage API keys, and configure translation preferences.
</p>
</div>
{/* Quick actions */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Link href="/dashboard/translate">
<Card className="cursor-pointer transition-colors hover:bg-secondary/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Translate Document</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<CardDescription>
Upload and translate Excel, Word, or PowerPoint files
</CardDescription>
</CardContent>
</Card>
</Link>
<Link href="/dashboard/api-keys">
<Card className="cursor-pointer transition-colors hover:bg-secondary/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">API Keys</CardTitle>
<Key className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<CardDescription>
Manage your API keys for automation
</CardDescription>
</CardContent>
</Card>
</Link>
{user?.tier === 'pro' && (
<Link href="/dashboard/glossaries">
<Card className="cursor-pointer transition-colors hover:bg-secondary/50">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Glossaries</CardTitle>
<BookText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<CardDescription>
Create custom terminology for translations
</CardDescription>
</CardContent>
</Card>
</Link>
)}
</div>
{/* Plan info */}
{user?.tier === 'free' && (
<Card className="mt-6 border-primary/50 bg-primary/5">
<CardContent className="flex items-center justify-between pt-6">
<div>
<p className="text-sm font-medium text-foreground">Upgrade to Pro</p>
<p className="text-xs text-muted-foreground">
Get unlimited translations, API access, and custom glossaries
</p>
</div>
<Link href="/pricing">
<Button variant="premium" size="sm" className="gap-1">
View Plans
<ChevronRight className="h-4 w-4" />
</Button>
</Link>
</CardContent>
</Card>
)}
<div className="flex items-center justify-center py-20">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
);
}