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

@@ -162,10 +162,10 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
setIsOpen(false);
} else {
const errData = await res.json().catch(() => null);
setError(errData?.message || `Import failed (${res.status})`);
setError(errData?.message || t('translate.glossary.importFailed').replace('{status}', String(res.status)));
}
} catch {
setError('Network error');
setError(t('translate.glossary.networkError'));
} finally {
setImportingId(null);
}
@@ -217,10 +217,10 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
fetchData();
} else {
const errData = await res.json().catch(() => null);
setError(errData?.message || 'Erreur lors de l\'ajout du terme');
setError(errData?.message || t('translate.glossary.addTermError'));
}
} catch {
setError('Erreur réseau');
setError(t('translate.glossary.networkError'));
} finally {
setIsAddingTerm(false);
}
@@ -316,7 +316,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
{mode === 'classic' ? (
<div className="p-2.5 bg-brand-dark/5 dark:bg-white/5 rounded-lg text-center">
<span className="text-xs font-black uppercase opacity-45 block">
Moteur neutre sans glossaire (IA uniquement)
{t('translate.glossary.classicMode') || 'Moteur neutre sans glossaire (IA uniquement)'}
</span>
</div>
) : !isPro ? (
@@ -330,7 +330,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
{/* Help Info text */}
<p className="text-[10.5px] text-brand-dark/60 dark:text-white/40 leading-normal font-light">
Le glossaire force la traduction de termes précis. Choisissez un glossaire dont la <strong>langue source</strong> correspond à la langue d'origine de votre document.
{t('translate.glossary.helpText') || 'Le glossaire force la traduction de termes précis. Choisissez un glossaire dont la langue source correspond à la langue d\'origine de votre document.'}
</p>
{/* Mismatch Warning — source language */}
@@ -338,7 +338,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
<div className="flex items-start gap-1.5 p-2 rounded-lg bg-amber-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400 text-[10px] leading-normal font-medium animate-fade-in">
<span className="shrink-0 text-amber-500"></span>
<span>
<strong>Attention :</strong> Ce glossaire utilise la langue source <strong>{getFlag(selected.source_language)} {selected.source_language.toUpperCase()}</strong>, mais votre document est configuré en <strong>{getFlag(sourceLang)} {sourceLang.toUpperCase()}</strong>.
<strong>{t('translate.glossary.sourceWarning') || 'Attention :'} Ce glossaire utilise la langue source</strong> <strong>{getFlag(selected.source_language)} {selected.source_language.toUpperCase()}</strong>, {t('translate.glossary.sourceWarningBut') || 'mais votre document est configuré en'} <strong>{getFlag(sourceLang)} {sourceLang.toUpperCase()}</strong>.
</span>
</div>
)}
@@ -348,7 +348,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
<div className="flex items-start gap-1.5 p-2 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400 text-[10px] leading-normal font-medium animate-fade-in">
<span className="shrink-0">🎯</span>
<span>
<strong>Incompatibilité de cible :</strong> Ce glossaire est prévu pour traduire vers <strong>{getFlag(selected.target_language)} {selected.target_language.toUpperCase()}</strong>, mais votre document cible <strong>{targetFlag} {targetLang.toUpperCase()}</strong>. Les termes risquent de ne pas être pertinents.
<strong>{t('translate.glossary.targetWarning') || 'Incompatibilité de cible :'}</strong> <strong>{getFlag(selected.target_language)} {selected.target_language.toUpperCase()}</strong>, {t('translate.glossary.targetWarningBut') || 'mais votre document cible'} <strong>{targetFlag} {targetLang.toUpperCase()}</strong>. {t('translate.glossary.targetWarningEnd') || 'Les termes risquent de ne pas être pertinents.'}
</span>
</div>
)}
@@ -367,12 +367,12 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
>
<div className="text-left min-w-0 flex-1 pr-2">
<span className="text-xs font-bold text-brand-dark dark:text-white tracking-tight block truncate">
{selected ? selected.name : (isLoading ? "Chargement..." : "Sélectionner un glossaire...")}
{selected ? selected.name : (isLoading ? t('translate.glossary.loading') || 'Chargement...' : t('translate.glossary.selectPlaceholder') || 'Sélectionner un glossaire...')}
</span>
<span className="text-[10px] font-extrabold uppercase tracking-wider text-brand-dark/40 dark:text-white/40 block mt-0.5">
{selected
? `${getFlag(selected.source_language)} ➜ ${selected.target_language === 'multi' ? '🌐 MULTILINGUE' : getFlag(selected.target_language || targetLang)} • ${selected.terms_count} termes`
: (filteredGlossaries.length > 0 || filteredTemplates.length > 0 ? "Sélectionnez un glossaire" : "Aucun glossaire disponible")
? `${getFlag(selected.source_language)}${selected.target_language === 'multi' ? `🌐 ${t('translate.glossary.multilingual') || 'MULTILINGUE'}` : getFlag(selected.target_language || targetLang)}${selected.terms_count} ${t('translate.glossary.terms') || 'termes'}`
: (filteredGlossaries.length > 0 || filteredTemplates.length > 0 ? t('translate.glossary.select') || 'Sélectionnez un glossaire' : t('translate.glossary.noGlossaryAvailable') || 'Aucun glossaire disponible')
}
</span>
</div>
@@ -391,7 +391,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
{sourceLang !== 'auto' && (glossaries.length > 0 || templates.length > 0) && (
<div className="px-2 py-1.5 border-b border-black/[0.03] dark:border-white/[0.03] flex justify-between items-center mb-1">
<span className="text-[10px] font-black text-brand-dark/40 dark:text-white/40 uppercase tracking-widest">
Filtrer par langue ({sourceFlag})
{t('translate.glossary.filterByLang') || 'Filtrer par langue'} ({sourceFlag})
</span>
<button
type="button"
@@ -401,7 +401,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
filterByLang ? "bg-brand-accent/10 text-brand-accent" : "bg-brand-dark/10 dark:bg-white/10 text-brand-dark/50 dark:text-white/50"
)}
>
{filterByLang ? "Actif" : "Inactif"}
{filterByLang ? t('translate.glossary.active') || 'Actif' : t('translate.glossary.inactive') || 'Inactif'}
</button>
</div>
)}
@@ -410,7 +410,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
{filteredGlossaries.length > 0 && (
<div className="mb-2">
<div className="px-2 py-1 text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest">
Mes Glossaires
{t('translate.glossary.myGlossaries') || 'Mes Glossaires'}
</div>
{filteredGlossaries.map(g => {
const flag = getFlag(g.source_language);
@@ -433,7 +433,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
<div className="text-left min-w-0 flex-1 pr-2">
<span className="text-xs font-bold block leading-none truncate">{g.name}</span>
<span className="text-[10px] uppercase tracking-wider text-brand-dark/40 dark:text-white/45 font-bold block mt-1">
{flag} ➜ {g.target_language === 'multi' ? '🌐 MULTILINGUE' : getFlag(g.target_language || targetLang)} • {g.terms_count} termes
{flag} {g.target_language === 'multi' ? `🌐 ${t('translate.glossary.multilingual') || 'MULTILINGUE'}` : getFlag(g.target_language || targetLang)} {g.terms_count} {t('translate.glossary.terms') || 'termes'}
</span>
</div>
{isSelected && <Check size={12} className="text-brand-accent shrink-0" />}
@@ -447,7 +447,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
{filteredTemplates.length > 0 && (
<div>
<div className="px-2 py-1 text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest border-t border-black/[0.03] dark:border-white/[0.03] pt-2 mt-1">
Modèles disponibles
{t('translate.glossary.availableTemplates') || 'Modèles disponibles'}
</div>
<div className="grid grid-cols-1 gap-0.5">
{filteredTemplates.map(tmpl => {
@@ -457,7 +457,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
);
const isAlreadySelected = existingGlossary?.id === glossaryId;
const flag = getFlag(tmpl.source_lang);
const tFlag = tmpl.target_lang === 'multi' ? '🌐 MULTILINGUE' : getFlag(tmpl.target_lang);
const tFlag = tmpl.target_lang === 'multi' ? `🌐 ${t('translate.glossary.multilingual') || 'MULTILINGUE'}` : getFlag(tmpl.target_lang);
return (
<button
@@ -481,10 +481,10 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
>
<div className="text-left min-w-0 flex-1 pr-2">
<span className="text-xs font-bold block leading-none truncate">
{isImporting ? 'Importation...' : tmpl.name.split('/')[0].trim()}
{isImporting ? t('translate.glossary.importing') || 'Importation...' : tmpl.name.split('/')[0].trim()}
</span>
<span className="text-[10px] uppercase tracking-wider text-brand-dark/40 dark:text-white/45 font-bold block mt-1">
{flag} ➜ {tFlag} • {tmpl.terms_count} termes {existingGlossary ? '(Importé)' : ''}
{flag} {tFlag} {tmpl.terms_count} {t('translate.glossary.terms') || 'termes'} {existingGlossary ? t('translate.glossary.imported') || '(Importé)' : ''}
</span>
</div>
{isImporting ? (
@@ -503,14 +503,14 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
{filteredGlossaries.length === 0 && filteredTemplates.length === 0 && (
<div className="px-2.5 py-4 text-center">
<p className="text-xs text-brand-dark/45 dark:text-white/45 italic mb-3 font-light">
Aucun glossaire ni modèle pour la langue source {sourceFlag || sourceLang.toUpperCase()}.
{t('translate.glossary.noGlossaryForSource') || 'Aucun glossaire ni modèle pour la langue source'} {sourceFlag || sourceLang.toUpperCase()}.
</p>
<div className="flex flex-col gap-1.5">
<a
href={`/dashboard/glossaries?new=true&source=${sourceLang}`}
className="w-full py-2 px-2 bg-brand-dark dark:bg-white text-white dark:text-brand-dark hover:opacity-90 rounded-lg text-xs font-bold uppercase tracking-wider block text-center transition-opacity cursor-pointer"
>
Créer un glossaire {sourceLang === 'auto' ? '' : sourceLang.toUpperCase()}
{t('translate.glossary.createGlossary') || 'Créer un glossaire'} {sourceLang === 'auto' ? '' : sourceLang.toUpperCase()}
</a>
{filterByLang && (glossaries.length > 0 || templates.length > 0) && (
<button
@@ -518,7 +518,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
onClick={() => setFilterByLang(false)}
className="w-full py-2 px-2 bg-brand-muted dark:bg-white/5 hover:bg-brand-muted/70 text-brand-dark dark:text-white rounded-lg text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer"
>
Afficher tous les glossaires
{t('translate.glossary.showAll') || 'Afficher tous les glossaires'}
</button>
)}
</div>
@@ -533,16 +533,16 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
<div className="bg-white/80 dark:bg-[#1a1a1a]/40 rounded-lg p-2.5 border border-black/[0.02] dark:border-white/[0.02] space-y-2">
<div className="flex justify-between items-center border-b border-black/[0.02] dark:border-white/[0.02] pb-1">
<span className="text-xs font-bold uppercase tracking-wider text-brand-dark/30 dark:text-white/30 block">
Aperçu des correspondances actives :
{t('translate.glossary.activePreview') || 'Aperçu des correspondances actives :'}
</span>
<span className="text-xs font-bold text-brand-dark/40 dark:text-white/40">
{selectedGlossaryDetail?.terms?.length || selected.terms_count} au total
{selectedGlossaryDetail?.terms?.length || selected.terms_count} {t('translate.glossary.total') || 'au total'}
</span>
</div>
{isLoadingDetail ? (
<div className="flex items-center justify-center py-3 gap-1.5 text-xs text-brand-dark/40 dark:text-white/30 font-light">
<Loader2 size={12} className="animate-spin text-brand-accent" /> Chargement...
<Loader2 size={12} className="animate-spin text-brand-accent" /> {t('translate.glossary.loading') || 'Chargement...'}
</div>
) : selectedGlossaryDetail?.terms && selectedGlossaryDetail.terms.length > 0 ? (
<div className="grid grid-cols-1 gap-1 max-h-[120px] overflow-y-auto pr-1">
@@ -560,12 +560,12 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
})}
{selectedGlossaryDetail.terms.length > 4 && (
<span className="text-[10px] text-brand-dark/40 dark:text-white/40 block text-right font-medium mt-1">
+ {selectedGlossaryDetail.terms.length - 4} autres termes
+ {selectedGlossaryDetail.terms.length - 4} {t('translate.glossary.moreTerms') || 'autres termes'}
</span>
)}
</div>
) : (
<p className="py-3 text-xs text-brand-dark/40 dark:text-white/30 italic text-center font-light">Aucun terme dans ce glossaire.</p>
<p className="py-3 text-xs text-brand-dark/40 dark:text-white/30 italic text-center font-light">{t('translate.glossary.noTerms') || 'Aucun terme dans ce glossaire.'}</p>
)}
</div>
)}
@@ -576,7 +576,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
<input
type="text"
required
placeholder="Terme Source"
placeholder={t('translate.glossary.sourceTerm') || 'Terme Source'}
value={newSource}
onChange={(e) => setNewSource(e.target.value)}
disabled={isAddingTerm || disabled}
@@ -586,7 +586,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
<input
type="text"
required
placeholder="Traduction"
placeholder={t('translate.glossary.translation') || 'Traduction'}
value={newTarget}
onChange={(e) => setNewTarget(e.target.value)}
disabled={isAddingTerm || disabled}
@@ -596,7 +596,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
type="submit"
disabled={isAddingTerm || disabled || !newSource.trim() || !newTarget.trim()}
className="px-3 bg-brand-dark dark:bg-white text-white dark:text-brand-dark rounded-lg flex items-center justify-center disabled:opacity-35 transition-colors cursor-pointer shrink-0"
title="Ajouter le terme"
title={t('translate.glossary.addTerm') || 'Ajouter le terme'}
>
{isAddingTerm ? (
<Loader2 size={12} className="animate-spin" />
@@ -610,7 +610,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
</div>
) : (
<div className="p-2.5 bg-brand-dark/5 dark:bg-white/5 rounded-lg text-center animate-fade-in">
<span className="text-xs font-black uppercase opacity-40">Moteur neutre sans glossaire appliqué</span>
<span className="text-xs font-black uppercase opacity-40">{t('translate.glossary.disabledMode') || 'Moteur neutre sans glossaire appliqué'}</span>
</div>
)}
</div>