Files
minicrm/server.js
2026-02-25 22:08:57 +05:00

1587 lines
62 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');
// doc
const apiDoc = require('./api-doc');
// Подключаем API для управления исполнителями
const userManagementAPI = require('./api-users');
//
const api2Groups = require('./api2-groups');
//
const chatAPI = require('./api-chat');
// Подключаем API для управления межсервисным взаимодействием
//const { setupUpravlenieEndpoints } = require('./upravlenie-service');
const apiKeysModule = require('./api-keys');
//
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();
};
// Middleware для проверки прав администратора
const requireAdmin = (req, res, next) => {
if (!req.session || !req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав. Требуется роль администратора' });
}
next();
};
// Middleware для проверки прав на управление задачами (admin или tasks)
const requireTasksAccess = (req, res, next) => {
if (!req.session || !req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
if (req.session.user.role !== 'admin' && req.session.user.role !== 'tasks') {
return res.status(403).json({ error: 'Недостаточно прав. Требуется роль admin или tasks' });
}
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 userGroups = [];
try {
userGroups = JSON.parse(user.groups || '[]');
} catch (e) {
userGroups = [];
}
// Получаем все группы из .env
const allowedGroups = process.env.ALLOWED_GROUPS ?
process.env.ALLOWED_GROUPS.split(',').map(g => g.trim()) : [];
const secretaryGroups = process.env.SECRETARY_GROUPS ?
process.env.SECRETARY_GROUPS.split(',').map(g => g.trim()) : [];
const helpGroups = process.env.HELP_GROUPS ?
process.env.HELP_GROUPS.split(',').map(g => g.trim()) : [];
const itHelpGroups = process.env.ITHELP_GROUPS ?
process.env.ITHELP_GROUPS.split(',').map(g => g.trim()) : [];
const requestGroups = process.env.REQUEST_GROUPS ?
process.env.REQUEST_GROUPS.split(',').map(g => g.trim()) : [];
const tasksGroups = process.env.TASKS_GROUPS ?
process.env.TASKS_GROUPS.split(',').map(g => g.trim()) : [];
// Определяем роль пользователя на основе групп
// ВСЕ пользователи являются 'teacher' по умолчанию
let actualRole = 'teacher';
if (userGroups && userGroups.length > 0) {
// Определяем наивысшую роль
// Порядок приоритета: admin > secretary > help > ithelp > request > tasks
if (userGroups.some(group => allowedGroups.includes(group))) {
actualRole = 'admin';
} else if (userGroups.some(group => secretaryGroups.includes(group))) {
actualRole = 'secretary';
} else if (userGroups.some(group => helpGroups.includes(group))) {
actualRole = 'help';
} else if (userGroups.some(group => itHelpGroups.includes(group))) {
actualRole = 'ithelp';
} else if (userGroups.some(group => requestGroups.includes(group))) {
actualRole = 'request';
} else if (userGroups.some(group => tasksGroups.includes(group))) {
actualRole = 'tasks';
}
// Если ни одна группа не совпала, остается '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: 'Не аутентифицирован' });
}
});
// Получаем актуальные группы пользователя из новой структуры
app.get('/api/user_v2', (req, res) => {
if (req.session.user) {
db.all(`
SELECT g.name
FROM user_groups g
JOIN user_group_memberships ugm ON g.id = ugm.group_id
WHERE ugm.user_id = ?
`, [req.session.user.id], (err, rows) => {
if (err) {
console.error('❌ Ошибка получения групп пользователя:', err);
return res.json({ user: req.session.user });
}
const userGroups = rows.map(row => row.name);
// Обновляем группы в сессии
req.session.user.groups = userGroups;
// Обновляем поле groups в старой таблице для совместимости
db.run(
"UPDATE users SET groups = ?, updated_at = datetime('now') WHERE id = ?",
[JSON.stringify(userGroups), req.session.user.id]
);
// Проверяем права администратора
const allowedGroups = process.env.ALLOWED_GROUPS ?
process.env.ALLOWED_GROUPS.split(',').map(g => g.trim()) : [];
const isAdmin = userGroups.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.status(401).json({ error: 'Не аутентифицирован' });
}
});
// API для получения пользователей группы "help"
app.get('/api/users/group/help', requireAuth, (req, res) => {
db.all(`
SELECT u.id, u.login, u.name, u.email, u.role, u.auth_type
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 = 'help'
ORDER BY u.name
`, [], (err, rows) => {
if (err) {
console.error('❌ Ошибка получения пользователей группы help:', err);
res.status(500).json({ error: err.message });
return;
}
res.json(rows);
});
});
// API для получения пользователей группы "doc"
app.get('/api/users/group/doc', requireAuth, (req, res) => {
db.all(`
SELECT u.id, u.login, u.name, u.email, u.role, u.auth_type
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 = 'doc'
ORDER BY u.name
`, [], (err, rows) => {
if (err) {
console.error('❌ Ошибка получения пользователей группы doc:', err);
res.status(500).json({ error: err.message });
return;
}
res.json(rows);
});
});
// 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 || '';
console.log('📋 Запрос всех пользователей для выбора');
let query = `
SELECT id, login, name, email, role, auth_type
FROM users
WHERE 1=1
`;
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) {
console.error('❌ Ошибка при запросе пользователей:', err);
res.status(500).json({ error: err.message });
return;
}
console.log(`✅ Загружено ${rows.length} пользователей для выбора`);
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) {
return res.redirect('/');
}
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) {
return res.redirect('/');
}
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('/');
}
const { role, name } = req.session.user;
const hasAccess = role === 'admin' || role === 'tasks';
if (!hasAccess) {
return res.status(403).send('в разработке');
}
res.sendFile(path.join(__dirname, 'public/doc.html'));
});
// Страница поддержка
app.get('/help', (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/help.html'));
});
// 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.put('/api/users/:userId', requireAuth, requireAdmin, (req, res) => {
const { userId } = req.params;
const { name, email, role, auth_type, is_active } = req.body;
// Проверяем существование пользователя
db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => {
if (err) {
console.error('❌ Ошибка проверки пользователя:', err);
return res.status(500).json({ error: err.message });
}
if (!user) {
return res.status(404).json({ error: 'Пользователь не найден' });
}
// Формируем запрос на обновление
const updates = [];
const params = [];
if (name !== undefined) {
updates.push('name = ?');
params.push(name);
}
if (email !== undefined) {
updates.push('email = ?');
params.push(email);
}
if (role !== undefined) {
updates.push('role = ?');
params.push(role);
}
if (auth_type !== undefined) {
updates.push('auth_type = ?');
params.push(auth_type);
}
if (is_active !== undefined) {
updates.push('is_active = ?');
params.push(is_active ? 1 : 0);
}
if (updates.length === 0) {
return res.status(400).json({ error: 'Нет данных для обновления' });
}
updates.push('updated_at = CURRENT_TIMESTAMP');
params.push(userId);
const query = `UPDATE users SET ${updates.join(', ')} WHERE id = ?`;
db.run(query, params, function(err) {
if (err) {
console.error('❌ Ошибка обновления пользователя:', err);
return res.status(500).json({ error: err.message });
}
res.json({
success: true,
message: 'Пользователь успешно обновлен',
changes: this.changes
});
});
});
});
// 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 });
});
});
// API для удаления файла из задачи
app.delete('/api/tasks/:taskId/files/:fileId', requireAuth, (req, res) => {
const { taskId, fileId } = req.params;
const userId = req.session.user.id;
const userRole = req.session.user.role;
// Получаем информацию о файле и задаче
db.get(`
SELECT tf.*, t.created_by, t.id as task_id
FROM task_files tf
JOIN tasks t ON tf.task_id = t.id
WHERE tf.id = ? AND tf.task_id = ?
`, [fileId, taskId], (err, file) => {
if (err) {
console.error('❌ Ошибка при поиске файла:', err);
return res.status(500).json({ error: err.message });
}
if (!file) {
return res.status(404).json({ error: 'Файл не найден' });
}
// Проверяем права на удаление
const canDelete =
userRole === 'admin' ||
userRole === 'tasks' ||
parseInt(file.created_by) === parseInt(userId) || // Автор задачи
parseInt(file.user_id) === parseInt(userId); // Загрузивший файл
if (!canDelete) {
return res.status(403).json({ error: 'У вас нет прав для удаления этого файла' });
}
// Удаляем файл из базы данных
db.run('DELETE FROM task_files WHERE id = ?', [fileId], function(deleteErr) {
if (deleteErr) {
console.error('❌ Ошибка удаления файла из БД:', deleteErr);
return res.status(500).json({ error: deleteErr.message });
}
// Удаляем физический файл с диска
if (file.file_path && fs.existsSync(file.file_path)) {
try {
fs.unlinkSync(file.file_path);
console.log(`✅ Файл ${file.file_path} удален с диска`);
} catch (unlinkErr) {
console.error('⚠️ Ошибка удаления файла с диска:', unlinkErr);
// Не возвращаем ошибку, так как запись в БД уже удалена
}
}
// Логируем действие
const { logActivity } = require('./database');
if (logActivity) {
logActivity(taskId, userId, 'FILE_DELETED', `Удален файл: ${file.original_name || fileId}`);
}
console.log(`✅ Файл ${fileId} удален из задачи ${taskId}`);
res.json({
success: true,
message: 'Файл успешно удален',
file_id: fileId
});
});
});
});
// API для массового удаления файлов
app.delete('/api/tasks/:taskId/files/batch-delete', requireAuth, (req, res) => {
const { taskId } = req.params;
const { file_ids } = req.body;
const userId = req.session.user.id;
const userRole = req.session.user.role;
if (!file_ids || !Array.isArray(file_ids) || file_ids.length === 0) {
return res.status(400).json({ error: 'Не указаны файлы для удаления' });
}
// Проверяем существование задачи
db.get('SELECT created_by FROM tasks WHERE id = ?', [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
// Создаем плейсхолдеры для SQL запроса
const placeholders = file_ids.map(() => '?').join(',');
// Получаем информацию о файлах
db.all(`
SELECT id, file_path, original_name, user_id, created_by
FROM task_files
WHERE id IN (${placeholders}) AND task_id = ?
`, [...file_ids, taskId], async (err, files) => {
if (err) {
console.error('❌ Ошибка при поиске файлов:', err);
return res.status(500).json({ error: err.message });
}
// Проверяем права для каждого файла
const canDeleteAll = files.every(file => {
return userRole === 'admin' ||
userRole === 'tasks' ||
parseInt(task.created_by) === parseInt(userId) || // Автор задачи
parseInt(file.user_id) === parseInt(userId); // Загрузивший файл
});
if (!canDeleteAll) {
return res.status(403).json({ error: 'У вас нет прав для удаления некоторых файлов' });
}
const deletedIds = [];
const failedIds = [];
// Удаляем файлы по одному
for (const file of files) {
try {
// Удаляем запись из БД
await new Promise((resolve, reject) => {
db.run('DELETE FROM task_files WHERE id = ?', [file.id], function(deleteErr) {
if (deleteErr) reject(deleteErr);
else resolve();
});
});
// Удаляем физический файл
if (file.file_path && fs.existsSync(file.file_path)) {
try {
fs.unlinkSync(file.file_path);
} catch (unlinkErr) {
console.error(`⚠️ Ошибка удаления файла ${file.file_path}:`, unlinkErr);
}
}
deletedIds.push(file.id);
} catch (deleteErr) {
console.error(`❌ Ошибка удаления файла ${file.id}:`, deleteErr);
failedIds.push(file.id);
}
}
// Логируем действие
const { logActivity } = require('./database');
if (logActivity && deletedIds.length > 0) {
logActivity(taskId, userId, 'FILES_DELETED', `Удалено файлов: ${deletedIds.length}`);
}
res.json({
success: true,
deleted_count: deletedIds.length,
deleted_ids: deletedIds,
failed_ids: failedIds,
message: `Успешно удалено ${deletedIds.length} файлов`
});
});
});
});
// API для получения информации о файле
app.get('/api/files/:fileId', requireAuth, (req, res) => {
const { fileId } = req.params;
db.get(`
SELECT tf.*, t.created_by, 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) {
return res.status(500).json({ error: err.message });
}
if (!file) {
return res.status(404).json({ error: 'Файл не найден' });
}
const { checkTaskAccess } = require('./database');
checkTaskAccess(req.session.user.id, file.task_id, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(403).json({ error: 'Нет доступа к файлу' });
}
res.json(file);
});
});
});
// Инициализация сервера
async function initializeServer() {
console.log('🚀 Инициализация сервера...');
try {
// 1. Инициализируем базу данных
console.log('🔧 Инициализация базы данных...');
await initializeDatabase();
// 2. Получаем объект БД
db = getDb();
console.log('✅ База данных готова');
// 3. Настраиваем authService с БД
authService.setDatabase(db);
console.log('✅ Сервис аутентификации готов');
// 4. Настраиваем endpoint'ы для задач (upload уже настроен в начале файла)
setupTaskEndpoints(app, db, upload);
console.log('✅ Endpoint\'ы задач настроены');
// 5. Подключаем API для управления исполнителями
userManagementAPI(app, db);
console.log('✅ API для управления исполнителями подключено');
apiDoc(app, db, upload);
console.log('✅ Endpoint\'ы документов настроены');
// 6. Загружаем админ роутер динамически
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('⚠️ Создана заглушка для админ роутера из-за ошибки');
}
// 7. Подключаем API для внешних идентификаторов
api2Groups(app, db);
console.log('✅ API для внешних идентификаторов подключено');
// 8. Подключаем API для управления межсервисным взаимодействием
//const upravlenieService = setupUpravlenieEndpoints(app, db);
//console.log('✅ API для управления межсервисным взаимодействием подключено');
// 9. Подключаем модуль API ключей
apiKeysModule(app, db, upload);
console.log('✅ Модуль API ключей подключен');
// 10. Помечаем сервер как готовый
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('📢 Система уведомлений активна');
// Подключаем API для чата
chatAPI(app, db, upload);
console.log('✅ API для чата задач подключено');
// Запускаем фоновые задачи
setInterval(checkOverdueTasks, 60000);
setInterval(checkUpcomingDeadlines, 60000);
});
}).catch(error => {
console.error('❌ Не удалось запустить сервер:', error);
process.exit(1);
});
// Экспортируем приложение для тестирования
module.exports = app;