From 918da882d2cfaa2c4844679aa0252855cb19fa00 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 23 Oct 2025 13:53:44 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 7 +- backend/routes/pages.js | 261 +++++++++- backend/scripts/seed/legalTemplatesSeed.js | 227 ++++++++ frontend/src/router/index.js | 20 +- frontend/src/services/pagesService.js | 9 +- frontend/src/views/ContentPageView.vue | 244 +++++++-- .../src/views/content/ContentListView.vue | 296 +++-------- .../src/views/content/ContentSettingsView.vue | 44 +- .../src/views/content/InternalListView.vue | 164 ++++++ frontend/src/views/content/PageEditView.vue | 484 ------------------ frontend/src/views/content/PageView.vue | 42 +- frontend/src/views/content/PublicPageView.vue | 36 +- .../src/views/content/PublishedListView.vue | 146 ++++++ .../src/views/content/TemplatesListView.vue | 151 ++++++ shared/permissions.js | 23 +- 15 files changed, 1327 insertions(+), 827 deletions(-) create mode 100644 backend/scripts/seed/legalTemplatesSeed.js create mode 100644 frontend/src/views/content/InternalListView.vue delete mode 100644 frontend/src/views/content/PageEditView.vue create mode 100644 frontend/src/views/content/PublishedListView.vue create mode 100644 frontend/src/views/content/TemplatesListView.vue diff --git a/backend/package.json b/backend/package.json index ec62284..e48c4b4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,7 +25,12 @@ "deploy:modules": "node scripts/deploy/deploy-modules.js", "generate:abi": "node scripts/generate-abi.js", "generate:flattened": "node scripts/generate-flattened.js", - "compile:full": "npx hardhat compile && npm run generate:abi && npm run generate:flattened" + "compile:full": "npx hardhat compile && npm run generate:abi && npm run generate:flattened", + "seed:legal": "node scripts/seed/legalTemplatesSeed.js" + }, + "bin": {}, + "engines": { + "node": ">=18" }, "dependencies": { "@anthropic-ai/sdk": "^0.51.0", diff --git a/backend/routes/pages.js b/backend/routes/pages.js index c8be1cd..4b0f624 100644 --- a/backend/routes/pages.js +++ b/backend/routes/pages.js @@ -13,6 +13,10 @@ 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']; @@ -70,8 +74,31 @@ async function ensureAdminPagesTable(fields) { 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('/', async (req, res) => { +router.post('/', upload.single('file'), async (req, res) => { if (!req.session || !req.session.authenticated) { return res.status(401).json({ error: 'Требуется аутентификация' }); } @@ -87,20 +114,65 @@ router.post('/', async (req, res) => { } const authorAddress = req.session.address; - const fields = Object.keys(req.body).filter(f => !FIELDS_TO_EXCLUDE.includes(f)); const tableName = `admin_pages_simple`; - // Формируем SQL для вставки данных - const colNames = ['author_address', ...fields].join(', '); - const values = [authorAddress, ...fields.map(f => { - const value = typeof req.body[f] === 'object' ? JSON.stringify(req.body[f]) : req.body[f]; - return value || ''; - })]; + // Собираем данные страницы + const bodyRaw = req.body || {}; + const pageData = { + title: bodyRaw.title || '', + summary: bodyRaw.summary || '', + content: bodyRaw.content || '', + seo: bodyRaw.seo ? (typeof bodyRaw.seo === 'string' ? bodyRaw.seo : JSON.stringify(bodyRaw.seo)) : null, + status: bodyRaw.status || 'draft', + settings: bodyRaw.settings ? (typeof bodyRaw.settings === 'string' ? bodyRaw.settings : JSON.stringify(bodyRaw.settings)) : null, + visibility: bodyRaw.visibility || 'public', + required_permission: bodyRaw.required_permission || null, + 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 + }; + + // Формируем SQL для вставки данных (только непустые поля) + const dataEntries = Object.entries(pageData).filter(([, v]) => 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 *`; const { rows } = await db.getQuery()(sql, values); - res.json(rows[0]); + const created = rows[0]; + + // Индексация в vector-search (только для HTML, если есть текст) + try { + if (created && (created.format === 'html' || pageData.format === 'html')) { + const text = stripHtml(created.content || pageData.content || ''); + if (text && text.length > 0) { + const url = created.visibility === 'public' && created.status === 'published' + ? `/public/page/${created.id}` + : `/content/page/${created.id}`; + await vectorSearchClient.upsert('legal_docs', [{ + row_id: created.id, + text, + metadata: { + doc_id: created.id, + title: created.title, + url, + visibility: created.visibility || pageData.visibility, + required_permission: created.required_permission || pageData.required_permission, + format: created.format || pageData.format, + updated_at: created.updated_at || null + } + }]); + } + } + } catch (e) { + console.error('[pages] vector upsert error:', e.message); + } + + res.json(created); }); // Получить все страницы админов @@ -171,8 +243,65 @@ router.get('/:id', async (req, res) => { res.json(rows[0]); }); +// Ручная переиндексация документа в 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}`; + 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 }); + } catch (e) { + console.error('[pages] manual reindex error:', e.message); + res.status(500).json({ error: 'Ошибка индексации' }); + } +}); + // Редактировать страницу по id -router.patch('/:id', async (req, res) => { +router.patch('/:id', upload.single('file'), async (req, res) => { if (!req.session || !req.session.authenticated) { return res.status(401).json({ error: 'Требуется аутентификация' }); } @@ -193,22 +322,58 @@ router.patch('/:id', async (req, res) => { ); if (!existsRes.rows[0].exists) return res.status(404).json({ error: 'Page table not found' }); - const fields = Object.keys(req.body).filter(f => !FIELDS_TO_EXCLUDE.includes(f)); - if (!fields.length) return res.status(400).json({ error: 'No fields to update' }); - - const filteredBody = {}; - fields.forEach(f => { - // Преобразуем объекты в JSON строки - filteredBody[f] = typeof req.body[f] === 'object' ? JSON.stringify(req.body[f]) : req.body[f]; - }); - const setClause = fields.map((f, i) => `"${f}" = $${i + 1}`).join(', '); - const values = Object.values(filteredBody); + const incoming = req.body || {}; + const updateData = {}; + for (const [k, v] of Object.entries(incoming)) { + if (FIELDS_TO_EXCLUDE.includes(k)) continue; + updateData[k] = typeof v === 'object' ? JSON.stringify(v) : v; + } + 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 = $${fields.length + 1} RETURNING *`; + + const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = NOW() WHERE id = $${entries.length + 1} RETURNING *`; const { rows } = await db.getQuery()(sql, values); if (!rows.length) return res.status(404).json({ error: 'Page not found' }); - res.json(rows[0]); + const updated = rows[0]; + + // Индексация для HTML + try { + if (updated && (updated.format === 'html')) { + const text = stripHtml(updated.content || ''); + if (text) { + const url = updated.visibility === 'public' && updated.status === 'published' + ? `/public/page/${updated.id}` + : `/content/page/${updated.id}`; + await vectorSearchClient.upsert('legal_docs', [{ + row_id: updated.id, + text, + metadata: { + doc_id: updated.id, + title: updated.title, + url, + visibility: updated.visibility, + required_permission: updated.required_permission, + format: updated.format, + updated_at: updated.updated_at || null + } + }]); + } + } + } catch (e) { + console.error('[pages] vector upsert (update) error:', e.message); + } + + res.json(updated); }); // Удалить страницу по id @@ -238,7 +403,15 @@ router.delete('/:id', async (req, res) => { [req.params.id] ); if (!rows.length) return res.status(404).json({ error: 'Page not found' }); - res.json(rows[0]); + const deleted = rows[0]; + try { + if (deleted && deleted.format === 'html') { + await vectorSearchClient.remove('legal_docs', [deleted.id]); + } + } catch (e) { + console.error('[pages] vector remove error:', e.message); + } + res.json(deleted); }); // Публичные маршруты для просмотра страниц (доступны всем пользователям) @@ -270,6 +443,44 @@ router.get('/public/all', async (req, res) => { } }); +// Внутренние документы (доступны аутентифицированным пользователям с доступом) +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); + if (!userAccessLevel.hasAccess) { + return res.status(403).json({ error: 'Only internal users can view pages' }); + } + + // READONLY/EDITOR видят внутренние опубликованные; EDITOR может видеть и черновики + const role = userAccessLevel.level; // '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 { diff --git a/backend/scripts/seed/legalTemplatesSeed.js b/backend/scripts/seed/legalTemplatesSeed.js new file mode 100644 index 0000000..7eeddbc --- /dev/null +++ b/backend/scripts/seed/legalTemplatesSeed.js @@ -0,0 +1,227 @@ +/** + * Seed системных юридических шаблонов (РКН-2025) в admin_pages_simple + * - Добавляет недостающие колонки (visibility, format и пр.) + * - Создает шаблоны с is_system_template = true и author_address = NULL + * - Повторный запуск — идемпотентен (по title + is_system_template) + */ + +const db = require('../../db'); + +async function getExistingColumns(tableName) { + const res = await db.getQuery()( + `SELECT column_name FROM information_schema.columns WHERE table_name = $1`, + [tableName] + ); + return res.rows.map(r => r.column_name); +} + +async function ensureTable(tableName) { + const existsRes = await db.getQuery()( + `SELECT to_regclass($1) as exists`, + [tableName] + ); + if (!existsRes.rows[0].exists) { + await db.getQuery()(` + CREATE TABLE ${tableName} ( + id SERIAL PRIMARY KEY, + author_address TEXT NULL, + title TEXT, + summary TEXT, + content TEXT, + seo JSONB, + status TEXT, + visibility TEXT, + required_permission TEXT, + format TEXT, + mime_type TEXT, + storage_type TEXT, + file_path TEXT, + size_bytes BIGINT, + checksum TEXT, + is_system_template BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + `); + } +} + +async function ensureColumns(tableName) { + const needed = { + author_address: 'TEXT', + title: 'TEXT', + summary: 'TEXT', + content: 'TEXT', + seo: 'JSONB', + status: 'TEXT', + visibility: 'TEXT', + required_permission: 'TEXT', + format: 'TEXT', + mime_type: 'TEXT', + storage_type: 'TEXT', + file_path: 'TEXT', + size_bytes: 'BIGINT', + checksum: 'TEXT', + is_system_template: 'BOOLEAN DEFAULT FALSE', + created_at: 'TIMESTAMP DEFAULT NOW()', + updated_at: 'TIMESTAMP DEFAULT NOW()' + }; + + const existing = await getExistingColumns(tableName); + for (const [col, type] of Object.entries(needed)) { + if (!existing.includes(col)) { + await db.getQuery()(`ALTER TABLE ${tableName} ADD COLUMN ${col} ${type}`); + } + } +} + +function htmlEscape(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>'); +} + +function tpl(content) { + // Лаконичный, «человеческий» текст с минимальными inline‑плейсхолдерами + return ` +

${htmlEscape(content.title)}

+

+ Настоящий документ предназначен для использования в рамках деятельности + {{company_name}} по адресу {{company_address}} и подлежит персонализации редактором. +

+ +

+ Ответственное лицо за вопросы персональных данных: {{responsible_person}} + ({{privacy_email}}, {{privacy_phone}}). +

+ +

+ Дата версии: {{date}} · Юрисдикция: {{jurisdiction}} · Язык: {{language}} +

+ +

+ Ниже приведён текст шаблона. Перед публикацией проверьте корректность реквизитов, + правовых оснований и сроков хранения данных. +

+ +${content.body || ''} +`; +} + +function doc(title, summary, visibility = 'public', requiredPermission = null) { + return { + title, + summary, + content: tpl({ title, visibility }), + seo: { title, description: summary, keywords: 'ПДн, политика, согласие' }, + status: 'draft', + visibility, + required_permission: requiredPermission, + format: 'html', + mime_type: 'text/html', + storage_type: 'embedded', + is_system_template: true + }; +} + +async function upsertTemplate(tableName, template) { + const exists = await db.getQuery()( + `SELECT id FROM ${tableName} WHERE title = $1 AND is_system_template = TRUE LIMIT 1`, + [template.title] + ); + if (exists.rows.length > 0) { + // Обновляем основные поля, не трогая author_address + const sql = `UPDATE ${tableName} + SET summary = $2, content = $3, seo = $4, status = $5, visibility = $6, + required_permission = $7, format = $8, mime_type = $9, storage_type = $10, + updated_at = NOW() + WHERE id = $1`; + await db.getQuery()(sql, [ + exists.rows[0].id, + template.summary, + template.content, + JSON.stringify(template.seo || {}), + template.status, + template.visibility, + template.required_permission, + template.format, + template.mime_type, + template.storage_type + ]); + return { updated: 1, inserted: 0 }; + } + + const sql = `INSERT INTO ${tableName} + (author_address, title, summary, content, seo, status, visibility, required_permission, format, mime_type, storage_type, is_system_template) + VALUES (NULL, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, TRUE)`; + await db.getQuery()(sql, [ + template.title, + template.summary, + template.content, + JSON.stringify(template.seo || {}), + template.status, + template.visibility, + template.required_permission, + template.format, + template.mime_type, + template.storage_type + ]); + return { updated: 0, inserted: 1 }; +} + +async function main() { + const tableName = 'admin_pages_simple'; + await ensureTable(tableName); + await ensureColumns(tableName); + + const publicDocs = [ + doc('Политика в отношении обработки персональных данных', 'Публичная политика обработки ПДн для пользователей.', 'public'), + doc('Политика конфиденциальности', 'Публичная политика конфиденциальности сервиса.', 'public'), + doc('Согласие на обработку персональных данных', 'Шаблон пользовательского согласия на обработку ПДн.', 'public'), + doc('Согласие на использование файлов cookie', 'Шаблон согласия на использование cookie по категориям.', 'public'), + doc('Согласие на трансграничную передачу ПДн', 'Шаблон согласия на трансграничную передачу ПДн.', 'public'), + doc('Согласие на обработку биометрических ПДн', 'Шаблон согласия на обработку биометрических ПДн.', 'public'), + doc('Права субъектов ПДн и отзыв согласия', 'Информация о правах субъектов ПДн и форма отзыва согласия.', 'public') + ]; + + const internalPermView = 'view_legal_docs'; + const internalDocs = [ + doc('Приказ о назначении ответственного за ПДн', 'Внутренний приказ о назначении ответственного.', 'internal', internalPermView), + doc('Должностная инструкция ответственного за ПДн', 'Обязанности и полномочия ответственного.', 'internal', internalPermView), + doc('Положение об обработке и защите ПДн', 'Локальный акт об обработке и защите ПДн.', 'internal', internalPermView), + doc('Регламент обращений субъектов ПДн', 'Порядок рассмотрения обращений субъектов.', 'internal', internalPermView), + doc('Регламент исполнения запросов субъектов', 'Доступ, исправление, удаление, ограничение.', 'internal', internalPermView), + doc('Политика хранения и уничтожения ПДн', 'Сроки хранения и процедуры уничтожения ПДн.', 'internal', internalPermView), + doc('Политика разграничения доступа к ПДн', 'Матрица ролей, уровни доступа.', 'internal', internalPermView), + doc('Перечень допущенных лиц и НДА', 'Список сотрудников/подрядчиков и обязательства о НДА.', 'internal', internalPermView), + doc('Шаблон DPA (поручение обработки ПДн)', 'Условия поручения обработки ПДн процессорам.', 'internal', internalPermView), + doc('Реестр операций по обработке ПДн', 'Цели, категории, сроки хранения, основания.', 'internal', internalPermView), + doc('Журналы учетов и инцидентов', 'Журналы доступа/операций и безопасности.', 'internal', internalPermView), + doc('Перечень и описание ИСПДн', 'Состав ИСПДн, типы и классификация.', 'internal', internalPermView), + doc('Модель угроз и меры защиты', 'Актуальная модель угроз и меры защиты.', 'internal', internalPermView), + doc('План обеспечения безопасности ПДн', 'Мероприятия по обеспечению безопасности ПДн.', 'internal', internalPermView), + doc('Регламент реагирования на инциденты', 'Порядок реагирования и план восстановления.', 'internal', internalPermView), + doc('Программа обучения и журнал инструктажей', 'Программа обучения и учет инструктажей.', 'internal', internalPermView), + doc('Уведомление РКН об обработке ПДн (шаблон)', 'Шаблон уведомления РКН об обработке ПДн.', 'internal', internalPermView), + doc('Процедуры трансграничной передачи ПДн', 'Порядок и уведомления для трансграничной передачи.', 'internal', internalPermView), + doc('Согласие ребенка/законного представителя', 'Шаблон согласия для несовершеннолетних.', 'internal', internalPermView), + doc('Политика работы с cookie и сторонними сервисами', 'Регламент для cookie/аналитики/рекламы.', 'internal', internalPermView) + ]; + + let inserted = 0, updated = 0; + for (const t of [...publicDocs, ...internalDocs]) { + const res = await upsertTemplate(tableName, t); + inserted += res.inserted; + updated += res.updated; + } + + console.log(`[seed:legal] completed. inserted=${inserted}, updated=${updated}`); +} + +main().then(() => process.exit(0)).catch(err => { + console.error('[seed:legal] error:', err); + process.exit(1); +}); + + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 0f6e687..ac0350c 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -193,6 +193,21 @@ const routes = [ name: 'content-list', component: () => import('../views/content/ContentListView.vue'), }, + { + path: '/content/templates', + name: 'content-templates', + component: () => import('../views/content/TemplatesListView.vue'), + }, + { + path: '/content/published', + name: 'content-published', + component: () => import('../views/content/PublishedListView.vue'), + }, + { + path: '/content/internal', + name: 'content-internal', + component: () => import('../views/content/InternalListView.vue'), + }, { path: '/content/create', name: 'content-create', @@ -208,11 +223,6 @@ const routes = [ name: 'page-view', component: () => import('../views/content/PageView.vue'), }, - { - path: '/content/page/:id/edit', - name: 'page-edit', - component: () => import('../views/content/PageEditView.vue'), - }, { path: '/public/page/:id', name: 'public-page-view', diff --git a/frontend/src/services/pagesService.js b/frontend/src/services/pagesService.js index 7a05617..b3fdd5d 100644 --- a/frontend/src/services/pagesService.js +++ b/frontend/src/services/pagesService.js @@ -18,8 +18,9 @@ export default { const res = await api.get('/pages'); return res.data; }, - async createPage(data) { - const res = await api.post('/pages', data); + async createPage(data, isFormData = false) { + const config = isFormData ? { headers: { 'Content-Type': 'multipart/form-data' } } : undefined; + const res = await api.post('/pages', data, config); return res.data; }, async getPage(id) { @@ -40,6 +41,10 @@ export default { const res = await api.get('/pages/public/all'); return res.data; }, + async getInternalPages() { + const res = await api.get('/pages/internal/all'); + return res.data; + }, async getPublicPage(id) { const res = await api.get(`/pages/public/${id}`); return res.data; diff --git a/frontend/src/views/ContentPageView.vue b/frontend/src/views/ContentPageView.vue index 9a17f3a..12b036f 100644 --- a/frontend/src/views/ContentPageView.vue +++ b/frontend/src/views/ContentPageView.vue @@ -22,8 +22,8 @@