maj api + correction bug

This commit is contained in:
2026-06-08 21:23:39 +02:00
parent 0054f731c7
commit ecfe7fb433
4 changed files with 204 additions and 39 deletions

View File

@@ -84,3 +84,7 @@ export async function updateSiteSettings(data: any) {
return { success: false, error: "Erreur lors de la mise à jour des paramètres" };
}
}
export async function getExternalApiKey() {
return process.env.EXTERNAL_API_KEY || "ezfzeFZEfztZEgZEFzETGZEGZERZERF";
}

View File

@@ -1,8 +1,8 @@
"use client";
import React, { useState, useEffect, useTransition } from 'react';
import { Save, Globe, Mail, Phone, MapPin, Share2, Link as LinkIcon, Loader2, Newspaper, Briefcase, Plus, Trash2 } from 'lucide-react';
import { getSiteSettings, updateSiteSettings } from '@/app/actions/settings';
import { Save, Globe, Mail, Phone, MapPin, Share2, Link as LinkIcon, Loader2, Newspaper, Briefcase, Plus, Trash2, Key, Terminal, Copy, Check } from 'lucide-react';
import { getSiteSettings, updateSiteSettings, getExternalApiKey } from '@/app/actions/settings';
import { toast } from 'react-hot-toast';
import { getBlogPosts } from '@/app/actions/actualites';
import RichTextEditor from '@/components/RichTextEditor';
@@ -49,6 +49,9 @@ export default function SettingsPage({
const [notFoundQuotes, setNotFoundQuotes] = useState<string[]>([]);
const [resetPasswordTemplate, setResetPasswordTemplate] = useState('');
const [verifyEmailTemplate, setVerifyEmailTemplate] = useState('');
const [apiKey, setApiKey] = useState('');
const [copied, setCopied] = useState(false);
const [apiOrigin, setApiOrigin] = useState('https://afroprenariat.com');
// Use a state for the tab if we want it to be fully client-side reactive without full reload
// But since the user wants it "like moderation center", I'll use URL search params
@@ -57,12 +60,17 @@ export default function SettingsPage({
useEffect(() => {
async function loadData() {
try {
const [settingsData, postsData] = await Promise.all([
const [settingsData, postsData, apiKeyData] = await Promise.all([
getSiteSettings(),
getBlogPosts()
getBlogPosts(),
getExternalApiKey()
]);
setSettings(settingsData);
setAllPosts(postsData);
setApiKey(apiKeyData);
if (typeof window !== 'undefined') {
setApiOrigin(window.location.origin);
}
setSelectedPostIds(settingsData?.homeBlogIds || []);
setSelectedHomeCategories(settingsData?.homeCategories || []);
setSelectedBlogCategories(settingsData?.homeBlogCategories || []);
@@ -83,34 +91,35 @@ export default function SettingsPage({
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = {
siteName: formData.get('siteName'),
siteSlogan: formData.get('siteSlogan'),
contactEmail: formData.get('contactEmail'),
contactPhone: formData.get('contactPhone'),
address: formData.get('address'),
facebookUrl: formData.get('facebookUrl'),
twitterUrl: formData.get('twitterUrl'),
instagramUrl: formData.get('instagramUrl'),
linkedinUrl: formData.get('linkedinUrl'),
footerText: formData.get('footerText'),
homeBlogShow: formData.get('homeBlogShow') === 'on',
homeBlogTitle: formData.get('homeBlogTitle'),
homeBlogSubtitle: formData.get('homeBlogSubtitle'),
homeBlogCount: parseInt(formData.get('homeBlogCount') as string || '3'),
homeBlogSelection: formData.get('homeBlogSelection'),
siteName: (formData.get('siteName') ?? settings?.siteName) as string,
siteSlogan: (formData.get('siteSlogan') ?? settings?.siteSlogan) as string,
contactEmail: (formData.get('contactEmail') ?? settings?.contactEmail) as string,
contactPhone: (formData.get('contactPhone') ?? settings?.contactPhone) as string,
address: (formData.get('address') ?? settings?.address) as string,
facebookUrl: (formData.get('facebookUrl') ?? settings?.facebookUrl) as string,
twitterUrl: (formData.get('twitterUrl') ?? settings?.twitterUrl) as string,
instagramUrl: (formData.get('instagramUrl') ?? settings?.instagramUrl) as string,
linkedinUrl: (formData.get('linkedinUrl') ?? settings?.linkedinUrl) as string,
footerText: (formData.get('footerText') ?? settings?.footerText) as string,
homeBlogShow: formData.has('homeBlogShow') ? formData.get('homeBlogShow') === 'on' : settings?.homeBlogShow,
homeBlogTitle: (formData.get('homeBlogTitle') ?? settings?.homeBlogTitle) as string,
homeBlogSubtitle: (formData.get('homeBlogSubtitle') ?? settings?.homeBlogSubtitle) as string,
homeBlogCount: formData.has('homeBlogCount') ? parseInt(formData.get('homeBlogCount') as string || '3') : settings?.homeBlogCount,
homeBlogSelection: (formData.get('homeBlogSelection') ?? settings?.homeBlogSelection) as string,
homeBlogIds: selectedPostIds,
homeBlogCategories: selectedBlogCategories,
homeCategories: selectedHomeCategories,
notFoundQuotes: notFoundQuotes.filter(q => q.trim() !== ''),
resetPasswordSubject: formData.get('resetPasswordSubject'),
resetPasswordSubject: (formData.get('resetPasswordSubject') ?? settings?.resetPasswordSubject) as string,
resetPasswordTemplate: resetPasswordTemplate,
verifyEmailSubject: formData.get('verifyEmailSubject'),
verifyEmailSubject: (formData.get('verifyEmailSubject') ?? settings?.verifyEmailSubject) as string,
verifyEmailTemplate: verifyEmailTemplate,
};
startTransition(async () => {
const result = await updateSiteSettings(data);
if (result.success) {
setSettings(result.settings);
toast.success("Paramètres mis à jour avec succès");
} else {
toast.error("Erreur lors de la mise à jour");
@@ -172,6 +181,7 @@ export default function SettingsPage({
{/* Tabs Switcher */}
<div className="flex gap-2 mb-8 bg-slate-900/50 p-1 rounded-xl border border-slate-800 w-fit">
<button
type="button"
onClick={() => setCurrentTab('general')}
className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${currentTab === 'general' ? 'bg-indigo-600 text-white shadow-lg' : 'text-slate-400 hover:text-white hover:bg-slate-800'}`}
>
@@ -179,12 +189,21 @@ export default function SettingsPage({
Général
</button>
<button
type="button"
onClick={() => setCurrentTab('emails')}
className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${currentTab === 'emails' ? 'bg-indigo-600 text-white shadow-lg' : 'text-slate-400 hover:text-white hover:bg-slate-800'}`}
>
<Mail className="w-4 h-4" />
Configuration Mail
</button>
<button
type="button"
onClick={() => setCurrentTab('api')}
className={`px-6 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${currentTab === 'api' ? 'bg-indigo-600 text-white shadow-lg' : 'text-slate-400 hover:text-white hover:bg-slate-800'}`}
>
<Key className="w-4 h-4" />
Intégration API
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
@@ -581,6 +600,105 @@ export default function SettingsPage({
</div>
)}
{currentTab === 'api' && (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Key className="w-5 h-5 text-amber-400" />
<h2 className="font-semibold text-white">Clé d'API Externe</h2>
</div>
<div className="p-6 space-y-4">
<p className="text-sm text-slate-400">
Utilisez cette clé API pour authentifier vos requêtes lors de la création d'articles depuis un système externe.
</p>
<div className="flex items-center gap-2 max-w-xl">
<div className="flex-grow bg-slate-950 border border-slate-700 rounded-xl p-3 text-white font-mono text-sm overflow-x-auto select-all">
{apiKey || "Chargement de la clé..."}
</div>
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(apiKey);
setCopied(true);
toast.success("Clé API copiée !");
setTimeout(() => setCopied(false), 2000);
}}
className="p-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl transition-all flex items-center gap-2 font-semibold text-sm cursor-pointer shrink-0"
>
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
{copied ? "Copié" : "Copier"}
</button>
</div>
<p className="text-[11px] text-slate-500">
Gardez cette clé confidentielle. Ne la partagez pas dans des environnements publics ou côté client.
</p>
</div>
</div>
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Terminal className="w-5 h-5 text-indigo-400" />
<h2 className="font-semibold text-white">Documentation de l'API de Création d'Articles</h2>
</div>
<div className="p-6 space-y-6">
<div>
<h3 className="text-sm font-bold text-slate-300 mb-2">Endpoint</h3>
<div className="bg-slate-950 border border-slate-700 rounded-xl p-3 flex items-center gap-2 font-mono text-xs text-white">
<span className="bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-2 py-0.5 rounded font-bold">POST</span>
<span className="text-slate-300">{apiOrigin}/api/external/articles</span>
</div>
</div>
<div>
<h3 className="text-sm font-bold text-slate-300 mb-2">Headers requis</h3>
<div className="bg-slate-950 border border-slate-700 rounded-xl p-4 font-mono text-xs text-slate-300 space-y-1">
<div><span className="text-indigo-400">Content-Type</span>: application/json</div>
<div><span className="text-indigo-400">x-api-key</span>: <span className="text-amber-400">{"{VOTRE_CLE_API}"}</span></div>
</div>
</div>
<div>
<h3 className="text-sm font-bold text-slate-300 mb-2">Corps de la requête (JSON)</h3>
<pre className="bg-slate-950 border border-slate-700 rounded-xl p-4 font-mono text-xs text-slate-300 overflow-x-auto">
{`{
"title": "Un nouvel article sur l'écosystème",
"excerpt": "Le résumé de l'article visible sur la liste.",
"content": "<h1>Contenu de l'article</h1><p>Corps de l'article en HTML ou texte brut.</p>",
"author": "Afropreneur API",
"imageUrl": "https://images.unsplash.com/photo-1516321318423-f06f85e504b3",
"type": "Actualité",
"tags": ["Technologie", "Innovation"],
"status": "PUBLISHED",
"publishedAt": "${new Date().toISOString()}"
}`}
</pre>
</div>
<div>
<h3 className="text-sm font-bold text-slate-300 mb-2">Exemple de requête (cURL)</h3>
<pre className="bg-slate-950 border border-slate-700 rounded-xl p-4 font-mono text-xs text-slate-300 overflow-x-auto whitespace-pre-wrap">
{`curl -X POST "${apiOrigin}/api/external/articles" \\
-H "Content-Type: application/json" \\
-H "x-api-key: ${apiKey || "VOTRE_CLE_API"}" \\
-d '{
"title": "Un nouvel article sur l ecosystème",
"excerpt": "Le résumé de l article.",
"content": "Le corps de l article.",
"author": "Afropreneur API",
"imageUrl": "https://images.unsplash.com/photo-1516321318423-f06f85e504b3",
"type": "Actualité",
"tags": ["Tech", "Startup"],
"status": "PUBLISHED"
}'`}
</pre>
</div>
</div>
</div>
</div>
)}
{currentTab !== 'api' && (
<div className="flex justify-end">
<button
type="submit"
@@ -595,6 +713,7 @@ export default function SettingsPage({
Enregistrer les modifications
</button>
</div>
)}
</form>
</div>
);

View File

@@ -96,15 +96,57 @@ export async function POST(request: NextRequest) {
.replace(/{siteName}/g, siteName);
const { sendEmail } = await import('@/lib/mail');
// Send user verification email
await sendEmail({
to: email,
subject,
html,
});
// Send admin notification email
try {
const adminEmail = settings?.contactEmail || 'support@afrohub.com';
const adminSubject = `[Admin] Nouvelle inscription sur ${siteName} : ${name}`;
const adminHtml = `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;">
<h2 style="color: #4f46e5; text-align: center;">Notification Admin - Inscription</h2>
<p>Bonjour,</p>
<p>Un nouvel utilisateur vient de s'inscrire sur <strong>${siteName}</strong> :</p>
<table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
<tr>
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb; font-weight: bold; width: 35%;">Nom complet :</td>
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb;">${name}</td>
</tr>
<tr>
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb; font-weight: bold;">Adresse email :</td>
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb;"><a href="mailto:${email}">${email}</a></td>
</tr>
<tr>
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb; font-weight: bold;">Date d'inscription :</td>
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb;">${new Date().toLocaleString('fr-FR')}</td>
</tr>
<tr>
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb; font-weight: bold;">Rôle par défaut :</td>
<td style="padding: 8px; border-bottom: 1px solid #e5e7eb;">VISITOR</td>
</tr>
</table>
<p style="margin-top: 25px; text-align: center;">
<a href="${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/admin" style="background-color: #4f46e5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold; display: inline-block;">Accéder au Dashboard Admin</a>
</p>
</div>
`;
await sendEmail({
to: adminEmail,
subject: adminSubject,
html: adminHtml,
});
} catch (adminMailError) {
console.error('Failed to send admin signup notification email:', adminMailError);
}
} catch (mailError) {
console.error('Failed to send verification email:', mailError);
// We don't fail registration if mail fails, but maybe we should?
// For now, just log it.
console.error('Failed to send registration email(s):', mailError);
}
// Omit password from response

File diff suppressed because one or more lines are too long