diff --git a/app/api/admin/blog-topics/[id]/route.js b/app/api/admin/blog-topics/[id]/route.js new file mode 100644 index 0000000..94d12a3 --- /dev/null +++ b/app/api/admin/blog-topics/[id]/route.js @@ -0,0 +1,13 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; +const h = (uid) => ({ 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(uid) }); + +export async function DELETE(req, { params }) { + const user = await requireUser(); + if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + const res = await fetch(`${ENGINE_URL}/api/admin/blog-topics/${params.id}`, { method: 'DELETE', headers: h(user.id) }); + return NextResponse.json(await res.json()); +} diff --git a/app/api/admin/blog-topics/generate/route.js b/app/api/admin/blog-topics/generate/route.js new file mode 100644 index 0000000..b25e58c --- /dev/null +++ b/app/api/admin/blog-topics/generate/route.js @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; +const h = (uid) => ({ 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(uid) }); + +export async function POST(req) { + const user = await requireUser(); + if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + const body = await req.json(); + const res = await fetch(`${ENGINE_URL}/api/admin/blog-topics/generate`, { + method: 'POST', headers: { ...h(user.id), 'Content-Type': 'application/json' }, body: JSON.stringify(body), + }); + return NextResponse.json(await res.json()); +} diff --git a/app/api/admin/blog-topics/route.js b/app/api/admin/blog-topics/route.js new file mode 100644 index 0000000..3d850e8 --- /dev/null +++ b/app/api/admin/blog-topics/route.js @@ -0,0 +1,26 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; +const h = (uid) => ({ 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(uid) }); + +// GET — список тем +export async function GET(req) { + const user = await requireUser(); + if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + const { searchParams } = new URL(req.url); + const res = await fetch(`${ENGINE_URL}/api/admin/blog-topics?${searchParams}`, { headers: h(user.id), cache: 'no-store' }); + return NextResponse.json(await res.json()); +} + +// POST — добавить тему +export async function POST(req) { + const user = await requireUser(); + if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + const body = await req.json(); + const res = await fetch(`${ENGINE_URL}/api/admin/blog-topics`, { + method: 'POST', headers: { ...h(user.id), 'Content-Type': 'application/json' }, body: JSON.stringify(body), + }); + return NextResponse.json(await res.json(), { status: res.status }); +} diff --git a/app/api/admin/email/test/route.js b/app/api/admin/email/test/route.js new file mode 100644 index 0000000..49c6fee --- /dev/null +++ b/app/api/admin/email/test/route.js @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; +const h = (uid) => ({ 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(uid) }); + +export async function POST(req) { + const user = await requireUser(); + if (!user?.isAdmin) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + const body = await req.json(); + const res = await fetch(`${ENGINE_URL}/api/admin/email/test`, { + method: 'POST', headers: { ...h(user.id), 'Content-Type': 'application/json' }, body: JSON.stringify(body), + }); + return NextResponse.json(await res.json(), { status: res.status }); +} diff --git a/components/AdminPanel.js b/components/AdminPanel.js index bdc4d90..cd3486b 100644 --- a/components/AdminPanel.js +++ b/components/AdminPanel.js @@ -1,6 +1,6 @@ 'use client'; import { useState } from 'react'; -import { Settings2, CreditCard, TrendingUp, Users, ChevronRight, Loader2, Eye, EyeOff, Save, RefreshCw, Check, AlertCircle, BarChart3, ArrowLeft, Zap, Tag, AlertTriangle, BookOpen, Sliders } from 'lucide-react'; +import { Settings2, CreditCard, TrendingUp, Users, ChevronRight, Loader2, Eye, EyeOff, Save, RefreshCw, Check, AlertCircle, BarChart3, ArrowLeft, Zap, Tag, AlertTriangle, BookOpen, Sliders, Mail } from 'lucide-react'; import Link from 'next/link'; import AdminBilling from './admin/AdminBilling'; import AdminUsers from './admin/AdminUsers'; @@ -8,7 +8,8 @@ import AdminPromos from './admin/AdminPromos'; import AdminQueue from './admin/AdminQueue'; import AdminLogs from './admin/AdminLogs'; import AdminAutogen from './admin/AdminAutogen'; -import AdminContent from './admin/AdminContent'; +import AdminContent from './admin/AdminContent'; +import AdminTopicBank from './admin/AdminTopicBank'; // ────────────────────────────────────────────────────────────── // Sidebar navigation @@ -22,8 +23,10 @@ const SECTIONS = [ { id: 'queue', label: 'Очередь', icon: Zap, desc: 'Задачи генерации, ошибки' }, { id: 'logs', label: 'Логи ошибок', icon: AlertTriangle, desc: 'Последние сбои и проблемы' }, { id: 'autogen', label: 'Автогенерация', icon: BookOpen, desc: 'Расписание статей блога' }, - { id: 'content', label: 'Контент-дефолты', icon: Sliders, desc: 'Настройки для новых каналов' }, - { id: 'plans', label: 'Тарифы', icon: BarChart3, desc: 'Планы, кредиты, операции' }, + { id: 'content', label: 'Контент-дефолты', icon: Sliders, desc: 'Настройки для новых каналов' }, + { id: 'topicbank', label: 'Банк тем блога', icon: BookOpen, desc: 'Темы для zeropost.ru' }, + { id: 'smtp', label: 'Email / SMTP', icon: Mail, desc: 'Уведомления пользователям' }, + { id: 'plans', label: 'Тарифы', icon: BarChart3, desc: 'Планы, кредиты, операции' }, { id: 'promos', label: 'Промокоды', icon: Tag, desc: 'Коды для кредитов и скидок' }, { id: 'billing', label: 'Пользователи', icon: Users, desc: 'Балансы и кредиты' }, ]; @@ -76,8 +79,10 @@ export default function AdminPanel({ initialSection = 'settings' }) { {section === 'queue' && } {section === 'logs' && } {section === 'autogen' && } - {section === 'content' && } - {section === 'plans' && } + {section === 'content' && } + {section === 'topicbank' && } + {section === 'smtp' && } />} + {section === 'plans' && } {section === 'promos' && } {section === 'billing' && } @@ -626,3 +631,41 @@ function DashboardSection() { ); } + +// ── SMTP Test Button ────────────────────────────────────────── +function SmtpTestButton() { + const [email, setEmail] = useState(''); + const [busy, setBusy] = useState(false); + const [msg, setMsg] = useState(''); + + async function test() { + if (!email.trim()) return; + setBusy(true); + const res = await fetch('/api/admin/email/test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ to: email }), + }).then(r => r.json()); + setBusy(false); + setMsg(res.ok ? '✅ Письмо отправлено' : '❌ ' + (res.error || res.message)); + setTimeout(() => setMsg(''), 5000); + } + + return ( +
+

Тест отправки

+
+ setEmail(e.target.value)} + onKeyDown={e => e.key === 'Enter' && test()} + type="email" placeholder="test@example.com" + className="input flex-1 text-sm py-1.5" /> + +
+ {msg &&

{msg}

} +
+ ); +} diff --git a/components/admin/AdminTopicBank.js b/components/admin/AdminTopicBank.js new file mode 100644 index 0000000..974c8f0 --- /dev/null +++ b/components/admin/AdminTopicBank.js @@ -0,0 +1,211 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { Plus, Trash2, Loader2, RefreshCw, Zap, Check, ChevronDown, ChevronRight } from 'lucide-react'; + +const CATEGORY_META = { + 'ai-tools': { label: 'AI инструменты', icon: '🤖', color: 'text-purple-400' }, + 'ai-dev': { label: 'AI разработка', icon: '💻', color: 'text-blue-400' }, + 'automation': { label: 'Автоматизация', icon: '⚙️', color: 'text-green-400' }, + 'cybersec': { label: 'Кибербезопасность', icon: '🔒', color: 'text-red-400' }, +}; + +export default function AdminTopicBank() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [open, setOpen] = useState({}); // cat → expanded + const [msg, setMsg] = useState(''); + const [gen, setGen] = useState({}); // cat → generating + // Форма добавления + const [addCat, setAddCat] = useState('ai-tools'); + const [addText, setAddText] = useState(''); + const [adding, setAdding] = useState(false); + const [showAdd, setShowAdd] = useState(false); + + async function load() { + setLoading(true); + try { + const res = await fetch('/api/admin/blog-topics?includeUsed=true&limit=200').then(r => r.json()); + setData(res); + } catch {} + setLoading(false); + } + + useEffect(() => { load(); }, []); + + async function addTopic() { + if (!addText.trim()) return; + setAdding(true); + const lines = addText.split('\n').map(l => l.trim()).filter(Boolean); + let added = 0; + for (const topic of lines) { + const res = await fetch('/api/admin/blog-topics', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ category: addCat, topic }), + }).then(r => r.json()); + if (res.id) added++; + } + setMsg(`✓ Добавлено ${added} тем`); + setAddText(''); setShowAdd(false); + load(); + setAdding(false); + setTimeout(() => setMsg(''), 2000); + } + + async function deleteTopic(id) { + await fetch(`/api/admin/blog-topics/${id}`, { method: 'DELETE' }); + load(); + } + + async function generate(category) { + setGen(g => ({ ...g, [category]: true })); + await fetch('/api/admin/blog-topics/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ category, count: 10 }), + }); + setMsg(`⚡ Генерирую 10 тем для ${category} (~30с)`); + setTimeout(() => { load(); setMsg(''); }, 35000); + setTimeout(() => setGen(g => ({ ...g, [category]: false })), 35000); + } + + const byCategory = {}; + for (const t of data?.topics || []) { + if (!byCategory[t.category]) byCategory[t.category] = []; + byCategory[t.category].push(t); + } + const stats = Object.fromEntries((data?.stats || []).map(s => [s.category, s])); + + return ( +
+
+
+

Банк тем блога

+

Темы для автогенерации статей на zeropost.ru

+
+
+ {msg && {msg}} + + +
+
+ + {/* Форма добавления */} + {showAdd && ( +
+

Добавить темы

+
+ +
+