From 6d15c5921a250757b64a2d57ae0e08241aff4230 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Oct 2025 17:32:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D1=87=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 8 + backend/routes/auth.js | 21 +- backend/routes/pages.js | 240 ++++++++++-- frontend/src/router/index.js | 10 + frontend/src/services/pagesService.js | 11 + .../src/views/content/ContentListView.vue | 177 +++------ frontend/src/views/content/PublicPageView.vue | 345 +++++++++++++++++ .../src/views/content/PublicPagesView.vue | 355 ++++++++++++++++++ 8 files changed, 1011 insertions(+), 156 deletions(-) create mode 100644 frontend/src/views/content/PublicPageView.vue create mode 100644 frontend/src/views/content/PublicPagesView.vue diff --git a/.gitignore b/.gitignore index 7d06464..f5a1526 100644 --- a/.gitignore +++ b/.gitignore @@ -204,6 +204,14 @@ backend/test_*.js backend/.env.local backend/.env.production +# ======================================== +# ДОКУМЕНТАЦИЯ - НЕ ПУБЛИКОВАТЬ! +# ======================================== + +# Документация проекта +docs/ +**/docs/ + # ======================================== # ПАТЕНТНЫЕ ДОКУМЕНТЫ - НЕ ПУБЛИКОВАТЬ! # ======================================== diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 0a0132c..680fb28 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -651,15 +651,22 @@ router.get('/check', async (req, res) => { try { identities = await identityService.getUserIdentities(req.session.userId); - // Проверяем роль пользователя - const roleResult = await db.getQuery()('SELECT role FROM users WHERE id = $1', [ - req.session.userId, - ]); + // Для пользователей с кошельком проверяем токены в реальном времени + if (authType === 'wallet' && req.session.address) { + isAdmin = await authService.checkAdminTokens(req.session.address); + logger.info(`[auth/check] Admin status for wallet ${req.session.address}: ${isAdmin}`); + } else { + // Для других типов аутентификации используем роль из БД + const roleResult = await db.getQuery()('SELECT role FROM users WHERE id = $1', [ + req.session.userId, + ]); - if (roleResult.rows.length > 0) { - isAdmin = roleResult.rows[0].role === 'admin'; - req.session.isAdmin = isAdmin; + if (roleResult.rows.length > 0) { + isAdmin = roleResult.rows[0].role === 'admin'; + } } + + req.session.isAdmin = isAdmin; } catch (error) { logger.error(`[session/check] Error fetching identities: ${error.message}`); } diff --git a/backend/routes/pages.js b/backend/routes/pages.js index 270e89f..9f9f24f 100644 --- a/backend/routes/pages.js +++ b/backend/routes/pages.js @@ -16,23 +16,40 @@ const db = require('../db'); const FIELDS_TO_EXCLUDE = ['image', 'tags']; -// Проверка и создание таблицы для пользователя-админа -async function ensureUserPagesTable(userId, fields) { +// Проверка и создание общей таблицы для всех админов +async function ensureAdminPagesTable(fields) { fields = fields.filter(f => !FIELDS_TO_EXCLUDE.includes(f)); - const tableName = `pages_user_${userId}`; + const tableName = `admin_pages_simple`; + + // Получаем ключ шифрования + const fs = require('fs'); + const path = require('path'); + let encryptionKey = 'default-key'; + + try { + const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key'); + if (fs.existsSync(keyPath)) { + encryptionKey = fs.readFileSync(keyPath, 'utf8').trim(); + } + } catch (keyError) { + // console.error('Error reading encryption key:', keyError); + } + // Проверяем, есть ли таблица const existsRes = await db.getQuery()( `SELECT to_regclass($1) as exists`, [tableName] ); + if (!existsRes.rows[0].exists) { - // Формируем SQL для создания таблицы с нужными полями + // Формируем 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}" TEXT`); + columns.push(`"${field}_encrypted" TEXT`); } const sql = `CREATE TABLE ${tableName} (${columns.join(', ')})`; await db.getQuery()(sql); @@ -42,87 +59,170 @@ async function ensureUserPagesTable(userId, fields) { `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) { - if (!existingCols.includes(field)) { + const encryptedField = `${field}_encrypted`; + if (!existingCols.includes(encryptedField)) { await db.getQuery()( - `ALTER TABLE ${tableName} ADD COLUMN "${field}" TEXT` + `ALTER TABLE ${tableName} ADD COLUMN "${encryptedField}" TEXT` ); } } } - return tableName; + return { tableName, encryptionKey }; } // Создать страницу (только для админа) router.post('/', async (req, res) => { - if (!req.user || !req.user.isAdmin) { + 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 isAdmin = await authService.checkAdminTokens(req.session.address); + if (!isAdmin) { return res.status(403).json({ error: 'Only admin can create pages' }); } - const userId = req.user.id; + + const authorAddress = req.session.address; const fields = Object.keys(req.body).filter(f => !FIELDS_TO_EXCLUDE.includes(f)); - const filteredBody = {}; - fields.forEach(f => { filteredBody[f] = req.body[f]; }); - const tableName = await ensureUserPagesTable(userId, fields); + const tableName = `admin_pages_simple`; // Формируем SQL для вставки данных - const colNames = fields.map(f => `"${f}"`).join(', '); - const values = Object.values(filteredBody); + 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 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]); }); -// Получить все страницы пользователя-админа +// Получить все страницы админов router.get('/', async (req, res) => { - if (!req.user || !req.user.isAdmin) { + 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 isAdmin = await authService.checkAdminTokens(req.session.address); + if (!isAdmin) { return res.status(403).json({ error: 'Only admin can view pages' }); } - const userId = req.user.id; - const tableName = `pages_user_${userId}`; + + const tableName = `admin_pages_simple`; + + // Получаем ключ шифрования + const fs = require('fs'); + const path = require('path'); + let encryptionKey = 'default-key'; + + try { + const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key'); + if (fs.existsSync(keyPath)) { + encryptionKey = fs.readFileSync(keyPath, 'utf8').trim(); + } + } catch (keyError) { + // console.error('Error reading encryption key:', keyError); + } + // Проверяем, есть ли таблица 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`); + + // Получаем все страницы всех админов + const { rows } = await db.getQuery()(` + SELECT * FROM ${tableName} + ORDER BY created_at DESC + `); + res.json(rows); }); -// Получить одну страницу по id +// Получить одну страницу по id (только для админа) router.get('/:id', async (req, res) => { - if (!req.user || !req.user.isAdmin) { + 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 isAdmin = await authService.checkAdminTokens(req.session.address); + if (!isAdmin) { return res.status(403).json({ error: 'Only admin can view pages' }); } - const userId = req.user.id; - const tableName = `pages_user_${userId}`; + + 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]); + + 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' }); res.json(rows[0]); }); // Редактировать страницу по id router.patch('/:id', async (req, res) => { - if (!req.user || !req.user.isAdmin) { + 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 isAdmin = await authService.checkAdminTokens(req.session.address); + if (!isAdmin) { return res.status(403).json({ error: 'Only admin can edit pages' }); } - const userId = req.user.id; - const tableName = `pages_user_${userId}`; + + 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 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 => { filteredBody[f] = req.body[f]; }); + 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); values.push(req.params.id); + const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = NOW() WHERE id = $${fields.length + 1} RETURNING *`; const { rows } = await db.getQuery()(sql, values); if (!rows.length) return res.status(404).json({ error: 'Page not found' }); @@ -131,18 +231,92 @@ router.patch('/:id', async (req, res) => { // Удалить страницу по id router.delete('/:id', async (req, res) => { - if (!req.user || !req.user.isAdmin) { + 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 isAdmin = await authService.checkAdminTokens(req.session.address); + if (!isAdmin) { return res.status(403).json({ error: 'Only admin can delete pages' }); } - const userId = req.user.id; - const tableName = `pages_user_${userId}`; + + 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()(`DELETE FROM ${tableName} WHERE id = $1 RETURNING *`, [req.params.id]); + + const { rows } = await db.getQuery()( + `DELETE FROM ${tableName} WHERE id = $1 RETURNING *`, + [req.params.id] + ); if (!rows.length) return res.status(404).json({ error: 'Page not found' }); res.json(rows[0]); }); +// Публичные маршруты для просмотра страниц (доступны всем пользователям) +// Получить все опубликованные страницы +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 { rows } = await db.getQuery()(` + SELECT * FROM ${tableName} + WHERE status = 'published' + ORDER BY created_at DESC + `); + + 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; \ No newline at end of file diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 95790d9..1a8240b 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -197,6 +197,16 @@ const routes = [ name: 'page-edit', component: () => import('../views/content/PageEditView.vue'), }, + { + path: '/pages/public', + name: 'public-pages', + component: () => import('../views/content/PublicPagesView.vue'), + }, + { + path: '/pages/public/:id', + name: 'public-page-view', + component: () => import('../views/content/PublicPageView.vue'), + }, { path: '/management', name: 'management', diff --git a/frontend/src/services/pagesService.js b/frontend/src/services/pagesService.js index 0b699d3..7a05617 100644 --- a/frontend/src/services/pagesService.js +++ b/frontend/src/services/pagesService.js @@ -13,6 +13,7 @@ import api from '../api/axios'; export default { + // Админские методы (требуют аутентификации и прав админа) async getPages() { const res = await api.get('/pages'); return res.data; @@ -33,4 +34,14 @@ export default { const res = await api.delete(`/pages/${id}`); return res.data; }, + + // Публичные методы (доступны всем пользователям) + async getPublicPages() { + const res = await api.get('/pages/public/all'); + return res.data; + }, + async getPublicPage(id) { + const res = await api.get(`/pages/public/${id}`); + return res.data; + }, }; \ No newline at end of file diff --git a/frontend/src/views/content/ContentListView.vue b/frontend/src/views/content/ContentListView.vue index df28b91..5bc72b0 100644 --- a/frontend/src/views/content/ContentListView.vue +++ b/frontend/src/views/content/ContentListView.vue @@ -21,13 +21,18 @@
@@ -131,12 +133,18 @@
-

Нет созданных страниц

-

Создайте первую страницу для вашего DLE

- +
@@ -146,29 +154,6 @@ - -
-
-

Шаблоны страниц

-

Готовые шаблоны для быстрого создания контента

-
- -
-
-
- -
-

{{ template.name }}

-

{{ template.description }}

- -
-
-
@@ -207,6 +192,7 @@ import { ref, computed, onMounted } from 'vue'; import { useRouter } from 'vue-router'; import BaseLayout from '../../components/BaseLayout.vue'; import pagesService from '../../services/pagesService'; +import { useAuthContext } from '../../composables/useAuth'; // Props const props = defineProps({ @@ -232,6 +218,7 @@ const props = defineProps({ const emit = defineEmits(['auth-action-completed']); const router = useRouter(); +const { isAdmin, address } = useAuthContext(); // Состояние const activeTab = ref('pages'); @@ -248,33 +235,6 @@ const publishSettings = ref({ autoPublish: false }); -// Шаблоны -const templates = ref([ - { - id: 1, - name: 'О компании', - description: 'Стандартная страница с информацией о компании', - icon: 'fas fa-building' - }, - { - id: 2, - name: 'Услуги', - description: 'Страница с описанием услуг и сервисов', - icon: 'fas fa-cogs' - }, - { - id: 3, - name: 'Контакты', - description: 'Контактная информация и форма обратной связи', - icon: 'fas fa-address-book' - }, - { - id: 4, - name: 'Блог', - description: 'Шаблон для ведения блога и новостей', - icon: 'fas fa-blog' - } -]); // Вычисляемые свойства const filteredPages = computed(() => { @@ -290,12 +250,20 @@ function goToCreate() { router.push({ name: 'content-create' }); } +function goToPublicPages() { + router.push({ name: 'public-pages' }); +} + function goBack() { router.go(-1); } function goToPage(id) { - router.push({ name: 'page-view', params: { id } }); + if (isAdmin.value && address.value) { + router.push({ name: 'page-view', params: { id } }); + } else { + router.push({ name: 'public-page-view', params: { id } }); + } } function goToEdit(id) { @@ -313,18 +281,18 @@ async function deletePage(id) { } } -function useTemplate(template) { - router.push({ - name: 'content-create', - query: { template: template.id } - }); -} function formatDate(date) { if (!date) return 'Не указана'; return new Date(date).toLocaleDateString('ru-RU'); } +function formatAddress(address) { + if (!address) return ''; + // Показываем сокращенный адрес: 0x1234...5678 + return `${address.slice(0, 6)}...${address.slice(-4)}`; +} + function getStatusText(status) { const statusMap = { draft: 'Черновик', @@ -337,7 +305,25 @@ function getStatusText(status) { async function loadPages() { try { isLoading.value = true; - pages.value = await pagesService.getPages(); + + // Проверяем роль админа через кошелек + if (isAdmin.value && address.value) { + try { + // Пытаемся загрузить админские страницы + const response = await pagesService.getPages(); + pages.value = response; + } catch (error) { + if (error.response?.status === 403) { + // Пользователь не админ или нет токенов, загружаем публичные страницы + pages.value = await pagesService.getPublicPages(); + } else { + throw error; + } + } + } else { + // Пользователь не админ или нет кошелька, загружаем публичные страницы + pages.value = await pagesService.getPublicPages(); + } } catch (error) { console.error('Ошибка загрузки страниц:', error); pages.value = []; @@ -592,44 +578,6 @@ onMounted(() => { 100% { transform: rotate(360deg); } } -.templates-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 20px; - margin-top: 20px; -} - -.template-card { - background: white; - border-radius: var(--radius-sm); - padding: 25px; - text-align: center; - border: 1px solid #e9ecef; - cursor: pointer; - transition: all 0.3s ease; -} - -.template-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - transform: translateY(-2px); -} - -.template-icon { - font-size: 3rem; - color: var(--color-primary); - margin-bottom: 15px; -} - -.template-card h3 { - color: var(--color-primary); - margin: 0 0 10px 0; -} - -.template-card p { - color: var(--color-grey-dark); - margin: 0 0 20px 0; - line-height: 1.5; -} .settings-grid { display: grid; @@ -747,9 +695,6 @@ onMounted(() => { grid-template-columns: 1fr; } - .templates-grid { - grid-template-columns: 1fr; - } .settings-grid { grid-template-columns: 1fr; diff --git a/frontend/src/views/content/PublicPageView.vue b/frontend/src/views/content/PublicPageView.vue new file mode 100644 index 0000000..5d9d053 --- /dev/null +++ b/frontend/src/views/content/PublicPageView.vue @@ -0,0 +1,345 @@ + + + + + + + diff --git a/frontend/src/views/content/PublicPagesView.vue b/frontend/src/views/content/PublicPagesView.vue new file mode 100644 index 0000000..e5a4d30 --- /dev/null +++ b/frontend/src/views/content/PublicPagesView.vue @@ -0,0 +1,355 @@ + + + + + + +