Files
minicrm/server.js
2026-01-28 22:03:01 +05:00

1652 lines
67 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.
// server.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const session = require('express-session');
require('dotenv').config();
// Импортируем модули
const { initializeDatabase, getDb, isInitialized } = require('./database');
const authService = require('./auth');
const postgresLogger = require('./postgres');
const { sendTaskNotifications, checkUpcomingDeadlines, getStatusText } = require('./notifications');
const { setupUploadMiddleware } = require('./upload-middleware');
const { setupTaskEndpoints } = require('./task-endpoints');
const app = express();
const PORT = process.env.PORT || 3000;
// Глобальные переменные
let db = null;
let serverReady = false;
let adminRouter = null;
let upload = null;
// Инициализируем multer сразу с настройками по умолчанию
const uploadsDir = path.join(__dirname, 'data', 'uploads');
const createDirIfNotExists = (dirPath) => {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
};
createDirIfNotExists(uploadsDir);
// Создаем базовую конфигурацию multer
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, uploadsDir);
},
filename: function (req, file, cb) {
// Используем оригинальное имя файла с timestamp для уникальности
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
const name = path.basename(file.originalname, ext);
cb(null, name + '-' + uniqueSuffix + ext);
}
});
// Создаем экземпляр multer сразу
upload = multer({
storage: storage,
limits: {
fileSize: 50 * 1024 * 1024 // 50MB
}
});
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use('/uploads', express.static(path.join(__dirname, 'data', 'uploads')));
app.use(session({
secret: process.env.SESSION_SECRET || 'fallback_secret_change_in_production',
resave: true,
saveUninitialized: false,
cookie: {
secure: false,
maxAge: 24 * 60 * 60 * 1000,
httpOnly: true
}
}));
// Middleware для проверки готовности сервера
app.use((req, res, next) => {
if (!serverReady && req.path !== '/health' && req.path !== '/api/health') {
return res.status(503).json({
error: 'Сервер запускается...',
status: 'initializing'
});
}
next();
});
// Health check endpoints
app.get('/health', (req, res) => {
res.json({
status: serverReady ? 'ready' : 'initializing',
database: isInitialized() ? 'connected' : 'connecting',
auth: authService.isReady() ? 'ready' : 'waiting',
timestamp: new Date().toISOString()
});
});
app.get('/api/health', (req, res) => {
res.json({
status: serverReady ? 'ready' : 'initializing',
database: isInitialized() ? 'connected' : 'connecting',
auth: authService.isReady() ? 'ready' : 'waiting',
timestamp: new Date().toISOString()
});
});
// Вспомогательные функции
function checkIfOverdue(dueDate, status) {
if (!dueDate || status === 'completed') return false;
const now = new Date();
const due = new Date(dueDate);
return due < now;
}
function checkOverdueTasks() {
if (!db) {
console.error('❌ База данных не доступна для проверки просроченных задач');
return;
}
const now = new Date().toISOString();
const query = `
SELECT ta.id, ta.task_id, ta.user_id, ta.status, ta.due_date
FROM task_assignments ta
JOIN tasks t ON ta.task_id = t.id
WHERE ta.due_date IS NOT NULL
AND ta.due_date < ?
AND ta.status NOT IN ('completed', 'overdue')
AND t.status = 'active'
AND t.closed_at IS NULL
`;
db.all(query, [now], (err, assignments) => {
if (err) {
console.error('❌ Ошибка при проверке просроченных задач:', err);
return;
}
assignments.forEach(assignment => {
db.run(
"UPDATE task_assignments SET status = 'overdue' WHERE id = ?",
[assignment.id]
);
const { logActivity } = require('./database');
if (logActivity) {
logActivity(assignment.task_id, assignment.user_id, 'STATUS_CHANGED', 'Задача просрочена');
}
});
});
}
// Middleware для аутентификации
const requireAuth = (req, res, next) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
next();
};
// API для аутентификации
app.post('/api/login', async (req, res) => {
const { login, password } = req.body;
if (!login || !password) {
return res.status(400).json({ error: 'Логин и пароль обязательны' });
}
try {
const user = await authService.authenticate(login, password);
if (user) {
const sessionUser = {
id: user.id,
login: user.login,
name: user.name,
email: user.email,
role: user.role,
auth_type: user.auth_type,
groups: user.groups ? (typeof user.groups === 'string' ? JSON.parse(user.groups) : user.groups) : []
};
req.session.user = sessionUser;
req.session.save((err) => {
if (err) {
console.error('❌ Ошибка сохранения сессии:', err);
return res.status(500).json({ error: 'Ошибка сохранения сессии' });
}
console.log(`✅ Успешная авторизация: ${user.name} (${user.login}) через ${user.auth_type}`);
if (user.groups) {
console.log(`Группы пользователя: ${user.groups}`);
}
res.json({
success: true,
user: sessionUser
});
});
} else {
console.log(`❌ Неудачная попытка входа: ${login}`);
res.status(401).json({ error: 'Неверный логин или пароль' });
}
} catch (error) {
console.error('❌ Ошибка аутентификации:', error.message);
res.status(500).json({
error: 'Ошибка сервера при авторизации',
details: error.message
});
}
});
app.post('/api/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
console.error('❌ Ошибка при выходе:', err);
return res.status(500).json({ error: 'Ошибка при выходе' });
}
res.json({ success: true });
});
});
app.get('/api/user', (req, res) => {
if (req.session.user) {
if (req.session.user.auth_type === 'ldap') {
if (!db) {
return res.status(503).json({ error: 'База данных не готова' });
}
db.get("SELECT groups FROM users WHERE id = ?", [req.session.user.id], (err, user) => {
if (err || !user) {
req.session.destroy();
return res.status(401).json({ error: 'Пользователь не найден' });
}
let groups = [];
try {
groups = JSON.parse(user.groups || '[]');
} catch (e) {
groups = [];
}
const allowedGroups = process.env.ALLOWED_GROUPS ?
process.env.ALLOWED_GROUPS.split(',').map(g => g.trim()) : [];
const isAdmin = groups.some(group => allowedGroups.includes(group));
const actualRole = isAdmin ? 'admin' : 'teacher';
if (req.session.user.role !== actualRole) {
console.log(`Обновлена роль пользователя ${req.session.user.login} с ${req.session.user.role} на ${actualRole}`);
db.run(
"UPDATE users SET role = ?, updated_at = datetime('now') WHERE id = ?",
[actualRole, req.session.user.id]
);
req.session.user.role = actualRole;
}
res.json({ user: req.session.user });
});
} else {
res.json({ user: req.session.user });
}
} else {
res.status(401).json({ error: 'Не аутентифицирован' });
}
});
// Middleware для проверки наличия БД в API endpoints
app.use((req, res, next) => {
if (!db && req.path.startsWith('/api/') && req.path !== '/api/health' && req.path !== '/api/login') {
return res.status(503).json({ error: 'База данных не готова' });
}
next();
});
// API для пользователей
app.get('/api/users', requireAuth, (req, res) => {
const search = req.query.search || '';
let query = `
SELECT id, login, name, email, role, auth_type
FROM users
WHERE role IN ('admin', 'teacher')
`;
const params = [];
if (search) {
query += ` AND (login LIKE ? OR name LIKE ? OR email LIKE ?)`;
const searchPattern = `%${search}%`;
params.push(searchPattern, searchPattern, searchPattern);
}
query += " ORDER BY name";
db.all(query, params, (err, rows) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json(rows);
});
});
// API для файлов
app.get('/api/tasks/:taskId/files', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
const { checkTaskAccess } = require('./database');
checkTaskAccess(userId, taskId, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' });
}
db.all(`
SELECT tf.*, u.name as user_name, u.login as user_login
FROM task_files tf
LEFT JOIN users u ON tf.user_id = u.id
WHERE tf.task_id = ?
ORDER BY tf.uploaded_at DESC
`, [taskId], (err, files) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json(files);
});
});
});
app.get('/api/files/:fileId/download', requireAuth, (req, res) => {
const { fileId } = req.params;
const userId = req.session.user.id;
db.get("SELECT tf.*, t.id as task_id FROM task_files tf JOIN tasks t ON tf.task_id = t.id WHERE tf.id = ?", [fileId], (err, file) => {
if (err || !file) {
return res.status(404).json({ error: 'Файл не найдена' });
}
const { checkTaskAccess } = require('./database');
checkTaskAccess(userId, file.task_id, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(404).json({ error: 'Файл не найден или у вас нет прав доступа' });
}
if (!fs.existsSync(file.file_path)) {
return res.status(404).json({ error: 'Файл не найден на сервере' });
}
// Исправляем кодировку имени файла
let decodedFileName = file.original_name;
// Пробуем декодировать если это UTF-8 в Latin-1 (для старых записей)
try {
if (/^[A-Za-z0-9\.\-_]+$/.test(decodedFileName)) {
// Если имя содержит только латинские символы, оставляем как есть
} else if (decodedFileName.includes('Ð') || decodedFileName.includes('Ñ')) {
// Исправляем неправильно декодированную кириллицу
decodedFileName = Buffer.from(decodedFileName, 'binary').toString('utf8');
}
} catch (e) {
console.error('Ошибка декодирования имени файла:', e);
}
// Кодируем имя файла для безопасной передачи
const encodedFileName = encodeURIComponent(decodedFileName);
// Устанавливаем заголовки для скачивания
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`);
res.setHeader('Content-Type', 'application/octet-stream');
// Отправляем файл
res.sendFile(file.file_path);
});
});
});
// API для логов активности
app.get('/api/activity-logs', requireAuth, (req, res) => {
const userId = req.session.user.id;
let query = `
SELECT al.*, u.name as user_name, t.title as task_title
FROM activity_logs al
LEFT JOIN users u ON al.user_id = u.id
LEFT JOIN tasks t ON al.task_id = t.id
WHERE 1=1
`;
if (req.session.user.role !== 'admin') {
query += ` AND (t.created_by = ${userId} OR al.task_id IN (
SELECT task_id FROM task_assignments WHERE user_id = ${userId}
))`;
}
query += " ORDER BY al.created_at DESC LIMIT 100";
db.all(query, (err, logs) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json(logs);
});
});
// API для логов уведомлений из PostgreSQL
app.get('/api/notification-logs', requireAuth, async (req, res) => {
try {
const {
taskId,
status,
startDate,
endDate,
limit = 50,
offset = 0
} = req.query;
// Получаем все логи для текущего пользователя
const query = `
SELECT * FROM sms_logs
WHERE creator_id = ? OR assignee_id = ?
${taskId ? 'AND task_id = ?' : ''}
${status ? 'AND status = ?' : ''}
${startDate ? 'AND created_at >= ?' : ''}
${endDate ? 'AND created_at <= ?' : ''}
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`;
const params = [req.session.user.id, req.session.user.id];
if (taskId) params.push(taskId);
if (status) params.push(status);
if (startDate) params.push(startDate);
if (endDate) params.push(endDate);
params.push(parseInt(limit), parseInt(offset));
const logs = await new Promise((resolve, reject) => {
db.all(query, params, (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
const countQuery = `
SELECT COUNT(*) as total FROM sms_logs
WHERE creator_id = ? OR assignee_id = ?
${taskId ? 'AND task_id = ?' : ''}
${status ? 'AND status = ?' : ''}
${startDate ? 'AND created_at >= ?' : ''}
${endDate ? 'AND created_at <= ?' : ''}
`;
const countParams = [req.session.user.id, req.session.user.id];
if (taskId) countParams.push(taskId);
if (status) countParams.push(status);
if (startDate) countParams.push(startDate);
if (endDate) countParams.push(endDate);
const countResult = await new Promise((resolve, reject) => {
db.get(countQuery, countParams, (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
res.json({
logs: logs || [],
total: countResult?.total || 0,
limit: parseInt(limit),
offset: parseInt(offset)
});
} catch (error) {
console.error('❌ Ошибка получения логов уведомлений:', error);
res.status(500).json({ error: 'Ошибка получения логов' });
}
});
// API для статистики уведомлений
app.get('/api/notification-stats', requireAuth, async (req, res) => {
try {
const { period = 'day' } = req.query;
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
let dateFilter = '';
switch (period) {
case 'day':
dateFilter = "created_at >= CURRENT_DATE";
break;
case 'week':
dateFilter = "created_at >= DATE('now', '-7 days')";
break;
case 'month':
dateFilter = "created_at >= DATE('now', '-30 days')";
break;
case 'year':
dateFilter = "created_at >= DATE('now', '-365 days')";
break;
default:
dateFilter = "created_at >= CURRENT_DATE";
}
const statsQuery = `
SELECT
status,
COUNT(*) as count,
COUNT(CASE WHEN sent_at IS NOT NULL THEN 1 END) as sent_count,
COUNT(CASE WHEN error_message IS NOT NULL THEN 1 END) as error_count
FROM sms_logs
WHERE ${dateFilter}
GROUP BY status
`;
const totalQuery = `
SELECT
COUNT(*) as total,
COUNT(CASE WHEN sent_at IS NOT NULL THEN 1 END) as total_sent,
COUNT(CASE WHEN error_message IS NOT NULL THEN 1 END) as total_errors
FROM sms_logs
WHERE ${dateFilter}
`;
const [stats, total] = await Promise.all([
new Promise((resolve, reject) => {
db.all(statsQuery, [], (err, rows) => {
if (err) reject(err);
else resolve(rows || []);
});
}),
new Promise((resolve, reject) => {
db.get(totalQuery, [], (err, row) => {
if (err) reject(err);
else resolve(row || { total: 0, total_sent: 0, total_errors: 0 });
});
})
]);
res.json({
period: period,
stats: stats,
total: total.total || 0,
totalSent: total.total_sent || 0,
totalErrors: total.total_errors || 0,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('❌ Ошибка получения статистики:', error);
res.status(500).json({ error: 'Ошибка получения статистики' });
}
});
// API для проверки состояния PostgreSQL
app.get('/api/postgres-health', requireAuth, async (req, res) => {
try {
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
const health = await postgresLogger.healthCheck();
res.json(health);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Админ панель
app.get('/admin', (req, res) => {
if (!req.session.user || req.session.user.role !== 'admin') {
return res.status(403).send('Доступ запрещен');
}
res.sendFile(path.join(__dirname, 'public/admin.html'));
});
// Страница профилей пользователей (только для админов)
app.get('/admin/profiles', (req, res) => {
if (!req.session.user || req.session.user.role !== 'admin') {
return res.status(403).send('Доступ запрещен');
}
res.sendFile(path.join(__dirname, 'public/admin-profiles.html'));
});
// Админ панель для документов
app.get('/admin-doc', (req, res) => {
if (!req.session.user || req.session.user.role !== 'admin') {
return res.status(403).send('Доступ запрещен');
}
res.sendFile(path.join(__dirname, 'public/admin-doc.html'));
});
// Страница согласования документов
app.get('/doc', (req, res) => {
if (!req.session.user) {
return res.redirect('/');
}
if (!req.session.user || req.session.user.role !== 'admin') {
return res.status(403).send('в разработке');
}
res.sendFile(path.join(__dirname, 'public/doc.html'));
});
// API для типов документов
app.get('/api/document-types', requireAuth, (req, res) => {
db.all("SELECT * FROM simple_document_types ORDER BY name", [], (err, rows) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json(rows);
});
});
// API для документов (ИСПРАВЛЕНО: upload определен в начале файла)
app.post('/api/documents', requireAuth, upload.array('files', 15), async (req, res) => {
try {
console.log('📝 Начало создания документа...');
const userId = req.session.user.id;
const {
title,
description,
dueDate,
documentTypeId,
documentNumber,
documentDate,
pagesCount,
urgencyLevel,
comment
} = req.body;
console.log('📋 Данные документа:', {
title, userId, documentTypeId, documentNumber
});
// Валидация обязательных полей - только название
if (!title || title.trim() === '') {
return res.status(400).json({ error: 'Название документа обязательно' });
}
// Находим группу "Секретарь"
db.get(`
SELECT u.id
FROM users u
JOIN user_group_memberships ugm ON u.id = ugm.user_id
JOIN user_groups g ON ugm.group_id = g.id
WHERE g.name = 'Секретарь'
LIMIT 1
`, async (err, secretary) => {
if (err) {
console.error('❌ Ошибка поиска секретаря:', err);
return res.status(500).json({ error: 'Ошибка поиска секретаря' });
}
if (!secretary) {
console.warn('⚠️ Секретарь не найден в группе "Секретарь"');
return res.status(400).json({
error: 'Не найден секретарь для согласования документов. Пожалуйста, добавьте пользователя в группу "Секретарь".'
});
}
console.log('✅ Найден секретарь из группы:', secretary.id);
// Создаем задачу
db.run(`
INSERT INTO tasks (title, description, due_date, created_by, status, created_at)
VALUES (?, ?, ?, ?, 'active', datetime('now'))
`, [
`Документ: ${title}`,
description || '',
dueDate || null,
userId
], function(err) {
if (err) {
console.error('❌ Ошибка создания задачи:', err);
return res.status(500).json({ error: 'Ошибка создания задачи' });
}
const taskId = this.lastID;
console.log('✅ Задача создана, ID:', taskId);
// Создаем запись документа в таблице simple_documents
// Тип документа не обязателен - может быть NULL
db.run(`
INSERT INTO simple_documents (
task_id, document_type_id, document_number,
document_date, pages_count, urgency_level, comment
) VALUES (?, ?, ?, ?, ?, ?, ?)
`, [
taskId,
documentTypeId || null, // Может быть NULL
documentNumber || null,
documentDate || null,
pagesCount || null,
urgencyLevel || 'normal',
comment || null
], function(err) {
if (err) {
console.error('❌ Ошибка создания записи документа:', err);
// Удаляем задачу если не удалось создать документ
db.run("DELETE FROM tasks WHERE id = ?", [taskId]);
return res.status(500).json({ error: 'Ошибка создания записи документа' });
}
const documentId = this.lastID;
console.log('✅ Запись документа создана, ID:', documentId);
// Назначаем задачу секретарю
db.run(`
INSERT INTO task_assignments (task_id, user_id, status, created_at)
VALUES (?, ?, 'assigned', datetime('now'))
`, [taskId, secretary.id], function(err) {
if (err) {
console.error('❌ Ошибка назначения задачи секретарю:', err);
// Удаляем задачу и документ
db.run("DELETE FROM simple_documents WHERE task_id = ?", [taskId]);
db.run("DELETE FROM tasks WHERE id = ?", [taskId]);
return res.status(500).json({ error: 'Ошибка назначения задачи секретарю' });
}
console.log('✅ Задача назначена секретарю');
// Загружаем файлы если есть
if (req.files && req.files.length > 0) {
console.log('📁 Файлов для загрузки:', req.files.length);
const uploadPromises = req.files.map(file => {
return new Promise((resolve, reject) => {
const filePath = file.path;
const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8');
db.run(`
INSERT INTO task_files (task_id, user_id, file_path, original_name, file_size, uploaded_at)
VALUES (?, ?, ?, ?, ?, datetime('now'))
`, [taskId, userId, filePath, originalName, file.size], function(err) {
if (err) {
console.error('❌ Ошибка сохранения файла в БД:', err);
reject(err);
} else {
console.log('✅ Файл сохранен:', originalName);
resolve();
}
});
});
});
Promise.all(uploadPromises)
.then(() => {
console.log('✅ Все файлы загружены');
// Логируем действие
const { logActivity } = require('./database');
logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ для согласования: ${title}`);
res.json({
success: true,
taskId: taskId,
documentId: documentId,
message: 'Документ успешно создан и отправлен на согласование'
});
})
.catch(error => {
console.error('❌ Ошибка загрузки файлов:', error);
// Все равно возвращаем успех, так как задача и документ созданы
const { logActivity } = require('./database');
logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ для согласования: ${title}`);
res.json({
success: true,
taskId: taskId,
documentId: documentId,
message: 'Документ создан, но были проблемы с загрузкой файлов'
});
});
} else {
console.log('📁 Файлы не прикреплены');
// Логируем действие
const { logActivity } = require('./database');
logActivity(taskId, userId, 'DOCUMENT_CREATED', `Создан документ для согласования: ${title}`);
res.json({
success: true,
taskId: taskId,
documentId: documentId,
message: 'Документ успешно создан и отправлен на согласование'
});
}
});
});
});
});
} catch (error) {
console.error('❌ Общая ошибка создания документа:', error);
res.status(500).json({
error: 'Ошибка создания документа',
details: error.message
});
}
});
// Получение моих документов
app.get('/api/documents/my', requireAuth, (req, res) => {
const userId = req.session.user.id;
console.log('📄 Запрос документов пользователя ID:', userId);
db.all(`
SELECT
t.id,
t.title,
t.description,
t.due_date,
t.created_at,
t.status,
t.closed_at,
sd.id as document_id,
sd.document_type_id,
sdt.name as document_type_name,
sd.document_number,
sd.document_date,
sd.pages_count,
sd.urgency_level,
sd.comment,
sd.refusal_reason,
u.name as creator_name,
ta.status as assignment_status,
ta.user_id as assignee_id,
au.name as assignee_name
FROM tasks t
LEFT JOIN simple_documents sd ON t.id = sd.task_id
LEFT JOIN simple_document_types sdt ON sd.document_type_id = sdt.id
LEFT JOIN users u ON t.created_by = u.id
LEFT JOIN task_assignments ta ON t.id = ta.task_id
LEFT JOIN users au ON ta.user_id = au.id
WHERE t.created_by = ?
AND t.title LIKE 'Документ:%'
ORDER BY t.created_at DESC
`, [userId], async (err, tasks) => {
if (err) {
console.error('❌ Ошибка получения документов:', err);
return res.status(500).json({
error: 'Ошибка получения документов',
details: err.message
});
}
console.log('✅ Найдено задач:', tasks.length);
// Загружаем файлы для каждой задачи
const tasksWithFiles = await Promise.all(tasks.map(async (task) => {
try {
const files = await new Promise((resolve, reject) => {
db.all(`
SELECT tf.*, u.name as user_name
FROM task_files tf
LEFT JOIN users u ON tf.user_id = u.id
WHERE tf.task_id = ?
ORDER BY tf.uploaded_at DESC
`, [task.id], (err, rows) => {
if (err) reject(err);
else resolve(rows || []);
});
});
task.files = files || [];
} catch (error) {
console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, error);
task.files = [];
}
return task;
}));
res.json(tasksWithFiles);
});
});
// Получение документов для секретаря
app.get('/api/documents/secretary', requireAuth, (req, res) => {
const userId = req.session.user.id;
console.log('📄 Запрос документов для секретаря ID:', userId);
// Проверяем, что пользователь секретарь
db.get(`
SELECT 1 FROM users u
JOIN user_group_memberships ugm ON u.id = ugm.user_id
JOIN user_groups g ON ugm.group_id = g.id
WHERE u.id = ? AND g.name = 'Секретарь'
`, [userId], (err, isSecretary) => {
if (err || !isSecretary) {
// Пробуем альтернативный способ проверки
db.get("SELECT groups FROM users WHERE id = ?", [userId], (err, user) => {
if (err || !user || !user.groups || !user.groups.includes('Секретарь')) {
console.log('⚠️ Пользователь не является секретарем:', userId);
return res.status(403).json({ error: 'Недостаточно прав. Требуется роль секретаря.' });
}
fetchDocuments();
});
} else {
fetchDocuments();
}
});
function fetchDocuments() {
db.all(`
SELECT
t.id,
t.title,
t.description,
t.due_date,
t.created_at,
ta.status as assignment_status,
sd.id as document_id,
sd.document_type_id,
sdt.name as document_type_name,
sd.document_number,
sd.document_date,
sd.pages_count,
sd.urgency_level,
sd.comment,
sd.refusal_reason,
u.name as creator_name
FROM tasks t
JOIN task_assignments ta ON t.id = ta.task_id
LEFT JOIN simple_documents sd ON t.id = sd.task_id
LEFT JOIN simple_document_types sdt ON sd.document_type_id = sdt.id
LEFT JOIN users u ON t.created_by = u.id
WHERE ta.user_id = ?
AND t.title LIKE 'Документ:%'
AND t.status = 'active'
AND t.closed_at IS NULL
ORDER BY
CASE sd.urgency_level
WHEN 'very_urgent' THEN 1
WHEN 'urgent' THEN 2
ELSE 3
END,
t.due_date ASC,
t.created_at DESC
`, [userId], async (err, tasks) => {
if (err) {
console.error('❌ Ошибка получения документов для секретаря:', err);
return res.status(500).json({
error: 'Ошибка получения документов',
details: err.message
});
}
console.log('✅ Найдено задач для секретаря:', tasks.length);
// Загружаем файлы для каждой задачи
const tasksWithFiles = await Promise.all(tasks.map(async (task) => {
try {
const files = await new Promise((resolve, reject) => {
db.all(`
SELECT tf.*, u.name as user_name
FROM task_files tf
LEFT JOIN users u ON tf.user_id = u.id
WHERE tf.task_id = ?
ORDER BY tf.uploaded_at DESC
`, [task.id], (err, rows) => {
if (err) reject(err);
else resolve(rows || []);
});
});
task.files = files || [];
} catch (error) {
console.error(`Ошибка загрузки файлов для задачи ${task.id}:`, error);
task.files = [];
}
return task;
}));
res.json(tasksWithFiles);
});
}
});
// Обновление статуса документа
app.put('/api/documents/:id/status', requireAuth, (req, res) => {
const documentId = req.params.id;
const { status, comment, refusalReason } = req.body;
const userId = req.session.user.id;
// Проверяем права (только секретарь или администратор)
db.get(`
SELECT 1 FROM users u
JOIN user_group_memberships ugm ON u.id = ugm.user_id
JOIN user_groups g ON ugm.group_id = g.id
WHERE u.id = ? AND g.name = 'Секретарь'
`, [userId], (err, isSecretary) => {
if (err || !isSecretary) {
db.get("SELECT groups FROM users WHERE id = ?", [userId], (err, user) => {
if (err || !user || !user.groups || !user.groups.includes('Секретарь')) {
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
}
updateDocumentStatus();
});
} else {
updateDocumentStatus();
}
});
function updateDocumentStatus() {
db.get("SELECT task_id FROM simple_documents WHERE id = ?", [documentId], (err, document) => {
if (err || !document) {
return res.status(404).json({ error: 'Документ не найден' });
}
const taskId = document.task_id;
// Обновляем статус в задании
db.run("UPDATE task_assignments SET status = ? WHERE task_id = ? AND user_id = ?",
[status, taskId, userId], function(err) {
if (err) {
return res.status(500).json({ error: err.message });
}
// Обновляем причину отказа если есть
if (refusalReason) {
db.run("UPDATE simple_documents SET refusal_reason = ? WHERE id = ?",
[refusalReason, documentId]);
}
// Логируем действие
const { logActivity } = require('./database');
const actionMap = {
'approved': 'Документ согласован',
'completed': 'Документ согласован',
'received': 'Оригинал документа получен',
'signed': 'Документ подписан',
'refused': 'В согласовании отказано'
};
const actionText = actionMap[status] || `Статус изменен на: ${status}`;
logActivity(taskId, userId, 'STATUS_CHANGED', actionText);
res.json({ success: true });
}
);
});
}
});
// Отзыв документа
app.post('/api/documents/:id/cancel', requireAuth, (req, res) => {
const documentId = req.params.id;
const userId = req.session.user.id;
db.get("SELECT task_id FROM simple_documents WHERE id = ?", [documentId], (err, document) => {
if (err || !document) {
return res.status(404).json({ error: 'Документ не найден' });
}
const taskId = document.task_id;
// Проверяем, что пользователь создатель задачи
db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
if (parseInt(task.created_by) !== parseInt(userId)) {
return res.status(403).json({ error: 'Вы не являетесь создателем этого документа' });
}
// Обновляем статус задачи
db.run("UPDATE tasks SET status = 'cancelled', closed_at = datetime('now') WHERE id = ?", [taskId], function(err) {
if (err) {
return res.status(500).json({ error: err.message });
}
// Логируем действие
const { logActivity } = require('./database');
logActivity(taskId, userId, 'STATUS_CHANGED', 'Документ отозван создателем');
res.json({ success: true });
});
});
});
});
// Получение пакета документов
app.get('/api/documents/:id/package', requireAuth, async (req, res) => {
const documentId = req.params.id;
const userId = req.session.user.id;
// Проверяем доступ к документу
db.get(`
SELECT t.id, t.created_by
FROM documents d
JOIN tasks t ON d.task_id = t.id
WHERE d.id = ?
`, [documentId], async (err, result) => {
if (err || !result) {
return res.status(404).json({ error: 'Документ не найден' });
}
// Проверяем, что пользователь имеет доступ (создатель или секретарь)
const isCreator = parseInt(result.created_by) === parseInt(userId);
const isSecretary = req.session.user.groups && req.session.user.groups.includes('Секретарь');
if (!isCreator && !isSecretary) {
return res.status(403).json({ error: 'Недостаточно прав' });
}
// Здесь будет логика создания ZIP архива с документами
// Пока возвращаем заглушку
res.json({
success: true,
message: 'Функция создания пакета документов будет реализована в следующей версии'
});
});
});
// API для получения настроек уведомлений пользователя
app.get('/api/user/settings', requireAuth, async (req, res) => {
try {
if (!req.session.user || !req.session.user.id) {
return res.status(401).json({ error: 'Не аутентифицирован' });
}
const userId = req.session.user.id;
const { getDb } = require('./database');
const db = getDb();
db.get("SELECT email_notifications, notification_email, telegram_notifications, telegram_chat_id, vk_notifications, vk_user_id FROM user_settings WHERE user_id = ?",
[userId],
(err, settings) => {
if (err) {
console.error('❌ Ошибка получения настроек:', err);
return res.status(500).json({ error: 'Ошибка получения настроек' });
}
if (!settings) {
// Возвращаем настройки по умолчанию
res.json({
email_notifications: true,
notification_email: req.session.user.email || '',
telegram_notifications: false,
telegram_chat_id: '',
vk_notifications: false,
vk_user_id: ''
});
} else {
// Преобразуем boolean из SQLite (0/1) в true/false
const result = {
email_notifications: !!settings.email_notifications,
notification_email: settings.notification_email || '',
telegram_notifications: !!settings.telegram_notifications,
telegram_chat_id: settings.telegram_chat_id || '',
vk_notifications: !!settings.vk_notifications,
vk_user_id: settings.vk_user_id || ''
};
res.json(result);
}
}
);
} catch (error) {
console.error('❌ Ошибка получения настроек:', error);
res.status(500).json({ error: 'Ошибка получения настроек' });
}
});
// API для сохранения настроек уведомлений
app.post('/api/user/settings', requireAuth, async (req, res) => {
try {
if (!req.session.user || !req.session.user.id) {
return res.status(401).json({ error: 'Не аутентифицирован' });
}
const userId = req.session.user.id;
const {
email_notifications,
notification_email,
telegram_notifications,
telegram_chat_id,
vk_notifications,
vk_user_id
} = req.body;
// Валидация
if (email_notifications === undefined ||
telegram_notifications === undefined ||
vk_notifications === undefined) {
return res.status(400).json({
error: 'Не все обязательные поля заполнены'
});
}
const { getDb } = require('./database');
const db = getDb();
// Проверяем, есть ли уже настройки для пользователя
db.get("SELECT id FROM user_settings WHERE user_id = ?", [userId], (err, existing) => {
if (err) {
console.error('❌ Ошибка проверки настроек:', err);
return res.status(500).json({ error: 'Ошибка сохранения настроек' });
}
if (existing) {
// Обновляем существующие настройки
db.run(
`UPDATE user_settings SET
email_notifications = ?,
notification_email = ?,
telegram_notifications = ?,
telegram_chat_id = ?,
vk_notifications = ?,
vk_user_id = ?,
updated_at = CURRENT_TIMESTAMP
WHERE user_id = ?`,
[
email_notifications ? 1 : 0,
notification_email || '',
telegram_notifications ? 1 : 0,
telegram_chat_id || '',
vk_notifications ? 1 : 0,
vk_user_id || '',
userId
],
function(updateErr) {
if (updateErr) {
console.error('❌ Ошибка обновления настроек:', updateErr);
return res.status(500).json({ error: 'Ошибка сохранения настроек' });
}
console.log(`✅ Настройки пользователя ${userId} обновлены`);
res.json({ success: true });
}
);
} else {
// Создаем новые настройки
db.run(
`INSERT INTO user_settings
(user_id, email_notifications, notification_email,
telegram_notifications, telegram_chat_id,
vk_notifications, vk_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
userId,
email_notifications ? 1 : 0,
notification_email || '',
telegram_notifications ? 1 : 0,
telegram_chat_id || '',
vk_notifications ? 1 : 0,
vk_user_id || ''
],
function(insertErr) {
if (insertErr) {
console.error('❌ Ошибка создания настроек:', insertErr);
return res.status(500).json({ error: 'Ошибка сохранения настроек' });
}
console.log(`✅ Настройки пользователя ${userId} созданы`);
res.json({ success: true });
}
);
}
});
} catch (error) {
console.error('❌ Ошибка сохранения настроек:', error);
res.status(500).json({ error: 'Ошибка сохранения настроек' });
}
});
// API для проверки настроек
app.get('/api/user/settings/check', requireAuth, (req, res) => {
try {
if (!req.session.user || !req.session.user.id) {
return res.status(401).json({ error: 'Не аутентифицирован' });
}
const userId = req.session.user.id;
const { getDb } = require('./database');
const db = getDb();
db.get("SELECT COUNT(*) as count FROM user_settings WHERE user_id = ?", [userId], (err, result) => {
if (err) {
console.error('❌ Ошибка проверки таблицы:', err);
return res.json({
table_exists: false,
user_has_settings: false,
error: err.message
});
}
res.json({
table_exists: true,
user_has_settings: result.count > 0,
user_id: userId
});
});
} catch (error) {
console.error('❌ Ошибка проверки настроек:', error);
res.status(500).json({ error: 'Ошибка проверки настроек' });
}
});
// API для проверки email уведомлений
app.get('/api/email-health', requireAuth, async (req, res) => {
try {
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
const emailNotifications = require('./email-notifications');
const health = {
ready: emailNotifications.isReady(),
email: process.env.YANDEX_EMAIL,
host: process.env.YANDEX_SMTP_HOST,
timestamp: new Date().toISOString()
};
res.json(health);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// API для групп пользователей
app.get('/api/groups', requireAuth, (req, res) => {
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
db.all(`
SELECT g.*, COUNT(ugm.user_id) as member_count
FROM user_groups g
LEFT JOIN user_group_memberships ugm ON g.id = ugm.group_id
GROUP BY g.id
ORDER BY g.name
`, [], (err, groups) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json(groups);
});
});
// API для всех пользователей с группами
app.get('/api/users/all', requireAuth, (req, res) => {
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
db.all(`
SELECT
u.id,
u.login,
u.name,
u.email,
u.role,
u.auth_type,
u.created_at,
json_group_array(
json_object(
'group_id', g.id,
'group_name', g.name,
'group_color', g.color
)
) as groups
FROM users u
LEFT JOIN user_group_memberships ugm ON u.id = ugm.user_id
LEFT JOIN user_groups g ON ugm.group_id = g.id
WHERE u.role IN ('admin', 'teacher')
GROUP BY u.id
ORDER BY u.name
`, [], (err, users) => {
if (err) {
console.error('Ошибка получения пользователей:', err);
res.status(500).json({ error: err.message });
return;
}
// Парсим JSON строку с группами
const usersWithGroups = users.map(user => {
try {
user.groups = JSON.parse(user.groups).filter(g => g.group_id !== null);
} catch (e) {
user.groups = [];
}
return user;
});
res.json(usersWithGroups);
});
});
// API для добавления пользователя в группу
app.post('/api/groups/:groupId/users/:userId', requireAuth, (req, res) => {
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
const { groupId, userId } = req.params;
// Проверяем существование группы и пользователя
db.serialize(() => {
db.get("SELECT id FROM user_groups WHERE id = ?", [groupId], (err, group) => {
if (err || !group) {
return res.status(404).json({ error: 'Группа не найдена' });
}
db.get("SELECT id FROM users WHERE id = ?", [userId], (err, user) => {
if (err || !user) {
return res.status(404).json({ error: 'Пользователь не найден' });
}
// Добавляем пользователя в группу
db.run(`
INSERT OR IGNORE INTO user_group_memberships (user_id, group_id)
VALUES (?, ?)
`, [userId, groupId], function(err) {
if (err) {
console.error('Ошибка добавления в группу:', err);
return res.status(500).json({ error: err.message });
}
if (this.changes > 0) {
// Обновляем группы пользователя в таблице users
db.get("SELECT groups FROM users WHERE id = ?", [userId], (err, userData) => {
if (err) {
return res.json({ success: true });
}
let groups = [];
try {
groups = JSON.parse(userData.groups || '[]');
} catch (e) {
groups = [];
}
// Получаем имя группы
db.get("SELECT name FROM user_groups WHERE id = ?", [groupId], (err, groupData) => {
if (groupData && !groups.includes(groupData.name)) {
groups.push(groupData.name);
db.run(
"UPDATE users SET groups = ?, updated_at = datetime('now') WHERE id = ?",
[JSON.stringify(groups), userId],
(updateErr) => {
if (updateErr) {
console.error('Ошибка обновления групп пользователя:', updateErr);
}
res.json({ success: true });
}
);
} else {
res.json({ success: true });
}
});
});
} else {
res.json({ success: true, message: 'Пользователь уже в группе' });
}
});
});
});
});
});
// API для удаления пользователя из группы
app.delete('/api/groups/:groupId/users/:userId', requireAuth, (req, res) => {
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
const { groupId, userId } = req.params;
// Удаляем пользователя из группы
db.run(`
DELETE FROM user_group_memberships
WHERE user_id = ? AND group_id = ?
`, [userId, groupId], function(err) {
if (err) {
console.error('Ошибка удаления из группы:', err);
return res.status(500).json({ error: err.message });
}
if (this.changes > 0) {
// Обновляем группы пользователя в таблице users
db.get("SELECT groups FROM users WHERE id = ?", [userId], (err, userData) => {
if (err) {
return res.json({ success: true });
}
let groups = [];
try {
groups = JSON.parse(userData.groups || '[]');
} catch (e) {
groups = [];
}
// Получаем имя группы
db.get("SELECT name FROM user_groups WHERE id = ?", [groupId], (err, groupData) => {
if (groupData) {
groups = groups.filter(group => group !== groupData.name);
db.run(
"UPDATE users SET groups = ?, updated_at = datetime('now') WHERE id = ?",
[JSON.stringify(groups), userId],
(updateErr) => {
if (updateErr) {
console.error('Ошибка обновления групп пользователя:', updateErr);
}
res.json({ success: true });
}
);
} else {
res.json({ success: true });
}
});
});
} else {
res.json({ success: true, message: 'Пользователь не был в группе' });
}
});
});
// API для создания группы
app.post('/api/groups', requireAuth, (req, res) => {
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
const { name, description, color, can_approve_documents } = req.body;
if (!name) {
return res.status(400).json({ error: 'Название группы обязательно' });
}
db.run(`
INSERT INTO user_groups (name, description, color, can_approve_documents)
VALUES (?, ?, ?, ?)
`, [name, description || '', color || '#3498db', can_approve_documents || false], function(err) {
if (err) {
console.error('Ошибка создания группы:', err);
res.status(500).json({ error: err.message });
return;
}
res.json({ success: true, id: this.lastID });
});
});
// Инициализация сервера
async function initializeServer() {
console.log('🚀 Инициализация сервера...');
try {
// 1. Инициализируем базу данных
console.log('🔧 Инициализация базы данных...');
await initializeDatabase();
// 2. Получаем объект БД
db = getDb();
console.log('✅ База данных готова');
const { initializeDocumentTypes } = require('./init-document-types');
initializeDocumentTypes(db);
console.log('✅ Сервис document готов');
// 3. Настраиваем authService с БД
authService.setDatabase(db);
console.log('✅ Сервис аутентификации готов');
// 4. Настраиваем endpoint'ы для задач (upload уже настроен в начале файла)
setupTaskEndpoints(app, db, upload);
console.log('✅ Endpoint\'ы задач настроены');
// 5. Загружаем админ роутер динамически
try {
adminRouter = require('./admin-server');
console.log('Admin router loaded:', adminRouter);
console.log('Type:', typeof adminRouter);
if (adminRouter && typeof adminRouter === 'function') {
app.use(adminRouter);
console.log('✅ Админ роутер подключен');
} else {
console.error('❌ Admin router is not a valid middleware function');
// Создаем заглушку, чтобы сервер работал
const express = require('express');
const stubRouter = express.Router();
stubRouter.get('*', (req, res) => {
res.status(501).json({ error: 'Admin router not available' });
});
app.use(stubRouter);
console.log('⚠️ Используется заглушка для админ роутера');
}
} catch (error) {
console.error('❌ Ошибка загрузки админ роутера:', error.message);
console.error('Stack:', error.stack);
// Создаем заглушку, чтобы сервер не падал
const express = require('express');
const stubRouter = express.Router();
stubRouter.get('*', (req, res) => {
res.status(503).json({
error: 'Admin panel temporarily unavailable',
message: error.message
});
});
app.use(stubRouter);
console.log('⚠️ Создана заглушка для админ роутера из-за ошибки');
}
// 6. Помечаем сервер как готовый
serverReady = true;
console.log('✅ Сервер полностью инициализирован');
} catch (error) {
console.error('❌ Ошибка инициализации сервера:', error.message);
console.error(error.stack);
process.exit(1);
}
}
// Запускаем инициализацию и сервер
initializeServer().then(() => {
// Запускаем сервер
app.listen(PORT, () => {
console.log(`🚀 CRM сервер запущен на порту ${PORT}`);
console.log(`🌐 Откройте http://localhost:${PORT} в браузере`);
console.log('📁 Данные хранятся в папке:', path.join(__dirname, 'data'));
console.log('👤 Тестовые пользователи:');
console.log(' - Логин: director, Пароль: director123 (Администратор)');
console.log(' - Логин: zavuch, Пароль: zavuch123');
console.log(' - Логин: teacher, Пароль: teacher123');
console.log('🔐 LDAP авторизация доступна для пользователей школы');
console.log(`👥 Разрешенные группы: ${process.env.ALLOWED_GROUPS}`);
console.log('📢 Система уведомлений активна');
// Запускаем фоновые задачи
setInterval(checkOverdueTasks, 60000);
setInterval(checkUpcomingDeadlines, 60000);
});
}).catch(error => {
console.error('❌ Не удалось запустить сервер:', error);
process.exit(1);
});
// Экспортируем приложение для тестирования
module.exports = app;