feat(blog-topics): PATCH endpoint + soften requireAdmin + dynamic category name
- requireAdmin softened: doesn't require x-user-id when users.is_admin column is absent (same pattern as /api/admin/zero). Auth still enforced via the global x-internal-secret middleware. - /api/admin/blog-topics/:id now supports PATCH (topic/tags/priority/is_used). - /api/admin/blog-topics/generate reads category name from categories table instead of using a hardcoded 4-entry map, so AI-generation works for any user-created category.
This commit is contained in:
+44
-11
@@ -9,11 +9,19 @@ const { query } = require('../config/db');
|
|||||||
function uid(req) { return req.headers['x-user-id'] ? parseInt(req.headers['x-user-id']) : null; }
|
function uid(req) { return req.headers['x-user-id'] ? parseInt(req.headers['x-user-id']) : null; }
|
||||||
|
|
||||||
async function requireAdmin(req, res) {
|
async function requireAdmin(req, res) {
|
||||||
|
// x-internal-secret уже проверен глобальным middleware (см. index.js).
|
||||||
|
// Опционально — если есть users.is_admin (multi-user окружение, как на dev2) — проверим;
|
||||||
|
// если колонки нет (минимальный prod), доверяем секрету.
|
||||||
const adminId = uid(req);
|
const adminId = uid(req);
|
||||||
if (!adminId) { res.status(401).json({ error: 'x-user-id required' }); return null; }
|
if (!adminId) return 'system';
|
||||||
const { rows: [u] } = await query('SELECT is_admin FROM users WHERE id=$1', [adminId]);
|
try {
|
||||||
if (!u?.is_admin) { res.status(403).json({ error: 'Forbidden' }); return null; }
|
const { rows: [u] } = await query('SELECT is_admin FROM users WHERE id=$1', [adminId]);
|
||||||
return adminId;
|
if (u && u.is_admin === false) { res.status(403).json({ error: 'Forbidden' }); return null; }
|
||||||
|
return adminId;
|
||||||
|
} catch (err) {
|
||||||
|
// колонки is_admin нет — это нормально для prod конфига
|
||||||
|
return adminId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/admin/dashboard
|
// GET /api/admin/dashboard
|
||||||
@@ -559,6 +567,29 @@ router.post('/blog-topics', async (req, res) => {
|
|||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// PATCH /api/admin/blog-topics/:id — обновить тему (topic, tags, priority, is_used)
|
||||||
|
router.patch('/blog-topics/:id', async (req, res) => {
|
||||||
|
if (!await requireAdmin(req, res)) return;
|
||||||
|
const { topic, tags, priority, is_used } = req.body || {};
|
||||||
|
const fields = [];
|
||||||
|
const vals = [];
|
||||||
|
if (topic !== undefined) { fields.push(`topic=$${fields.length+1}`); vals.push(String(topic).trim()); }
|
||||||
|
if (tags !== undefined) { fields.push(`tags=$${fields.length+1}`); vals.push(Array.isArray(tags) ? tags : []); }
|
||||||
|
if (priority !== undefined) { fields.push(`priority=$${fields.length+1}`); vals.push(parseInt(priority, 10) || 5); }
|
||||||
|
if (is_used !== undefined) { fields.push(`is_used=$${fields.length+1}`); vals.push(!!is_used); }
|
||||||
|
if (!fields.length) return res.status(400).json({ error: 'нечего обновлять' });
|
||||||
|
vals.push(parseInt(req.params.id, 10));
|
||||||
|
try {
|
||||||
|
const { rows: [row] } = await query(
|
||||||
|
`UPDATE blog_topics SET ${fields.join(', ')} WHERE id=$${vals.length} RETURNING *`,
|
||||||
|
vals
|
||||||
|
);
|
||||||
|
if (!row) return res.status(404).json({ error: 'не найдено' });
|
||||||
|
res.json({ ok: true, topic: row });
|
||||||
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
|
});
|
||||||
|
|
||||||
// DELETE /api/admin/blog-topics/:id
|
// DELETE /api/admin/blog-topics/:id
|
||||||
router.delete('/blog-topics/:id', async (req, res) => {
|
router.delete('/blog-topics/:id', async (req, res) => {
|
||||||
if (!await requireAdmin(req, res)) return;
|
if (!await requireAdmin(req, res)) return;
|
||||||
@@ -581,14 +612,16 @@ router.post('/blog-topics/generate', async (req, res) => {
|
|||||||
|
|
||||||
const ai = require('../services/ai');
|
const ai = require('../services/ai');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const CATEGORY_NAMES = {
|
|
||||||
'ai-tools': 'AI инструменты для работы и бизнеса',
|
|
||||||
'ai-dev': 'AI разработка и программирование',
|
|
||||||
'automation': 'Автоматизация процессов',
|
|
||||||
'cybersec': 'Кибербезопасность',
|
|
||||||
};
|
|
||||||
|
|
||||||
const system = `Ты редактор tech-блога. Генерируй темы для статей категории "${CATEGORY_NAMES[category] || category}".
|
// Имя и описание категории берём из БД (а не hardcoded)
|
||||||
|
const { rows: [catRow] } = await query(
|
||||||
|
'SELECT name, description FROM categories WHERE slug=$1',
|
||||||
|
[category]
|
||||||
|
);
|
||||||
|
const catName = catRow?.name || category;
|
||||||
|
const catDescr = catRow?.description ? ` (${catRow.description})` : '';
|
||||||
|
|
||||||
|
const system = `Ты редактор tech-блога. Генерируй темы для статей категории "${catName}"${catDescr}.
|
||||||
Темы должны быть: конкретными, практическими, интересными читателям.
|
Темы должны быть: конкретными, практическими, интересными читателям.
|
||||||
Формат: точные заголовки статей, не категории.
|
Формат: точные заголовки статей, не категории.
|
||||||
Ответь ТОЛЬКО JSON-массивом строк без markdown.`;
|
Ответь ТОЛЬКО JSON-массивом строк без markdown.`;
|
||||||
|
|||||||
Reference in New Issue
Block a user