Files
DLE/backend/routes/pages.js

1462 lines
64 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/VC-HB3-Accelerator
*/
const express = require('express');
const router = express.Router();
const db = require('../db');
const fs = require('fs');
const path = require('path');
const multer = require('multer');
const vectorSearchClient = require('../services/vectorSearchClient');
const FIELDS_TO_EXCLUDE = ['image', 'tags'];
// Проверка и создание общей таблицы для всех админов
async function ensureAdminPagesTable(fields) {
fields = fields.filter(f => !FIELDS_TO_EXCLUDE.includes(f));
const tableName = `admin_pages_simple`;
// Получаем ключ шифрования
// Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
// Проверяем, есть ли таблица
const existsRes = await db.getQuery()(
`SELECT to_regclass($1) as exists`, [tableName]
);
if (!existsRes.rows[0].exists) {
// Формируем SQL для создания таблицы с зашифрованными полями
let columns = [
'id SERIAL PRIMARY KEY',
'author_address_encrypted TEXT NOT NULL', // Зашифрованный адрес автора
'created_at TIMESTAMP DEFAULT NOW()',
'updated_at TIMESTAMP DEFAULT NOW()'
];
for (const field of fields) {
columns.push(`"${field}_encrypted" TEXT`);
}
const sql = `CREATE TABLE ${tableName} (${columns.join(', ')})`;
await db.getQuery()(sql);
} else {
// Проверяем, есть ли все нужные столбцы, и добавляем недостающие
const colRes = await db.getQuery()(
`SELECT column_name FROM information_schema.columns WHERE table_name = $1`, [tableName]
);
const existingCols = colRes.rows.map(r => r.column_name);
// Добавляем поле author_address_encrypted если его нет
if (!existingCols.includes('author_address_encrypted')) {
await db.getQuery()(
`ALTER TABLE ${tableName} ADD COLUMN author_address_encrypted TEXT`
);
}
for (const field of fields) {
const encryptedField = `${field}_encrypted`;
if (!existingCols.includes(encryptedField)) {
await db.getQuery()(
`ALTER TABLE ${tableName} ADD COLUMN "${encryptedField}" TEXT`
);
}
}
}
return { tableName, encryptionKey };
}
// Конфигурация загрузки файлов для юридических документов
// Храним файлы там, откуда их раздаёт express.static('/uploads', path.join(__dirname, 'uploads'))
const uploadsRoot = path.join(__dirname, '..', 'uploads');
const legalDir = path.join(uploadsRoot, 'legal');
if (!fs.existsSync(legalDir)) {
try { fs.mkdirSync(legalDir, { recursive: true }); } catch (e) {}
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, legalDir);
},
filename: function (req, file, cb) {
const safeName = Date.now() + '-' + (file.originalname || 'file');
cb(null, safeName);
}
});
const upload = multer({ storage });
function stripHtml(html) {
if (!html) return '';
return String(html).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
}
// Создать страницу (только для админа)
router.post('/', upload.single('file'), async (req, res) => {
console.log('[pages] POST /: Начало обработки запроса на создание страницы');
try {
if (!req.session || !req.session.authenticated) {
console.log('[pages] POST /: Ошибка аутентификации - сессия не найдена');
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (!req.session.address) {
console.log('[pages] POST /: Ошибка - адрес кошелька не найден');
return res.status(403).json({ error: 'Требуется подключение кошелька' });
}
console.log('[pages] POST /: Проверка прав доступа для адреса:', req.session.address);
// Проверяем роль админа через токены в кошельке
const authService = require('../services/auth-service');
let userAccessLevel;
try {
userAccessLevel = await authService.getUserAccessLevel(req.session.address);
} catch (authError) {
console.error('[pages] POST /: Ошибка при проверке прав доступа:', authError);
if (authError.message && authError.message.includes('timeout exceeded')) {
return res.status(503).json({ error: 'Ошибка подключения к базе данных. Попробуйте позже.' });
}
throw authError;
}
if (!userAccessLevel.hasAccess) {
console.log('[pages] POST /: Доступ запрещен - недостаточно прав');
return res.status(403).json({ error: 'Only admin can create pages' });
}
console.log('[pages] POST /: Права доступа подтверждены, уровень:', userAccessLevel.level);
const authorAddress = req.session.address;
const tableName = `admin_pages_simple`;
// Собираем данные страницы
const bodyRaw = req.body || {};
// Обрабатываем required_permission: если это пустая строка или 'null', устанавливаем null
let requiredPermission = null;
if (bodyRaw.required_permission) {
const perm = String(bodyRaw.required_permission).trim();
requiredPermission = (perm && perm !== 'null' && perm !== '') ? perm : null;
}
// Обрабатываем JSON поля (seo, settings) - могут прийти как строка из FormData
let seoValue = null;
if (bodyRaw.seo) {
if (typeof bodyRaw.seo === 'string') {
try {
seoValue = JSON.parse(bodyRaw.seo);
} catch (e) {
seoValue = bodyRaw.seo.trim() ? bodyRaw.seo : null;
}
} else if (typeof bodyRaw.seo === 'object') {
seoValue = bodyRaw.seo;
}
}
let settingsValue = null;
if (bodyRaw.settings) {
if (typeof bodyRaw.settings === 'string') {
try {
settingsValue = JSON.parse(bodyRaw.settings);
} catch (e) {
settingsValue = bodyRaw.settings.trim() ? bodyRaw.settings : null;
}
} else if (typeof bodyRaw.settings === 'object') {
settingsValue = bodyRaw.settings;
}
}
const pageData = {
title: bodyRaw.title || '',
summary: bodyRaw.summary || '',
content: bodyRaw.content || '',
seo: seoValue,
status: bodyRaw.status || 'draft',
settings: settingsValue,
visibility: bodyRaw.visibility || 'public',
required_permission: requiredPermission,
format: bodyRaw.format || (req.file ? (req.file.mimetype?.startsWith('image/') ? 'image' : 'pdf') : 'html'),
mime_type: req.file ? (req.file.mimetype || null) : (bodyRaw.mime_type || (bodyRaw.format === 'html' ? 'text/html' : null)),
storage_type: req.file ? 'file' : (bodyRaw.storage_type || 'embedded'),
file_path: req.file ? path.join('/uploads', 'legal', path.basename(req.file.path)) : (bodyRaw.file_path || null),
size_bytes: req.file ? req.file.size : (bodyRaw.size_bytes || null),
checksum: bodyRaw.checksum || null,
// Нормализуем категорию: приводим к нижнему регистру для консистентности
category: (bodyRaw.category && String(bodyRaw.category).trim()) ? String(bodyRaw.category).trim().toLowerCase() : null,
// Обрабатываем category_id: может быть null или числом
category_id: (bodyRaw.category_id && bodyRaw.category_id !== 'null' && bodyRaw.category_id !== '')
? (() => { const parsed = parseInt(bodyRaw.category_id); return isNaN(parsed) ? null : parsed; })()
: null,
// Обрабатываем parent_id: может быть null или числом
parent_id: (bodyRaw.parent_id && bodyRaw.parent_id !== 'null' && bodyRaw.parent_id !== '')
? (() => { const parsed = parseInt(bodyRaw.parent_id); return isNaN(parsed) ? null : parsed; })()
: null,
// Обрабатываем order_index: должно быть числом
order_index: (bodyRaw.order_index && bodyRaw.order_index !== 'null' && bodyRaw.order_index !== '')
? (() => { const parsed = parseInt(bodyRaw.order_index); return isNaN(parsed) ? 0 : parsed; })()
: 0,
nav_path: bodyRaw.nav_path || null,
is_index_page: bodyRaw.is_index_page === true || bodyRaw.is_index_page === 'true'
};
console.log('[pages] POST /: Создание страницы, данные:', {
title: pageData.title,
visibility: pageData.visibility,
required_permission: pageData.required_permission,
status: pageData.status,
format: pageData.format
});
// Формируем SQL для вставки данных (включаем все поля, даже null)
// Фильтруем только undefined, null значения включаем (они допустимы в БД)
const dataEntries = Object.entries(pageData).filter(([k, v]) => {
// Исключаем только undefined, null и пустые строки для некоторых полей допустимы
return v !== undefined;
});
const colNames = ['author_address', ...dataEntries.map(([k]) => k)].join(', ');
const values = [authorAddress, ...dataEntries.map(([, v]) => v)];
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
const sql = `INSERT INTO ${tableName} (${colNames}) VALUES (${placeholders}) RETURNING *`;
console.log('[pages] POST /: SQL запрос:', sql.substring(0, 300) + '...');
console.log('[pages] POST /: Количество параметров:', values.length);
console.log('[pages] POST /: Колонки:', colNames);
console.log('[pages] POST /: Значения (первые 5):', values.slice(0, 5).map(v => v === null ? 'NULL' : (typeof v === 'string' ? v.substring(0, 50) : v)));
// Проверяем, что ответ еще не был отправлен перед запросом к БД
if (res.headersSent || res.destroyed) {
console.error('[pages] POST /: Ответ уже отправлен перед запросом к БД');
return;
}
console.log('[pages] POST /: Выполнение SQL запроса к БД...');
let rows;
try {
const result = await db.getQuery()(sql, values);
rows = result.rows;
console.log('[pages] POST /: SQL запрос выполнен успешно, создана страница с ID:', rows[0]?.id);
} catch (dbError) {
console.error('[pages] POST /: Ошибка БД при выполнении SQL:', dbError);
console.error('[pages] POST /: Код ошибки БД:', dbError.code);
console.error('[pages] POST /: Сообщение БД:', dbError.message);
console.error('[pages] POST /: Детали БД:', dbError.detail);
throw dbError;
}
const created = rows[0];
// Проверяем еще раз перед отправкой ответа
if (res.headersSent || res.destroyed) {
console.error('[pages] POST /: Ответ уже отправлен после запроса к БД');
return;
}
// Индексация выполняется ТОЛЬКО вручную через кнопку "Индекс" (POST /:id/reindex)
// Автоматическая индексация при создании отключена
res.json(created);
} catch (error) {
console.error('[pages] Ошибка при создании страницы:', error);
console.error('[pages] Стек ошибки:', error.stack);
console.error('[pages] Код ошибки:', error.code);
console.error('[pages] Сообщение ошибки:', error.message);
// Если ответ уже отправлен, не пытаемся отправлять ошибку
if (res.headersSent || res.destroyed) {
console.error('[pages] POST /: Ответ уже отправлен в catch блоке');
return;
}
// Определяем статус код на основе типа ошибки
let statusCode = 500;
let errorMessage = 'Ошибка при создании страницы';
if (error.message && error.message.includes('timeout exceeded when trying to connect')) {
statusCode = 503; // Service Unavailable
errorMessage = 'Ошибка подключения к базе данных. Попробуйте позже.';
} else if (error.code === '23505') { // PostgreSQL unique violation
statusCode = 409; // Conflict
errorMessage = 'Страница с такими данными уже существует';
} else if (error.code === '23502') { // PostgreSQL not null violation
statusCode = 400; // Bad Request
errorMessage = 'Отсутствует обязательное поле: ' + (error.column || 'неизвестно');
} else if (error.code === '42703') { // PostgreSQL undefined column
statusCode = 400; // Bad Request
errorMessage = 'Неверное поле в запросе: ' + (error.message || 'неизвестно');
} else if (error.message) {
errorMessage = error.message;
}
res.status(statusCode).json({
success: false,
error: errorMessage,
message: errorMessage
});
}
});
// Получить все страницы админов
router.get('/', async (req, res) => {
if (!req.session || !req.session.authenticated) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (!req.session.address) {
return res.status(403).json({ error: 'Требуется подключение кошелька' });
}
// Проверяем роль админа через токены в кошельке
const authService = require('../services/auth-service');
const userAccessLevel = await authService.getUserAccessLevel(req.session.address);
if (!userAccessLevel.hasAccess) {
return res.status(403).json({ error: 'Only admin can view pages' });
}
const tableName = `admin_pages_simple`;
// Получаем ключ шифрования
// Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
// Проверяем, есть ли таблица
const existsRes = await db.getQuery()(
`SELECT to_regclass($1) as exists`, [tableName]
);
if (!existsRes.rows[0].exists) return res.json([]);
// Получаем все страницы всех админов
const { rows } = await db.getQuery()(`
SELECT * FROM ${tableName}
ORDER BY created_at DESC
`);
res.json(rows);
});
// ========== РОУТЫ ДЛЯ КАТЕГОРИЙ (должны быть ПЕРЕД параметрическими роутами типа /:id) ==========
// Создать категорию
router.post('/categories', async (req, res) => {
if (!req.session || !req.session.authenticated) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (!req.session.address) {
return res.status(403).json({ error: 'Требуется подключение кошелька' });
}
const authService = require('../services/auth-service');
const userAccessLevel = await authService.getUserAccessLevel(req.session.address);
if (!userAccessLevel.hasAccess) {
return res.status(403).json({ error: 'Only admin can create categories' });
}
try {
const { name, display_name, description, order_index } = req.body;
if (!name || !name.trim()) {
return res.status(400).json({ error: 'Название категории обязательно' });
}
const normalizedName = name.trim().toLowerCase();
const displayName = display_name || name.trim();
// Проверяем, не существует ли уже категория с таким названием
const existsRes = await db.getQuery()(
`SELECT id FROM document_categories WHERE name = $1`,
[normalizedName]
);
if (existsRes.rows.length > 0) {
return res.status(409).json({ error: 'Категория с таким названием уже существует' });
}
// Создаем категорию
const { rows } = await db.getQuery()(
`INSERT INTO document_categories (name, display_name, description, order_index, author_address)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`,
[normalizedName, displayName, description || null, order_index || 0, req.session.address]
);
console.log(`[pages] POST /categories: создана категория "${normalizedName}"`);
res.json(rows[0]);
} catch (error) {
console.error('[pages] Ошибка создания категории:', error);
// Если таблица не существует, возвращаем успех (для обратной совместимости)
if (error.message.includes('does not exist') || error.message.includes('relation')) {
console.warn('[pages] Таблица document_categories не существует, пропускаем создание');
res.json({ name: req.body.name.trim().toLowerCase(), display_name: req.body.name.trim() });
} else {
res.status(500).json({ error: 'Ошибка создания категории: ' + error.message });
}
}
});
// Удалить категорию
router.delete('/categories/:name', async (req, res) => {
if (!req.session || !req.session.authenticated) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (!req.session.address) {
return res.status(403).json({ error: 'Требуется подключение кошелька' });
}
const authService = require('../services/auth-service');
const userAccessLevel = await authService.getUserAccessLevel(req.session.address);
if (!userAccessLevel.hasAccess) {
return res.status(403).json({ error: 'Only admin can delete categories' });
}
try {
const categoryName = decodeURIComponent(req.params.name).toLowerCase();
if (categoryName === 'uncategorized') {
return res.status(400).json({ error: 'Нельзя удалить категорию "Без категории"' });
}
// Удаляем категорию
const { rows } = await db.getQuery()(
`DELETE FROM document_categories WHERE name = $1 RETURNING *`,
[categoryName]
);
if (rows.length === 0) {
return res.status(404).json({ error: 'Категория не найдена' });
}
console.log(`[pages] DELETE /categories/:name: удалена категория "${categoryName}"`);
res.json({ success: true, deleted: rows[0] });
} catch (error) {
console.error('[pages] Ошибка удаления категории:', error);
// Если таблица не существует, возвращаем успех (для обратной совместимости)
if (error.message.includes('does not exist') || error.message.includes('relation')) {
console.warn('[pages] Таблица document_categories не существует, пропускаем удаление');
res.json({ success: true });
} else {
res.status(500).json({ error: 'Ошибка удаления категории: ' + error.message });
}
}
});
// Получить список всех категорий (для выпадающего списка)
router.get('/categories', async (req, res) => {
try {
// Сначала пытаемся получить категории из таблицы document_categories
try {
const categoriesRes = await db.getQuery()(
`SELECT name, display_name FROM document_categories ORDER BY order_index, created_at`
);
if (categoriesRes.rows.length > 0) {
const categories = categoriesRes.rows.map(row => row.name);
return res.json(categories);
}
} catch (err) {
console.warn('[pages] Таблица document_categories не существует, используем старый метод');
}
// Fallback: получаем категории из документов
const tableName = `admin_pages_simple`;
const existsRes = await db.getQuery()(
`SELECT to_regclass($1) as exists`, [tableName]
);
if (!existsRes.rows[0].exists) {
return res.json([]);
}
// Получаем уникальные категории из опубликованных публичных страниц
const { rows } = await db.getQuery()(`
SELECT DISTINCT category
FROM ${tableName}
WHERE visibility = 'public'
AND status = 'published'
AND category IS NOT NULL
AND category != ''
ORDER BY category ASC
`);
const categories = rows.map(row => row.category).filter(Boolean);
res.json(categories);
} catch (error) {
console.error('Ошибка получения категорий:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// ========== КОНЕЦ РОУТОВ ДЛЯ КАТЕГОРИЙ ==========
// Получить одну страницу по id (с проверкой прав доступа)
router.get('/:id', async (req, res) => {
try {
if (!req.session || !req.session.authenticated) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (!req.session.address) {
return res.status(403).json({ error: 'Требуется подключение кошелька' });
}
const tableName = `admin_pages_simple`;
const existsRes = await db.getQuery()(
`SELECT to_regclass($1) as exists`, [tableName]
);
if (!existsRes.rows[0].exists) return res.status(404).json({ error: 'Page table not found' });
const { rows } = await db.getQuery()(
`SELECT * FROM ${tableName} WHERE id = $1`,
[req.params.id]
);
if (!rows.length) return res.status(404).json({ error: 'Page not found' });
const page = rows[0];
// Проверяем доступ к странице в зависимости от её видимости
const authService = require('../services/auth-service');
const userAccessLevel = await authService.getUserAccessLevel(req.session.address);
const role = userAccessLevel.level; // 'user' | 'readonly' | 'editor'
// Публичные страницы доступны всем
if (page.visibility === 'public' && page.status === 'published') {
return res.json(page);
}
// Внутренние страницы требуют проверки прав
if (page.visibility === 'internal') {
// Редактор видит все внутренние страницы (включая черновики)
if (role === 'editor') {
return res.json(page);
}
// Обычные пользователи видят только опубликованные внутренние страницы
if (page.status !== 'published') {
return res.status(403).json({ error: 'Доступ запрещен: страница не опубликована' });
}
// Если у страницы указан required_permission, проверяем права пользователя
if (page.required_permission) {
const { PERMISSIONS, hasPermission } = require('../shared/permissions');
// Проверяем права доступа пользователя
// VIEW_BASIC_DOCS доступно всем аутентифицированным пользователям (user, readonly, editor)
// VIEW_LEGAL_DOCS требует readonly или editor
// MANAGE_LEGAL_DOCS требует editor
if (page.required_permission === PERMISSIONS.VIEW_LEGAL_DOCS && !hasPermission(role, PERMISSIONS.VIEW_LEGAL_DOCS)) {
return res.status(403).json({ error: 'Доступ запрещен: требуются права читателя' });
}
if (page.required_permission === PERMISSIONS.MANAGE_LEGAL_DOCS && !hasPermission(role, PERMISSIONS.MANAGE_LEGAL_DOCS)) {
return res.status(403).json({ error: 'Доступ запрещен: требуются права редактора' });
}
}
// Если required_permission не указан или права проверены, возвращаем страницу
return res.json(page);
}
// Для всех остальных случаев (например, draft публичных страниц) требуется роль редактора
if (role !== 'editor') {
return res.status(403).json({ error: 'Доступ запрещен: требуются права редактора' });
}
res.json(page);
} catch (error) {
console.error('[pages] Ошибка получения страницы:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Ручная переиндексация документа в vector-search (только для админа)
router.post('/:id/reindex', async (req, res) => {
if (!req.session || !req.session.authenticated) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (!req.session.address) {
return res.status(403).json({ error: 'Требуется подключение кошелька' });
}
// Проверяем роль админа через токены в кошельке
const authService = require('../services/auth-service');
const userAccessLevel = await authService.getUserAccessLevel(req.session.address);
if (!userAccessLevel.hasAccess) {
return res.status(403).json({ error: 'Only admin can reindex pages' });
}
const tableName = `admin_pages_simple`;
const existsRes = await db.getQuery()( `SELECT to_regclass($1) as exists`, [tableName] );
if (!existsRes.rows[0].exists) return res.status(404).json({ error: 'Page table not found' });
const { rows } = await db.getQuery()( `SELECT * FROM ${tableName} WHERE id = $1`, [req.params.id] );
if (!rows.length) return res.status(404).json({ error: 'Page not found' });
const page = rows[0];
if (page.format !== 'html') {
return res.status(422).json({ error: 'Индексация поддерживается только для HTML' });
}
const text = stripHtml(page.content || '');
if (!text) {
return res.status(422).json({ error: 'Пустое содержимое для индексации' });
}
try {
const url = page.visibility === 'public' && page.status === 'published'
? `/public/page/${page.id}`
: `/content/page/${page.id}`;
// Удаляем старые чанки документа перед реиндексацией
// Удаляем возможные чанки (doc_id_chunk_0, doc_id_chunk_1, ...) и сам документ (doc_id)
const oldRowIds = [String(page.id)]; // Удаляем основной документ
// Также удаляем возможные чанки (до 100 чанков на документ)
for (let i = 0; i < 100; i++) {
oldRowIds.push(`${page.id}_chunk_${i}`);
}
try {
await vectorSearchClient.remove('legal_docs', oldRowIds);
console.log(`[pages] Удалены старые чанки документа ${page.id} перед реиндексацией`);
} catch (removeError) {
console.warn(`[pages] Ошибка удаления старых чанков (продолжаем индексацию):`, removeError.message);
// Продолжаем индексацию даже если удаление не удалось
}
// Используем Semantic Chunking для разбивки документа
const semanticChunkingService = require('../services/semanticChunkingService');
const docLength = text.length;
const useLLM = docLength <= 8000;
const chunks = await semanticChunkingService.chunkDocument(text, {
maxChunkSize: 1500,
overlap: 200,
useLLM
});
// Индексируем каждый чанк отдельно
const rowsToUpsert = chunks.map((chunk, index) => ({
row_id: `${page.id}_chunk_${index}`,
text: chunk.text,
metadata: {
doc_id: page.id,
chunk_index: index,
section: chunk.metadata?.section || 'Документ',
parent_doc_id: page.id,
title: page.title,
url: `${url}#chunk_${index}`,
visibility: page.visibility,
required_permission: page.required_permission,
format: page.format,
updated_at: page.updated_at || null,
isComplete: chunk.metadata?.isComplete || false
}
}));
if (chunks.length > 1) {
console.log(`[pages] Документ ${page.id} разбит на ${chunks.length} чанков при реиндексации`);
await vectorSearchClient.upsert('legal_docs', rowsToUpsert);
} else {
// Если чанк один, индексируем как раньше
await vectorSearchClient.upsert('legal_docs', [{
row_id: page.id,
text,
metadata: {
doc_id: page.id,
title: page.title,
url,
visibility: page.visibility,
required_permission: page.required_permission,
format: page.format,
updated_at: page.updated_at || null
}
}]);
}
res.json({ success: true, chunksCount: chunks.length });
} catch (e) {
console.error('[pages] manual reindex error:', e.message);
res.status(500).json({ error: 'Ошибка индексации' });
}
});
// Редактировать страницу по id
router.patch('/:id', upload.single('file'), async (req, res) => {
console.log('[pages] PATCH /:id: Начало обработки запроса на обновление страницы ID:', req.params.id);
try {
if (!req.session || !req.session.authenticated) {
console.log('[pages] PATCH /:id: Ошибка аутентификации - сессия не найдена');
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (!req.session.address) {
console.log('[pages] PATCH /:id: Ошибка - адрес кошелька не найден');
return res.status(403).json({ error: 'Требуется подключение кошелька' });
}
console.log('[pages] PATCH /:id: Проверка прав доступа для адреса:', req.session.address);
// Проверяем роль админа через токены в кошельке
const authService = require('../services/auth-service');
let userAccessLevel;
try {
userAccessLevel = await authService.getUserAccessLevel(req.session.address);
} catch (authError) {
console.error('[pages] PATCH /:id: Ошибка при проверке прав доступа:', authError);
if (authError.message && authError.message.includes('timeout exceeded')) {
return res.status(503).json({ error: 'Ошибка подключения к базе данных. Попробуйте позже.' });
}
throw authError;
}
if (!userAccessLevel.hasAccess) {
console.log('[pages] PATCH /:id: Доступ запрещен - недостаточно прав');
return res.status(403).json({ error: 'Only admin can edit pages' });
}
console.log('[pages] PATCH /:id: Права доступа подтверждены, уровень:', userAccessLevel.level);
const tableName = `admin_pages_simple`;
const existsRes = await db.getQuery()(
`SELECT to_regclass($1) as exists`, [tableName]
);
if (!existsRes.rows[0].exists) return res.status(404).json({ error: 'Page table not found' });
const incoming = req.body || {};
const updateData = {};
console.log(`[pages] PATCH /:id (${req.params.id}): получены данные для обновления:`, JSON.stringify(incoming, null, 2));
console.log(`[pages] PATCH /:id (${req.params.id}): тип req.body:`, typeof req.body);
console.log(`[pages] PATCH /:id (${req.params.id}): ключи в req.body:`, Object.keys(incoming));
// Обрабатываем required_permission:
// Если visibility меняется на public, required_permission должен быть null
// Если visibility = internal и нет required_permission, устанавливаем null
if ('visibility' in incoming && incoming.visibility === 'public') {
updateData.required_permission = null;
} else if ('required_permission' in incoming) {
if (incoming.required_permission) {
const perm = String(incoming.required_permission).trim();
updateData.required_permission = (perm && perm !== 'null' && perm !== '') ? perm : null;
} else {
updateData.required_permission = null;
}
}
for (const [k, v] of Object.entries(incoming)) {
if (FIELDS_TO_EXCLUDE.includes(k)) continue;
if (k === 'required_permission') continue; // Уже обработано выше
// Нормализуем категорию: приводим к нижнему регистру для консистентности
if (k === 'category') {
updateData[k] = (v && String(v).trim()) ? String(v).trim().toLowerCase() : null;
}
// Обрабатываем category_id: может быть null или числом
else if (k === 'category_id') {
if (v === null || v === 'null' || v === '' || v === undefined) {
updateData[k] = null;
} else {
const parsed = parseInt(v);
updateData[k] = isNaN(parsed) ? null : parsed;
}
}
// Обрабатываем parent_id: может быть null или числом
else if (k === 'parent_id') {
if (v === null || v === 'null' || v === '' || v === undefined) {
updateData[k] = null;
} else {
const parsed = parseInt(v);
updateData[k] = isNaN(parsed) ? null : parsed;
}
}
// Обрабатываем order_index: должно быть числом
else if (k === 'order_index') {
if (v === null || v === 'null' || v === '' || v === undefined) {
updateData[k] = 0;
} else {
const parsed = parseInt(v);
updateData[k] = isNaN(parsed) ? 0 : parsed;
}
}
// Обрабатываем is_index_page: должно быть boolean
else if (k === 'is_index_page') {
updateData[k] = v === true || v === 'true' || v === 1 || v === '1';
}
// Обрабатываем JSON поля (seo, settings) - могут прийти как строка из FormData
else if (k === 'seo' || k === 'settings') {
if (typeof v === 'string') {
try {
// Если это строка JSON, пытаемся распарсить
const parsed = JSON.parse(v);
updateData[k] = parsed;
} catch (e) {
// Если не JSON, сохраняем как строку или null
updateData[k] = v && v.trim() ? v : null;
}
} else if (typeof v === 'object' && v !== null) {
// Если это уже объект, сериализуем в JSON
updateData[k] = v;
} else {
updateData[k] = v || null;
}
}
// Остальные поля
else {
updateData[k] = typeof v === 'object' && v !== null ? JSON.stringify(v) : v;
}
}
console.log(`[pages] PATCH /:id (${req.params.id}): обработанные данные для обновления:`, updateData);
if (req.file) {
updateData.format = req.file.mimetype?.startsWith('image/') ? 'image' : 'pdf';
updateData.mime_type = req.file.mimetype || null;
updateData.storage_type = 'file';
updateData.file_path = path.join('/uploads', 'legal', path.basename(req.file.path));
updateData.size_bytes = req.file.size;
}
const entries = Object.entries(updateData);
if (!entries.length) return res.status(400).json({ error: 'No fields to update' });
const setClause = entries.map(([f], i) => `"${f}" = $${i + 1}`).join(', ');
const values = entries.map(([, v]) => v);
values.push(req.params.id);
const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = NOW() WHERE id = $${entries.length + 1} RETURNING *`;
console.log(`[pages] PATCH /:id (${req.params.id}): SQL запрос:`, sql);
console.log(`[pages] PATCH /:id (${req.params.id}): значения:`, values);
// Проверяем, что ответ еще не был отправлен перед запросом к БД
if (res.headersSent || res.destroyed) {
console.error('[pages] PATCH /:id: Ответ уже отправлен перед запросом к БД');
return;
}
console.log('[pages] PATCH /:id: Выполнение SQL запроса к БД...');
let rows;
try {
const result = await db.getQuery()(sql, values);
rows = result.rows;
} catch (dbError) {
console.error('[pages] PATCH /:id: Ошибка БД при выполнении SQL:', dbError);
console.error('[pages] PATCH /:id: Код ошибки БД:', dbError.code);
console.error('[pages] PATCH /:id: Сообщение БД:', dbError.message);
console.error('[pages] PATCH /:id: Детали БД:', dbError.detail);
// Если ответ уже отправлен, не пытаемся отправлять ошибку
if (res.headersSent || res.destroyed) {
console.error('[pages] PATCH /:id: Ответ уже отправлен в catch блоке БД');
return;
}
// Определяем статус код на основе типа ошибки
let statusCode = 500;
let errorMessage = 'Ошибка при обновлении страницы';
if (dbError.message && dbError.message.includes('timeout exceeded when trying to connect')) {
statusCode = 503; // Service Unavailable
errorMessage = 'Ошибка подключения к базе данных. Попробуйте позже.';
} else if (dbError.code === '23505') { // PostgreSQL unique violation
statusCode = 409; // Conflict
errorMessage = 'Страница с такими данными уже существует';
} else if (dbError.code === '23502') { // PostgreSQL not null violation
statusCode = 400; // Bad Request
errorMessage = 'Отсутствует обязательное поле: ' + (dbError.column || 'неизвестно');
} else if (dbError.code === '42703') { // PostgreSQL undefined column
statusCode = 400; // Bad Request
errorMessage = 'Неверное поле в запросе: ' + (dbError.message || 'неизвестно');
} else if (dbError.message) {
errorMessage = dbError.message;
}
return res.status(statusCode).json({
success: false,
error: errorMessage,
message: errorMessage
});
}
if (!rows.length) {
console.error('[pages] PATCH /:id: Страница не найдена после обновления');
return res.status(404).json({ error: 'Page not found' });
}
const updated = rows[0];
console.log(`[pages] PATCH /:id (${req.params.id}): страница успешно обновлена:`, {
id: updated.id,
title: updated.title,
category: updated.category,
parent_id: updated.parent_id,
order_index: updated.order_index,
is_index_page: updated.is_index_page,
visibility: updated.visibility,
required_permission: updated.required_permission
});
// Проверяем еще раз перед отправкой ответа
if (res.headersSent || res.destroyed) {
console.error('[pages] PATCH /:id: Ответ уже отправлен после запроса к БД');
return;
}
// Индексация выполняется ТОЛЬКО вручную через кнопку "Индекс" (POST /:id/reindex)
// Автоматическая индексация при обновлении отключена
res.json(updated);
} catch (error) {
console.error('[pages] PATCH /:id: Ошибка при обновлении страницы:', error);
console.error('[pages] PATCH /:id: Стек ошибки:', error.stack);
console.error('[pages] PATCH /:id: Код ошибки:', error.code);
console.error('[pages] PATCH /:id: Сообщение ошибки:', error.message);
// Если ответ уже отправлен, не пытаемся отправлять ошибку
if (res.headersSent || res.destroyed) {
console.error('[pages] PATCH /:id: Ответ уже отправлен в catch блоке');
return;
}
// Определяем статус код на основе типа ошибки
let statusCode = 500;
let errorMessage = 'Ошибка при обновлении страницы';
if (error.message && error.message.includes('timeout exceeded when trying to connect')) {
statusCode = 503; // Service Unavailable
errorMessage = 'Ошибка подключения к базе данных. Попробуйте позже.';
} else if (error.code === '23505') { // PostgreSQL unique violation
statusCode = 409; // Conflict
errorMessage = 'Страница с такими данными уже существует';
} else if (error.message) {
errorMessage = error.message;
}
res.status(statusCode).json({
success: false,
error: errorMessage,
message: errorMessage
});
}
});
// Удалить страницу по id
router.delete('/:id', async (req, res) => {
if (!req.session || !req.session.authenticated) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (!req.session.address) {
return res.status(403).json({ error: 'Требуется подключение кошелька' });
}
// Проверяем роль админа через токены в кошельке
const authService = require('../services/auth-service');
const userAccessLevel = await authService.getUserAccessLevel(req.session.address);
if (!userAccessLevel.hasAccess) {
return res.status(403).json({ error: 'Only admin can delete pages' });
}
const tableName = `admin_pages_simple`;
const pageId = parseInt(req.params.id);
const existsRes = await db.getQuery()(
`SELECT to_regclass($1) as exists`, [tableName]
);
if (!existsRes.rows[0].exists) return res.status(404).json({ error: 'Page table not found' });
// Сначала получаем информацию о странице перед удалением
const pageResult = await db.getQuery()(
`SELECT * FROM ${tableName} WHERE id = $1`,
[pageId]
);
if (!pageResult.rows.length) return res.status(404).json({ error: 'Page not found' });
const pageToDelete = pageResult.rows[0];
// Находим все медиа-файлы, связанные с этой страницей
try {
const mediaResult = await db.getQuery()(
`SELECT id, file_hash FROM content_media WHERE page_id = $1`,
[pageId]
);
const deletedMediaCount = mediaResult.rows.length;
console.log(`[pages] Найдено ${deletedMediaCount} медиа-файлов, связанных со страницей ${pageId}`);
// Для каждого медиа-файла проверяем, используется ли он в других местах
for (const media of mediaResult.rows) {
if (media.file_hash) {
// Проверяем, сколько раз используется этот файл (по file_hash)
const usageResult = await db.getQuery()(
`SELECT COUNT(*) as count FROM content_media WHERE file_hash = $1`,
[media.file_hash]
);
const usageCount = parseInt(usageResult.rows[0].count);
// Если файл используется только один раз (только в этой странице), удаляем его полностью
if (usageCount === 1) {
await db.getQuery()(
`DELETE FROM content_media WHERE id = $1`,
[media.id]
);
console.log(`[pages] Удален медиа-файл ID ${media.id} (file_hash: ${media.file_hash}), использовался только в удаляемой странице`);
} else {
// Если файл используется в других местах, просто убираем связь со страницей
await db.getQuery()(
`UPDATE content_media SET page_id = NULL WHERE id = $1`,
[media.id]
);
console.log(`[pages] Убрана связь медиа-файла ID ${media.id} со страницей ${pageId} (файл используется в ${usageCount} местах)`);
}
} else {
// Если file_hash отсутствует, просто удаляем файл
await db.getQuery()(
`DELETE FROM content_media WHERE id = $1`,
[media.id]
);
console.log(`[pages] Удален медиа-файл ID ${media.id} (без file_hash)`);
}
}
if (deletedMediaCount > 0) {
console.log(`[pages] Обработано ${deletedMediaCount} медиа-файлов при удалении страницы ${pageId}`);
}
} catch (mediaError) {
console.error('[pages] Ошибка при удалении медиа-файлов:', mediaError);
// Продолжаем удаление страницы даже если произошла ошибка с медиа-файлами
}
// Удаляем страницу
const { rows } = await db.getQuery()(
`DELETE FROM ${tableName} WHERE id = $1 RETURNING *`,
[pageId]
);
if (!rows.length) return res.status(404).json({ error: 'Page not found' });
const deleted = rows[0];
// Удаляем из векторного поиска
try {
if (deleted && deleted.format === 'html') {
// Удаляем документ и все его чанки
const rowIdsToDelete = [String(deleted.id)]; // Основной документ
// Удаляем возможные чанки (до 100 чанков на документ)
for (let i = 0; i < 100; i++) {
rowIdsToDelete.push(`${deleted.id}_chunk_${i}`);
}
await vectorSearchClient.remove('legal_docs', rowIdsToDelete);
console.log(`[pages] Удалены документ ${deleted.id} и все его чанки из векторного поиска`);
}
} catch (e) {
console.error('[pages] vector remove error:', e.message);
}
res.json(deleted);
});
// Публичные маршруты для просмотра страниц (доступны всем пользователям)
// Получить все опубликованные страницы
router.get('/public/all', async (req, res) => {
try {
const tableName = `admin_pages_simple`;
// Проверяем, есть ли таблица
const existsRes = await db.getQuery()(
`SELECT to_regclass($1) as exists`, [tableName]
);
if (!existsRes.rows[0].exists) {
return res.json([]);
}
// Поддержка фильтрации по категории и родителю
const { category, parent_id, search } = req.query;
let whereClause = `WHERE visibility = 'public' AND status = 'published'`;
const params = [];
let paramIndex = 1;
if (category) {
whereClause += ` AND category = $${paramIndex}`;
params.push(category);
paramIndex++;
}
if (parent_id !== undefined) {
if (parent_id === null || parent_id === 'null' || parent_id === '') {
whereClause += ` AND parent_id IS NULL`;
} else {
const parsed = parseInt(parent_id);
if (!isNaN(parsed)) {
whereClause += ` AND parent_id = $${paramIndex}`;
params.push(parsed);
paramIndex++;
}
}
}
if (search) {
whereClause += ` AND (title ILIKE $${paramIndex} OR summary ILIKE $${paramIndex})`;
params.push(`%${search}%`);
paramIndex++;
}
// Сортировка: сначала по категории, затем по order_index, затем по created_at
const { rows } = await db.getQuery()(`
SELECT * FROM ${tableName}
${whereClause}
ORDER BY
COALESCE(category, '') ASC,
COALESCE(order_index, 0) ASC,
created_at DESC
`, params);
console.log(`[pages] GET /public/all: найдено ${rows.length} публичных документов`);
if (rows.length > 0) {
console.log(`[pages] Примеры документов:`, rows.slice(0, 3).map(r => ({ id: r.id, title: r.title, category: r.category })));
}
res.json(rows);
} catch (error) {
console.error('Ошибка получения публичных страниц:', error);
// Возвращаем пустой массив вместо объекта с ошибкой, чтобы фронтенд не ломался
console.error('[pages] GET /public/all: ошибка, возвращаем пустой массив');
res.status(500).json([]);
}
});
// Получить иерархическую структуру всех публичных страниц
router.get('/public/structure', async (req, res) => {
try {
const tableName = `admin_pages_simple`;
const existsRes = await db.getQuery()(
`SELECT to_regclass($1) as exists`, [tableName]
);
if (!existsRes.rows[0].exists) {
return res.json({ categories: [] });
}
// Получаем все опубликованные публичные страницы
const { rows } = await db.getQuery()(`
SELECT
id,
title,
summary,
category,
parent_id,
order_index,
nav_path,
is_index_page,
created_at
FROM ${tableName}
WHERE visibility = 'public' AND status = 'published'
ORDER BY
COALESCE(category, '') ASC,
COALESCE(order_index, 0) ASC,
created_at ASC
`);
// Группируем по категориям и строим иерархию
const categories = {};
const pagesById = {};
console.log(`[pages] GET /public/structure: найдено ${rows.length} страниц`);
rows.forEach(page => {
pagesById[page.id] = {
...page,
children: []
};
// Нормализуем категорию: приводим к нижнему регистру для консистентности
const cat = (page.category && String(page.category).trim())
? String(page.category).trim().toLowerCase()
: 'uncategorized';
if (!categories[cat]) {
categories[cat] = {
name: cat,
pages: []
};
}
});
// Строим иерархию
rows.forEach(page => {
const pageObj = pagesById[page.id];
if (page.parent_id && pagesById[page.parent_id]) {
// Дочерний документ - добавляем в children родителя
pagesById[page.parent_id].children.push(pageObj);
} else {
// Родительский документ (без parent_id) - добавляем в категорию
// Нормализуем категорию: приводим к нижнему регистру для консистентности
const cat = (page.category && String(page.category).trim())
? String(page.category).trim().toLowerCase()
: 'uncategorized';
// Убеждаемся, что категория существует (на случай, если она была создана только в первом цикле)
if (!categories[cat]) {
categories[cat] = {
name: cat,
pages: []
};
}
categories[cat].pages.push(pageObj);
}
});
// Сортируем children внутри каждого родительского документа
Object.values(categories).forEach(cat => {
cat.pages.forEach(page => {
if (page.children && Array.isArray(page.children) && page.children.length > 0) {
page.children.sort((a, b) => {
if (a.order_index !== b.order_index) {
return (a.order_index || 0) - (b.order_index || 0);
}
return new Date(a.created_at) - new Date(b.created_at);
});
}
});
});
// Загружаем все категории из таблицы document_categories (включая пустые)
try {
const categoriesRes = await db.getQuery()(
`SELECT name, display_name, description, order_index
FROM document_categories
ORDER BY order_index, created_at`
);
categoriesRes.rows.forEach(cat => {
const normalizedName = cat.name.toLowerCase();
if (!categories[normalizedName]) {
// Добавляем пустую категорию, если её нет в списке из документов
categories[normalizedName] = {
name: normalizedName,
pages: []
};
}
// Обновляем отображаемое название
if (cat.display_name) {
categories[normalizedName].display_name = cat.display_name;
}
});
} catch (err) {
console.warn('[pages] Ошибка загрузки категорий из document_categories (таблица может не существовать):', err.message);
}
console.log(`[pages] GET /public/structure: создано ${Object.keys(categories).length} категорий:`, Object.keys(categories));
// Сортируем страницы в категориях: сначала родительские (с детьми), потом остальные
Object.values(categories).forEach(cat => {
cat.pages.sort((a, b) => {
// Сначала документы с детьми (родительские)
const aHasChildren = a.children && Array.isArray(a.children) && a.children.length > 0;
const bHasChildren = b.children && Array.isArray(b.children) && b.children.length > 0;
// Если a имеет детей, а b нет - a идет первым (отрицательное значение = a перед b)
if (aHasChildren && !bHasChildren) return -1;
// Если b имеет детей, а a нет - b идет первым (положительное значение = b перед a)
if (!aHasChildren && bHasChildren) return 1;
// Если оба с детьми или оба без детей, сортируем по order_index и created_at
if (a.order_index !== b.order_index) {
return (a.order_index || 0) - (b.order_index || 0);
}
return new Date(a.created_at) - new Date(b.created_at);
});
// Логируем результат сортировки для отладки
console.log(`[pages] Категория "${cat.name}": порядок документов:`,
cat.pages.map(p => ({
id: p.id,
title: p.title,
hasChildren: p.children && Array.isArray(p.children) && p.children.length > 0,
childrenCount: p.children ? p.children.length : 0
}))
);
});
const result = {
categories: Object.values(categories),
totalPages: rows.length
};
// Логируем результат для отладки
console.log(`[pages] GET /public/structure: возвращаем ${result.categories.length} категорий`);
result.categories.forEach(cat => {
console.log(` - ${cat.name}: ${cat.pages.length} страниц`);
});
res.json(result);
} catch (error) {
console.error('Ошибка получения структуры страниц:', error);
// Возвращаем пустую структуру вместо объекта с ошибкой, чтобы фронтенд не ломался
console.error('[pages] GET /public/structure: ошибка, возвращаем пустую структуру');
res.status(500).json({ categories: [], totalPages: 0 });
}
});
// Получить навигацию для конкретного документа
router.get('/public/:id/navigation', async (req, res) => {
try {
const tableName = `admin_pages_simple`;
const pageId = parseInt(req.params.id);
const existsRes = await db.getQuery()(
`SELECT to_regclass($1) as exists`, [tableName]
);
if (!existsRes.rows[0].exists) {
return res.status(404).json({ error: 'Страница не найдена' });
}
// Получаем текущую страницу
const { rows: currentPage } = await db.getQuery()(`
SELECT * FROM ${tableName}
WHERE id = $1 AND visibility = 'public' AND status = 'published'
`, [pageId]);
if (currentPage.length === 0) {
return res.status(404).json({ error: 'Страница не найдена' });
}
const page = currentPage[0];
const category = page.category || null;
const parentId = page.parent_id || null;
// Получаем все страницы той же категории/родителя для навигации
let whereClause = `WHERE visibility = 'public' AND status = 'published'`;
const params = [];
let paramIndex = 1;
if (parentId) {
whereClause += ` AND parent_id = $${paramIndex}`;
params.push(parentId);
paramIndex++;
} else if (category) {
whereClause += ` AND category = $${paramIndex} AND (parent_id IS NULL OR parent_id = 0)`;
params.push(category);
paramIndex++;
} else {
whereClause += ` AND (category IS NULL OR category = '') AND (parent_id IS NULL OR parent_id = 0)`;
}
const { rows: siblings } = await db.getQuery()(`
SELECT id, title, nav_path, order_index
FROM ${tableName}
${whereClause}
ORDER BY COALESCE(order_index, 0) ASC, created_at ASC
`, params);
// Находим текущую страницу в списке
const currentIndex = siblings.findIndex(p => p.id === pageId);
const prevPage = currentIndex > 0 ? siblings[currentIndex - 1] : null;
const nextPage = currentIndex < siblings.length - 1 ? siblings[currentIndex + 1] : null;
// Получаем родительскую страницу
let parentPage = null;
if (parentId) {
const { rows: parent } = await db.getQuery()(`
SELECT id, title, nav_path FROM ${tableName}
WHERE id = $1 AND visibility = 'public' AND status = 'published'
`, [parentId]);
if (parent.length > 0) {
parentPage = parent[0];
}
}
// Формируем breadcrumbs
const breadcrumbs = [];
if (category) {
breadcrumbs.push({ name: category, path: null });
}
if (parentPage) {
breadcrumbs.push({ name: parentPage.title, path: `/content/published/${parentPage.id}` });
}
breadcrumbs.push({ name: page.title, path: null });
res.json({
previous: prevPage ? {
id: prevPage.id,
title: prevPage.title,
path: `/content/published/${prevPage.id}`
} : null,
next: nextPage ? {
id: nextPage.id,
title: nextPage.title,
path: `/content/published/${nextPage.id}`
} : null,
parent: parentPage ? {
id: parentPage.id,
title: parentPage.title,
path: `/content/published/${parentPage.id}`
} : null,
breadcrumbs
});
} catch (error) {
console.error('Ошибка получения навигации:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Внутренние документы (доступны всем аутентифицированным пользователям с подключенным кошельком)
router.get('/internal/all', async (req, res) => {
try {
if (!req.session || !req.session.authenticated) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (!req.session.address) {
return res.status(403).json({ error: 'Требуется подключение кошелька' });
}
const tableName = `admin_pages_simple`;
const existsRes = await db.getQuery()( `SELECT to_regclass($1) as exists`, [tableName] );
if (!existsRes.rows[0].exists) {
return res.json([]);
}
const authService = require('../services/auth-service');
const userAccessLevel = await authService.getUserAccessLevel(req.session.address);
// Все аутентифицированные пользователи с подключенным кошельком могут видеть внутренние страницы
// EDITOR может видеть все (включая черновики), обычные пользователи - только опубликованные
const role = userAccessLevel.level; // 'user' | 'readonly' | 'editor'
let sql;
if (role === 'editor') {
// Редактор видит все внутренние страницы, включая черновики
sql = `SELECT * FROM ${tableName} WHERE visibility = 'internal' ORDER BY created_at DESC`;
} else {
// Обычные пользователи видят только опубликованные внутренние страницы
sql = `SELECT * FROM ${tableName} WHERE visibility = 'internal' AND status = 'published' ORDER BY created_at DESC`;
}
const { rows } = await db.getQuery()(sql);
res.json(rows);
} catch (error) {
console.error('Ошибка получения внутренних страниц:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Получить одну опубликованную страницу по id
router.get('/public/:id', async (req, res) => {
try {
const tableName = `admin_pages_simple`;
// Проверяем, есть ли таблица
const existsRes = await db.getQuery()(
`SELECT to_regclass($1) as exists`, [tableName]
);
if (!existsRes.rows[0].exists) {
return res.status(404).json({ error: 'Страница не найдена или не опубликована' });
}
// Ищем страницу среди всех админов
const { rows } = await db.getQuery()(`
SELECT * FROM ${tableName}
WHERE id = $1 AND status = 'published'
`, [req.params.id]);
if (rows.length > 0) {
return res.json(rows[0]);
}
res.status(404).json({ error: 'Страница не найдена или не опубликована' });
} catch (error) {
console.error('Ошибка получения публичной страницы:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
module.exports = router;