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" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getExternalApiKey() {
|
||||
return process.env.EXTERNAL_API_KEY || "ezfzeFZEfztZEgZEFzETGZEGZERZERF";
|
||||
}
|
||||
|
||||
@@ -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,20 +600,120 @@ export default function SettingsPage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 disabled:bg-slate-700 text-white px-8 py-3 rounded-xl font-bold transition-all shadow-lg flex items-center gap-2"
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-5 h-5" />
|
||||
)}
|
||||
Enregistrer les modifications
|
||||
</button>
|
||||
</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"
|
||||
disabled={isPending}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 disabled:bg-slate-700 text-white px-8 py-3 rounded-xl font-bold transition-all shadow-lg flex items-center gap-2"
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-5 h-5" />
|
||||
)}
|
||||
Enregistrer les modifications
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user