forked from admin/zeropost-engine
feat: SMTP, maintenance mode, blog topic bank UI
8. SMTP: emailService.js (nodemailer), templates (welcome/payment/low_credits) /api/admin/email/test — тест отправки app_settings category=smtp (HOST/PORT/USER/PASS/FROM/ENABLED) 9. Maintenance mode: middleware в index.js, MAINTENANCE_MODE в engine settings При true → 503 для всех запросов кроме /uploads и /api/settings 10. Blog topic bank: DB: blog_topics(category,topic,is_used,source,priority) 40 тем мигрированы из хардкода (source=hardcoded) autogen.js: getNextTopic берёт из DB, fallback на TOPIC_BANK admin API: GET/POST /blog-topics, DELETE /:id, POST /generate (AI +10)
This commit is contained in:
@@ -490,3 +490,127 @@ router.delete('/autogen/queue/:id', async (req, res) => {
|
||||
res.json({ ok: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// ── EMAIL ────────────────────────────────────────────────────
|
||||
|
||||
// POST /api/admin/email/test — тестовая отправка
|
||||
router.post('/email/test', async (req, res) => {
|
||||
if (!await requireAdmin(req, res)) return;
|
||||
const { to } = req.body;
|
||||
if (!to) return res.status(400).json({ error: 'to обязателен' });
|
||||
try {
|
||||
const email = require('../services/emailService');
|
||||
const result = await email.send({
|
||||
to,
|
||||
subject: '✅ ZeroPost SMTP тест',
|
||||
html: '<p>Если ты видишь это письмо — SMTP настроен правильно!</p>',
|
||||
text: 'Если ты видишь это письмо — SMTP настроен правильно!',
|
||||
});
|
||||
if (result.skipped) return res.json({ ok: false, message: 'SMTP отключён или не настроен' });
|
||||
if (result.error) return res.status(500).json({ error: result.error });
|
||||
res.json({ ok: true, messageId: result.messageId });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// ── BLOG TOPIC BANK ──────────────────────────────────────────
|
||||
|
||||
// GET /api/admin/blog-topics — список тем по категории
|
||||
router.get('/blog-topics', async (req, res) => {
|
||||
if (!await requireAdmin(req, res)) return;
|
||||
const { category, includeUsed = 'false', limit = 100 } = req.query;
|
||||
try {
|
||||
const where = category ? 'WHERE bt.category=$1' : '';
|
||||
const args = category ? [category] : [];
|
||||
|
||||
const { rows } = await query(`
|
||||
SELECT bt.*,
|
||||
EXISTS(SELECT 1 FROM articles a WHERE a.source_topic=bt.topic) as is_published
|
||||
FROM blog_topics bt
|
||||
${where}
|
||||
${includeUsed !== 'true' ? (where ? 'AND' : 'WHERE') + ' bt.is_used=false' : ''}
|
||||
ORDER BY bt.priority DESC, bt.created_at ASC
|
||||
LIMIT ${parseInt(limit)}
|
||||
`, args);
|
||||
|
||||
// Статистика по категориям
|
||||
const { rows: stats } = await query(`
|
||||
SELECT category,
|
||||
count(*)::int as total,
|
||||
count(*) FILTER (WHERE is_used=false)::int as unused
|
||||
FROM blog_topics GROUP BY category
|
||||
`);
|
||||
|
||||
res.json({ topics: rows, stats });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// POST /api/admin/blog-topics — добавить тему
|
||||
router.post('/blog-topics', async (req, res) => {
|
||||
if (!await requireAdmin(req, res)) return;
|
||||
const { category, topic, tags = [], priority = 5 } = req.body;
|
||||
if (!category || !topic) return res.status(400).json({ error: 'category и topic обязательны' });
|
||||
try {
|
||||
const { rows: [row] } = await query(
|
||||
`INSERT INTO blog_topics (category, topic, tags, priority, source)
|
||||
VALUES ($1,$2,$3,$4,'manual') ON CONFLICT DO NOTHING RETURNING *`,
|
||||
[category, topic.trim(), tags, priority]
|
||||
);
|
||||
res.json(row || { error: 'Такая тема уже есть' });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// DELETE /api/admin/blog-topics/:id
|
||||
router.delete('/blog-topics/:id', async (req, res) => {
|
||||
if (!await requireAdmin(req, res)) return;
|
||||
try {
|
||||
await query('DELETE FROM blog_topics WHERE id=$1', [req.params.id]);
|
||||
res.json({ ok: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// POST /api/admin/blog-topics/generate — AI генерация новых тем для категории
|
||||
router.post('/blog-topics/generate', async (req, res) => {
|
||||
if (!await requireAdmin(req, res)) return;
|
||||
const { category, count = 10 } = req.body;
|
||||
if (!category) return res.status(400).json({ error: 'category обязателен' });
|
||||
try {
|
||||
res.json({ ok: true, message: `Генерирую ${count} тем для ${category}...` });
|
||||
// Берём уже существующие темы для дедупликации
|
||||
const { rows: existing } = await query('SELECT topic FROM blog_topics WHERE category=$1', [category]);
|
||||
const existingTopics = existing.map(r => r.topic).join('\n');
|
||||
|
||||
const ai = require('../services/ai');
|
||||
const config = require('../config');
|
||||
const CATEGORY_NAMES = {
|
||||
'ai-tools': 'AI инструменты для работы и бизнеса',
|
||||
'ai-dev': 'AI разработка и программирование',
|
||||
'automation': 'Автоматизация процессов',
|
||||
'cybersec': 'Кибербезопасность',
|
||||
};
|
||||
|
||||
const system = `Ты редактор tech-блога. Генерируй темы для статей категории "${CATEGORY_NAMES[category] || category}".
|
||||
Темы должны быть: конкретными, практическими, интересными читателям.
|
||||
Формат: точные заголовки статей, не категории.
|
||||
Ответь ТОЛЬКО JSON-массивом строк без markdown.`;
|
||||
|
||||
const userMsg = `Придумай ${count} уникальных тем.${existingTopics ? `\n\nИзбегай повторений:\n${existingTopics.slice(0,800)}` : ''}`;
|
||||
|
||||
const result = await ai.chat(
|
||||
config.ai.models.topics || 'claude-haiku-4-5-20251001',
|
||||
system, userMsg, 0.9, 600
|
||||
);
|
||||
|
||||
const topics = JSON.parse(result.replace(/```json|```/g, '').trim());
|
||||
let added = 0;
|
||||
for (const topic of topics.slice(0, count)) {
|
||||
if (!topic?.trim()) continue;
|
||||
const { rows: [row] } = await query(
|
||||
`INSERT INTO blog_topics (category, topic, source)
|
||||
VALUES ($1,$2,'ai') ON CONFLICT DO NOTHING RETURNING id`,
|
||||
[category, topic.trim()]
|
||||
);
|
||||
if (row) added++;
|
||||
}
|
||||
console.log(`[BlogTopics] AI generated ${added} topics for ${category}`);
|
||||
} catch (err) { console.error(`[BlogTopics] generate error: ${err.message}`); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user