feat: новая функция
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
227
backend/scripts/seed/legalTemplatesSeed.js
Normal file
227
backend/scripts/seed/legalTemplatesSeed.js
Normal file
@@ -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, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function tpl(content) {
|
||||
// Лаконичный, «человеческий» текст с минимальными inline‑плейсхолдерами
|
||||
return `
|
||||
<h1>${htmlEscape(content.title)}</h1>
|
||||
<p>
|
||||
Настоящий документ предназначен для использования в рамках деятельности
|
||||
{{company_name}} по адресу {{company_address}} и подлежит персонализации редактором.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ответственное лицо за вопросы персональных данных: {{responsible_person}}
|
||||
(<a href="mailto:{{privacy_email}}">{{privacy_email}}</a>, {{privacy_phone}}).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Дата версии: {{date}} · Юрисдикция: {{jurisdiction}} · Язык: {{language}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ниже приведён текст шаблона. Перед публикацией проверьте корректность реквизитов,
|
||||
правовых оснований и сроков хранения данных.
|
||||
</p>
|
||||
|
||||
${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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user