fix(admin): secure routes, add real IP detection, SMTP header validation, and fix Next.js layout hydration mismatch
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m5s

This commit is contained in:
2026-06-01 23:16:03 +02:00
parent 6d27dc4cda
commit 6da8a85b1d
10 changed files with 1165 additions and 96 deletions

View File

@@ -25,14 +25,14 @@ import { useTranslationStore } from '@/lib/store';
import { API_BASE } from '@/lib/config';
const PRESETS = [
{ key: 'it', title: 'IT / Logiciel', desc: 'Développement, infrastructure, DevOps', icon: Monitor, templateId: 'technology' },
{ key: 'legal', title: 'Juridique / Contrats', desc: 'Droit des affaires, contentieux', icon: Scale, templateId: 'legal' },
{ key: 'medical', title: 'Médical / Santé', desc: 'Pharmacologie, chirurgie, diagnostic', icon: Stethoscope, templateId: 'medical' },
{ key: 'finance', title: 'Finance / Comptabilité', desc: 'IFRS, bilans, fiscalité', icon: BarChart3, templateId: 'finance' },
{ key: 'marketing', title: 'Marketing / Publicité', desc: 'Digital, branding, analytics', icon: Megaphone, templateId: 'marketing' },
{ key: 'hr', title: 'RH / Ressources Humaines', desc: 'Contrats, politiques, recrutement', icon: Users, templateId: 'hr' },
{ key: 'scientific', title: 'Scientifique / Recherche', desc: 'Publications, thèses, articles', icon: FlaskConical, templateId: 'scientific' },
{ key: 'ecommerce', title: 'E-commerce / Vente', desc: 'Boutiques en ligne, catalogues, CRM', icon: ShoppingCart, templateId: 'ecommerce' },
{ key: 'it', titleKey: 'glossaries.presets.it.title', descKey: 'glossaries.presets.it.desc', icon: Monitor, templateId: 'technology' },
{ key: 'legal', titleKey: 'glossaries.presets.legal.title', descKey: 'glossaries.presets.legal.desc', icon: Scale, templateId: 'legal' },
{ key: 'medical', titleKey: 'glossaries.presets.medical.title', descKey: 'glossaries.presets.medical.desc', icon: Stethoscope, templateId: 'medical' },
{ key: 'finance', titleKey: 'glossaries.presets.finance.title', descKey: 'glossaries.presets.finance.desc', icon: BarChart3, templateId: 'finance' },
{ key: 'marketing', titleKey: 'glossaries.presets.marketing.title', descKey: 'glossaries.presets.marketing.desc', icon: Megaphone, templateId: 'marketing' },
{ key: 'hr', titleKey: 'glossaries.presets.hr.title', descKey: 'glossaries.presets.hr.desc', icon: Users, templateId: 'hr' },
{ key: 'scientific', titleKey: 'glossaries.presets.scientific.title', descKey: 'glossaries.presets.scientific.desc', icon: FlaskConical, templateId: 'scientific' },
{ key: 'ecommerce', titleKey: 'glossaries.presets.ecommerce.title', descKey: 'glossaries.presets.ecommerce.desc', icon: ShoppingCart, templateId: 'ecommerce' },
];
export default function GlossariesPage() {
@@ -120,7 +120,7 @@ export default function GlossariesPage() {
toast({
title: t('context.presets.created'),
description: t('context.presets.createdDesc', {
name: glossary?.name ?? preset.title,
name: glossary?.name ?? t(preset.titleKey),
count: String(glossary?.terms?.length ?? 0),
}),
});
@@ -228,7 +228,7 @@ export default function GlossariesPage() {
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-8 w-8 border-4 border-brand-muted border-t-brand-accent mx-auto"></div>
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-light">Chargement...</p>
<p className="text-xs text-brand-dark/40 dark:text-white/40 font-light">{t('glossaries.loading')}</p>
</div>
</div>
);
@@ -280,16 +280,16 @@ export default function GlossariesPage() {
<div className="mb-10 p-5 rounded-2xl bg-brand-accent/5 dark:bg-brand-accent/10 border border-brand-accent/20 dark:border-brand-accent/15">
<div className="flex items-center gap-2 mb-4">
<Info size={15} className="text-brand-accent shrink-0" />
<span className="text-xs font-bold uppercase tracking-widest text-brand-accent">Comment ces paramètres sont utilisés</span>
<span className="text-xs font-bold uppercase tracking-widest text-brand-accent">{t('glossaries.howItWorks.title')}</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{/* Step 1 */}
<div className="flex items-start gap-3">
<div className="w-7 h-7 rounded-full bg-brand-accent text-white flex items-center justify-center text-[11px] font-black shrink-0 mt-0.5">1</div>
<div>
<p className="text-xs font-bold text-brand-dark dark:text-white mb-1">Configurez ici</p>
<p className="text-xs font-bold text-brand-dark dark:text-white mb-1">{t('glossaries.howItWorks.step1Title')}</p>
<p className="text-[11px] text-brand-dark/55 dark:text-white/50 font-light leading-relaxed">
Rédigez vos instructions de contexte ou créez/importez un glossaire de termes.
{t('glossaries.howItWorks.step1Desc')}
</p>
</div>
</div>
@@ -301,22 +301,22 @@ export default function GlossariesPage() {
<div className="flex items-start gap-3">
<div className="w-7 h-7 rounded-full bg-brand-accent text-white flex items-center justify-center text-[11px] font-black shrink-0 mt-0.5">2</div>
<div>
<p className="text-xs font-bold text-brand-dark dark:text-white mb-1">Activez dans Traduire</p>
<p className="text-xs font-bold text-brand-dark dark:text-white mb-1">{t('glossaries.howItWorks.step2Title')}</p>
<p className="text-[11px] text-brand-dark/55 dark:text-white/50 font-light leading-relaxed">
Sur la page de traduction, dans la colonne de droite, sélectionnez votre glossaire.
{t('glossaries.howItWorks.step2Desc')}
</p>
</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-brand-accent/10 flex items-center justify-between">
<p className="text-[11px] text-brand-dark/45 dark:text-white/40 font-light">
Les instructions de contexte s'appliquent <strong>automatiquement</strong> à toutes vos traductions IA une fois enregistrées. Les glossaires doivent être <strong>sélectionnés manuellement</strong> sur la page Traduire.
{t('glossaries.howItWorks.warning')}
</p>
<Link
href="/dashboard/translate"
className="ml-4 shrink-0 flex items-center gap-1.5 text-[11px] font-bold text-brand-accent hover:underline"
>
Aller à Traduire <ExternalLink size={11} />
{t('glossaries.howItWorks.goToTranslate')} <ExternalLink size={11} />
</Link>
</div>
</div>
@@ -337,16 +337,16 @@ export default function GlossariesPage() {
{promptHasUnsavedChanges ? (
<span className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-amber-600 dark:text-amber-400 bg-amber-500/10 px-3 py-1 rounded-full border border-amber-500/20">
<AlertCircle size={11} />
Non enregistré
{t('glossaries.status.unsaved')}
</span>
) : promptIsActive ? (
<span className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 px-3 py-1 rounded-full border border-emerald-500/20">
<CheckCircle2 size={11} />
Actif · s'applique à toutes les traductions IA
{t('glossaries.status.active')}
</span>
) : (
<span className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-brand-dark/30 dark:text-white/30 bg-brand-muted dark:bg-white/5 px-3 py-1 rounded-full border border-black/5 dark:border-white/5">
Inactif
{t('glossaries.status.inactive')}
</span>
)}
</div>
@@ -354,10 +354,10 @@ export default function GlossariesPage() {
{/* Explanation box */}
<div className="mb-5 p-3.5 rounded-xl bg-brand-muted/40 dark:bg-white/[0.03] border border-black/5 dark:border-white/5">
<p className="text-[11px] text-brand-dark/60 dark:text-white/50 font-light leading-relaxed">
<strong className="font-bold text-brand-dark/80 dark:text-white/80">À quoi ça sert ?</strong> Ces instructions sont envoyées automatiquement à l'IA avant chaque traduction, sans que vous ayez besoin de faire quoi que ce soit sur la page Traduire. Utilisez-les pour guider le style, le registre ou la terminologie générale.
<strong className="font-bold text-brand-dark/80 dark:text-white/80">{t('glossaries.instructions.whatForBold')}</strong> {t('glossaries.instructions.whatForDesc')}
</p>
<p className="text-[11px] text-brand-accent/80 dark:text-brand-accent/70 font-medium mt-2 italic">
Exemple : «&nbsp;Vous traduisez des rapports financiers. Soyez formel, précis et conservez tous les chiffres.&nbsp;»
{t('glossaries.instructions.example')}
</p>
</div>
@@ -369,14 +369,14 @@ export default function GlossariesPage() {
/>
<div className="flex justify-between items-center mt-4">
<p className="text-[10px] text-brand-dark/30 dark:text-white/25 font-light">
{systemPrompt.length > 0 ? `${systemPrompt.length} caractères` : 'Vide aucune instruction envoyée à l\'IA'}
{systemPrompt.length > 0 ? t('glossaries.instructions.charCount', { count: systemPrompt.length }) : t('glossaries.instructions.emptyHint')}
</p>
<div className="flex gap-3">
<button
onClick={handleClearPrompt}
className="px-5 py-2.5 bg-brand-muted dark:bg-white/5 text-brand-dark/50 dark:text-white/40 rounded-lg text-xs font-bold uppercase tracking-wider hover:text-brand-dark dark:hover:text-white transition-all cursor-pointer"
>
<Trash2 size={12} className="inline mr-1.5" />Tout effacer
<Trash2 size={12} className="inline mr-1.5" />{t('glossaries.instructions.clearAll')}
</button>
<button
onClick={handleSavePrompt}
@@ -384,9 +384,9 @@ export default function GlossariesPage() {
className="premium-button px-8 py-2.5 text-xs uppercase tracking-widest !rounded-lg flex items-center gap-2 disabled:opacity-50 cursor-pointer font-bold"
>
{isSavingPrompt
? <><Loader2 size={14} className="animate-spin" /> Enregistrement</>
? <><Loader2 size={14} className="animate-spin" /> {t('glossaries.instructions.saving')}</>
: promptSaved
? <><CheckCircle2 size={14} /> Enregistré</>
? <><CheckCircle2 size={14} /> {t('glossaries.instructions.saved')}</>
: <><Save size={14} /> {t('context.save')}</>
}
</button>
@@ -406,12 +406,12 @@ export default function GlossariesPage() {
{/* Explanation box */}
<div className="mb-6 p-3.5 rounded-xl bg-brand-muted/40 dark:bg-white/[0.03] border border-black/5 dark:border-white/5">
<p className="text-[11px] text-brand-dark/60 dark:text-white/50 font-light leading-relaxed">
<strong className="font-bold text-brand-dark/80 dark:text-white/80">À quoi ça sert ?</strong> Cliquer sur une carte crée un glossaire pré-rempli avec les termes spécialisés du domaine. Ce glossaire apparaîtra dans vos glossaires ci-dessous, et vous pourrez le <strong>sélectionner manuellement</strong> sur la page Traduire pour forcer des traductions de termes précis.
<strong className="font-bold text-brand-dark/80 dark:text-white/80">{t('glossaries.presets.whatForBold')}</strong> {t('glossaries.presets.whatForDesc')}
</p>
<div className="flex items-center gap-2 mt-2.5">
<MousePointerClick size={11} className="text-brand-accent shrink-0" />
<p className="text-[11px] text-brand-accent/80 font-medium">
Cliquez sur une carte glossaire créé sélectionnez-le dans Traduire
{t('glossaries.presets.clickHint')}
</p>
</div>
</div>
@@ -431,14 +431,14 @@ export default function GlossariesPage() {
<div className="p-1.5 bg-brand-accent/10 rounded-lg text-brand-accent group-hover:scale-110 transition-transform">
{isCreatingThis ? <Loader2 size={16} className="animate-spin" /> : <Icon size={16} />}
</div>
{isCreatingThis && <span className="text-[10px] text-brand-accent font-bold uppercase">Création</span>}
{isCreatingThis && <span className="text-[10px] text-brand-accent font-bold uppercase">{t('glossaries.presets.creating')}</span>}
</div>
<div>
<h4 className="text-xs font-bold text-brand-dark dark:text-white mb-1">
{p.title}
{t(p.titleKey)}
</h4>
<p className="text-[10px] text-brand-dark/45 dark:text-white/45 leading-normal font-light">
{p.desc}
{t(p.descKey)}
</p>
</div>
</button>
@@ -452,18 +452,18 @@ export default function GlossariesPage() {
<div className="flex items-center justify-between mb-5">
<div>
<h2 className="text-lg font-serif font-medium text-brand-dark dark:text-white tracking-tight">
Vos <span className="italic">glossaires</span>
{t('glossaries.grid.title')} <span className="italic">{t('glossaries.grid.titleHighlight')}</span>
</h2>
<p className="text-[11px] text-brand-dark/45 dark:text-white/40 font-light mt-1">
{glossaries.length > 0
? `${glossaries.length} glossaire${glossaries.length > 1 ? 's' : ''} — cliquez sur une carte pour la modifier`
: 'Créez votre premier glossaire ou importez un preset ci-dessus'}
? t('glossaries.grid.countWithAction', { count: glossaries.length, plural: glossaries.length > 1 ? 's' : '' })
: t('glossaries.grid.emptyAction')}
</p>
</div>
<div className="flex items-center gap-3">
{currentTargetInfo && (
<span className="flex items-center gap-1.5 text-[10px] font-bold text-brand-dark/50 dark:text-white/40 bg-brand-muted dark:bg-white/5 border border-black/5 dark:border-white/5 px-3 py-1.5 rounded-full">
<span>Traduction active :</span>
<span>{t('glossaries.grid.activeTranslation')}</span>
<span>{currentTargetInfo.flag} {currentTargetInfo.label}</span>
</span>
)}
@@ -473,7 +473,7 @@ export default function GlossariesPage() {
className="flex items-center gap-1.5 text-[11px] font-bold text-brand-accent hover:underline shrink-0"
>
<ExternalLink size={12} />
Aller à Traduire pour activer
{t('glossaries.grid.goToTranslate')}
</Link>
)}
</div>
@@ -512,12 +512,12 @@ export default function GlossariesPage() {
{/* Match / mismatch badge */}
{matchesTarget && (
<div className="absolute top-3 right-3 flex items-center gap-1 bg-brand-accent/10 text-brand-accent px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider">
<CheckCircle2 size={9} /> Compatible
<CheckCircle2 size={9} /> {t('glossaries.badge.compatible')}
</div>
)}
{mismatch && (
<div className="absolute top-3 right-3 flex items-center gap-1 bg-amber-500/10 text-amber-600 dark:text-amber-400 px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider">
<AlertCircle size={9} /> Autre cible
<AlertCircle size={9} /> {t('glossaries.badge.otherTarget')}
</div>
)}
@@ -556,7 +556,7 @@ export default function GlossariesPage() {
onClick={() => handleEditClick(glossary.id)}
className="flex-1 py-2 px-3 rounded-lg bg-brand-muted/60 dark:bg-white/5 hover:bg-brand-accent/10 dark:hover:bg-brand-accent/15 text-brand-dark/70 dark:text-white/60 hover:text-brand-accent text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer flex items-center justify-center gap-1.5"
>
<Save size={10} /> Modifier les termes
<Save size={10} /> {t('glossaries.card.editTerms')}
</button>
<button
onClick={(e) => {
@@ -565,7 +565,7 @@ export default function GlossariesPage() {
}}
className="py-2 px-3 rounded-lg bg-red-500/5 hover:bg-red-500 hover:text-white text-red-500 text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer flex items-center gap-1.5"
>
<Trash2 size={10} /> Supprimer
<Trash2 size={10} /> {t('glossaries.card.delete')}
</button>
</div>
</div>