maj api + correction bug
This commit is contained in:
@@ -84,3 +84,7 @@ export async function updateSiteSettings(data: any) {
|
|||||||
return { success: false, error: "Erreur lors de la mise à jour des paramètres" };
|
return { success: false, error: "Erreur lors de la mise à jour des paramètres" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getExternalApiKey() {
|
||||||
|
return process.env.EXTERNAL_API_KEY || "ezfzeFZEfztZEgZEFzETGZEGZERZERF";
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useTransition } from 'react';
|
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 { Save, Globe, Mail, Phone, MapPin, Share2, Link as LinkIcon, Loader2, Newspaper, Briefcase, Plus, Trash2, Key, Terminal, Copy, Check } from 'lucide-react';
|
||||||
import { getSiteSettings, updateSiteSettings } from '@/app/actions/settings';
|
import { getSiteSettings, updateSiteSettings, getExternalApiKey } from '@/app/actions/settings';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { getBlogPosts } from '@/app/actions/actualites';
|
import { getBlogPosts } from '@/app/actions/actualites';
|
||||||
import RichTextEditor from '@/components/RichTextEditor';
|
import RichTextEditor from '@/components/RichTextEditor';
|
||||||
@@ -49,6 +49,9 @@ export default function SettingsPage({
|
|||||||
const [notFoundQuotes, setNotFoundQuotes] = useState<string[]>([]);
|
const [notFoundQuotes, setNotFoundQuotes] = useState<string[]>([]);
|
||||||
const [resetPasswordTemplate, setResetPasswordTemplate] = useState('');
|
const [resetPasswordTemplate, setResetPasswordTemplate] = useState('');
|
||||||
const [verifyEmailTemplate, setVerifyEmailTemplate] = 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
|
// 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
|
// But since the user wants it "like moderation center", I'll use URL search params
|
||||||
@@ -57,12 +60,17 @@ export default function SettingsPage({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
try {
|
try {
|
||||||
const [settingsData, postsData] = await Promise.all([
|
const [settingsData, postsData, apiKeyData] = await Promise.all([
|
||||||
getSiteSettings(),
|
getSiteSettings(),
|
||||||
getBlogPosts()
|
getBlogPosts(),
|
||||||
|
getExternalApiKey()
|
||||||
]);
|
]);
|
||||||
setSettings(settingsData);
|
setSettings(settingsData);
|
||||||
setAllPosts(postsData);
|
setAllPosts(postsData);
|
||||||
|
setApiKey(apiKeyData);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
setApiOrigin(window.location.origin);
|
||||||
|
}
|
||||||
setSelectedPostIds(settingsData?.homeBlogIds || []);
|
setSelectedPostIds(settingsData?.homeBlogIds || []);
|
||||||
setSelectedHomeCategories(settingsData?.homeCategories || []);
|
setSelectedHomeCategories(settingsData?.homeCategories || []);
|
||||||
setSelectedBlogCategories(settingsData?.homeBlogCategories || []);
|
setSelectedBlogCategories(settingsData?.homeBlogCategories || []);
|
||||||
@@ -83,34 +91,35 @@ export default function SettingsPage({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData(e.currentTarget);
|
const formData = new FormData(e.currentTarget);
|
||||||
const data = {
|
const data = {
|
||||||
siteName: formData.get('siteName'),
|
siteName: (formData.get('siteName') ?? settings?.siteName) as string,
|
||||||
siteSlogan: formData.get('siteSlogan'),
|
siteSlogan: (formData.get('siteSlogan') ?? settings?.siteSlogan) as string,
|
||||||
contactEmail: formData.get('contactEmail'),
|
contactEmail: (formData.get('contactEmail') ?? settings?.contactEmail) as string,
|
||||||
contactPhone: formData.get('contactPhone'),
|
contactPhone: (formData.get('contactPhone') ?? settings?.contactPhone) as string,
|
||||||
address: formData.get('address'),
|
address: (formData.get('address') ?? settings?.address) as string,
|
||||||
facebookUrl: formData.get('facebookUrl'),
|
facebookUrl: (formData.get('facebookUrl') ?? settings?.facebookUrl) as string,
|
||||||
twitterUrl: formData.get('twitterUrl'),
|
twitterUrl: (formData.get('twitterUrl') ?? settings?.twitterUrl) as string,
|
||||||
instagramUrl: formData.get('instagramUrl'),
|
instagramUrl: (formData.get('instagramUrl') ?? settings?.instagramUrl) as string,
|
||||||
linkedinUrl: formData.get('linkedinUrl'),
|
linkedinUrl: (formData.get('linkedinUrl') ?? settings?.linkedinUrl) as string,
|
||||||
footerText: formData.get('footerText'),
|
footerText: (formData.get('footerText') ?? settings?.footerText) as string,
|
||||||
homeBlogShow: formData.get('homeBlogShow') === 'on',
|
homeBlogShow: formData.has('homeBlogShow') ? formData.get('homeBlogShow') === 'on' : settings?.homeBlogShow,
|
||||||
homeBlogTitle: formData.get('homeBlogTitle'),
|
homeBlogTitle: (formData.get('homeBlogTitle') ?? settings?.homeBlogTitle) as string,
|
||||||
homeBlogSubtitle: formData.get('homeBlogSubtitle'),
|
homeBlogSubtitle: (formData.get('homeBlogSubtitle') ?? settings?.homeBlogSubtitle) as string,
|
||||||
homeBlogCount: parseInt(formData.get('homeBlogCount') as string || '3'),
|
homeBlogCount: formData.has('homeBlogCount') ? parseInt(formData.get('homeBlogCount') as string || '3') : settings?.homeBlogCount,
|
||||||
homeBlogSelection: formData.get('homeBlogSelection'),
|
homeBlogSelection: (formData.get('homeBlogSelection') ?? settings?.homeBlogSelection) as string,
|
||||||
homeBlogIds: selectedPostIds,
|
homeBlogIds: selectedPostIds,
|
||||||
homeBlogCategories: selectedBlogCategories,
|
homeBlogCategories: selectedBlogCategories,
|
||||||
homeCategories: selectedHomeCategories,
|
homeCategories: selectedHomeCategories,
|
||||||
notFoundQuotes: notFoundQuotes.filter(q => q.trim() !== ''),
|
notFoundQuotes: notFoundQuotes.filter(q => q.trim() !== ''),
|
||||||
resetPasswordSubject: formData.get('resetPasswordSubject'),
|
resetPasswordSubject: (formData.get('resetPasswordSubject') ?? settings?.resetPasswordSubject) as string,
|
||||||
resetPasswordTemplate: resetPasswordTemplate,
|
resetPasswordTemplate: resetPasswordTemplate,
|
||||||
verifyEmailSubject: formData.get('verifyEmailSubject'),
|
verifyEmailSubject: (formData.get('verifyEmailSubject') ?? settings?.verifyEmailSubject) as string,
|
||||||
verifyEmailTemplate: verifyEmailTemplate,
|
verifyEmailTemplate: verifyEmailTemplate,
|
||||||
};
|
};
|
||||||
|
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const result = await updateSiteSettings(data);
|
const result = await updateSiteSettings(data);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
setSettings(result.settings);
|
||||||
toast.success("Paramètres mis à jour avec succès");
|
toast.success("Paramètres mis à jour avec succès");
|
||||||
} else {
|
} else {
|
||||||
toast.error("Erreur lors de la mise à jour");
|
toast.error("Erreur lors de la mise à jour");
|
||||||
@@ -172,6 +181,7 @@ export default function SettingsPage({
|
|||||||
{/* Tabs Switcher */}
|
{/* Tabs Switcher */}
|
||||||
<div className="flex gap-2 mb-8 bg-slate-900/50 p-1 rounded-xl border border-slate-800 w-fit">
|
<div className="flex gap-2 mb-8 bg-slate-900/50 p-1 rounded-xl border border-slate-800 w-fit">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setCurrentTab('general')}
|
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'}`}
|
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
|
Général
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setCurrentTab('emails')}
|
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'}`}
|
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" />
|
<Mail className="w-4 h-4" />
|
||||||
Configuration Mail
|
Configuration Mail
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
@@ -581,6 +600,105 @@ export default function SettingsPage({
|
|||||||
</div>
|
</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">
|
<div className="flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -595,6 +713,7 @@ export default function SettingsPage({
|
|||||||
Enregistrer les modifications
|
Enregistrer les modifications
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -96,15 +96,57 @@ export async function POST(request: NextRequest) {
|
|||||||
.replace(/{siteName}/g, siteName);
|
.replace(/{siteName}/g, siteName);
|
||||||
|
|
||||||
const { sendEmail } = await import('@/lib/mail');
|
const { sendEmail } = await import('@/lib/mail');
|
||||||
|
|
||||||
|
// Send user verification email
|
||||||
await sendEmail({
|
await sendEmail({
|
||||||
to: email,
|
to: email,
|
||||||
subject,
|
subject,
|
||||||
html,
|
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) {
|
} catch (mailError) {
|
||||||
console.error('Failed to send verification email:', mailError);
|
console.error('Failed to send registration email(s):', mailError);
|
||||||
// We don't fail registration if mail fails, but maybe we should?
|
|
||||||
// For now, just log it.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Omit password from response
|
// Omit password from response
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user