Files
minicrm/server.js
Калугин Олег Александрович 21b40be946 postgres
2025-12-18 09:39:18 +00:00

1664 lines
66 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.
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const session = require('express-session');
const fetch = require('node-fetch');
require('dotenv').config();
const { db, logActivity, createUserTaskFolder, saveTaskMetadata, updateTaskMetadata, checkTaskAccess } = require('./database');
const authService = require('./auth');
const adminRouter = require('./admin-server');
const postgresLogger = require('./postgres');
const app = express();
const PORT = process.env.PORT || 3000;
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
}
}));
app.use(adminRouter);
const requireAuth = (req, res, next) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Требуется аутентификация' });
}
next();
};
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const taskId = req.body.taskId || req.params.taskId;
const userLogin = req.session.user.login;
if (taskId) {
const userFolder = createUserTaskFolder(taskId, userLogin);
cb(null, userFolder);
} else {
const tempDir = path.join(__dirname, 'data', 'uploads', 'temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
cb(null, tempDir);
}
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 300 * 1024 * 1024,
files: 15
}
});
const createDirIfNotExists = (dirPath) => {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
};
function checkIfOverdue(dueDate, status) {
if (!dueDate || status === 'completed') return false;
const now = new Date();
const due = new Date(dueDate);
return due < now;
}
function checkOverdueTasks() {
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]
);
logActivity(assignment.task_id, assignment.user_id, 'STATUS_CHANGED', 'Задача просрочена');
});
});
}
function checkUpcomingDeadlines() {
const now = new Date();
const in48Hours = new Date(now.getTime() + 48 * 60 * 60 * 1000).toISOString();
const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
const nowISO = now.toISOString();
const query = `
SELECT DISTINCT ta.*, t.title, t.description, t.created_by, u.name as user_name, u.email as user_email,
creator.name as creator_name, creator.email as creator_email
FROM task_assignments ta
JOIN tasks t ON ta.task_id = t.id
JOIN users u ON ta.user_id = u.id
JOIN users creator ON t.created_by = creator.id
WHERE ta.due_date IS NOT NULL
AND ta.due_date > ?
AND ta.due_date <= ?
AND ta.status NOT IN ('completed', 'overdue')
AND t.status = 'active'
AND t.closed_at IS NULL
`;
db.all(query, [nowISO, in48Hours], async (err, assignments) => {
if (err) {
console.error('Ошибка при проверке сроков задач:', err);
return;
}
for (const assignment of assignments) {
const dueDate = new Date(assignment.due_date);
const timeLeft = dueDate.getTime() - now.getTime();
const hoursLeft = Math.floor(timeLeft / (60 * 60 * 1000));
if (hoursLeft <= 48 && hoursLeft > 24) {
await sendDeadlineNotification(assignment, 48);
} else if (hoursLeft <= 24) {
await sendDeadlineNotification(assignment, 24);
}
}
});
}
async function sendDeadlineNotification(assignment, hoursLeft) {
try {
if (!process.env.NOTIFICATION_SERVICE_URL ||
!process.env.NOTIFICATION_SERVICE_LOGIN ||
!process.env.NOTIFICATION_SERVICE_PASSWORD) {
return;
}
const notificationKey = `deadline_${hoursLeft}h_task_${assignment.task_id}_user_${assignment.user_id}`;
const lastSent = await getLastNotificationSent(notificationKey);
const now = new Date();
if (lastSent) {
const timeSinceLast = now.getTime() - new Date(lastSent).getTime();
if (timeSinceLast < 12 * 60 * 60 * 1000) {
return;
}
}
const subject = `⚠️ До окончания срока задачи осталось менее ${hoursLeft} часов`;
const content = `Задача: ${assignment.title}\n\n` +
`Описание: ${assignment.description || 'Без описания'}\n` +
`Срок выполнения: ${new Date(assignment.due_date).toLocaleString('ru-RU')}\n` +
`Осталось времени: ${hoursLeft} часов\n\n` +
`Пожалуйста, завершите задачу в срок.`;
const recipients = [
{ id: assignment.user_id, name: assignment.user_name, email: assignment.user_email },
{ id: assignment.created_by, name: assignment.creator_name, email: assignment.creator_email }
].filter((value, index, self) =>
self.findIndex(r => r.id === value.id) === index
);
const recipientIds = recipients.map(r => r.id);
const authHeader = Buffer.from(
`${process.env.NOTIFICATION_SERVICE_LOGIN}:${process.env.NOTIFICATION_SERVICE_PASSWORD}`
).toString('base64');
const FormData = require('form-data');
const formData = new FormData();
formData.append('subject', subject);
formData.append('content', content);
formData.append('recipients', JSON.stringify(recipientIds));
formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk']));
const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, {
method: 'POST',
headers: {
'Authorization': `Basic ${authHeader}`
},
body: formData
});
if (response.ok) {
await saveNotificationSent(notificationKey);
console.log(`✅ Уведомление о сроке (${hoursLeft}ч) отправлено для задачи ${assignment.task_id}`);
}
} catch (error) {
console.error('❌ Ошибка отправки уведомления о сроке:', error);
}
}
function getLastNotificationSent(key) {
return new Promise((resolve) => {
db.get("SELECT created_at FROM notification_logs WHERE notification_key = ? ORDER BY created_at DESC LIMIT 1",
[key], (err, row) => {
resolve(row ? row.created_at : null);
}
);
});
}
function saveNotificationSent(key) {
db.run("INSERT INTO notification_logs (notification_key) VALUES (?)", [key]);
}
function encodeBasicAuth(login, password) {
return Buffer.from(`${login}:${password}`).toString('base64');
}
async function sendTaskNotifications(type, taskId, taskTitle, taskDescription, authorId, comment = '', status = '', userName = '') {
try {
if (!process.env.NOTIFICATION_SERVICE_URL ||
!process.env.NOTIFICATION_SERVICE_LOGIN ||
!process.env.NOTIFICATION_SERVICE_PASSWORD) {
console.log('⚠️ Настройки сервиса уведомлений не заданы');
// Логируем в PostgreSQL даже если уведомления не отправляются
await logNotificationToPostgres({
type,
taskId,
taskTitle,
taskDescription,
authorId,
comment,
status,
userName,
error: 'Сервис уведомлений не настроен'
});
return;
}
console.log(`📢 Начинаем отправку уведомлений для задачи ${taskId}:`);
console.log(` Тип: ${type}, Автор действия: ${authorId}, Название: ${taskTitle}`);
// Получаем заказчика (создателя задачи) ОТДЕЛЬНО
const creator = await new Promise((resolve, reject) => {
db.get(`
SELECT t.created_by as user_id, u.name as user_name, u.login as user_login, u.email
FROM tasks t
LEFT JOIN users u ON t.created_by = u.id
WHERE t.id = ?
`, [taskId], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
// Получаем исполнителей ОТДЕЛЬНО
const assignees = await new Promise((resolve, reject) => {
db.all(`
SELECT ta.user_id, u.name as user_name, u.login as user_login, u.email
FROM task_assignments ta
LEFT JOIN users u ON ta.user_id = u.id
WHERE ta.task_id = ?
`, [taskId], (err, rows) => {
if (err) reject(err);
else resolve(rows || []);
});
});
// Собираем всех участников
const participants = [];
if (creator) {
participants.push({
...creator,
role: 'creator',
is_creator: true
});
}
if (assignees && assignees.length > 0) {
assignees.forEach(assignee => {
participants.push({
...assignee,
role: 'assignee',
is_creator: false
});
});
}
// Получаем информацию об авторе действия
const author = await new Promise((resolve, reject) => {
db.get("SELECT name, login FROM users WHERE id = ?", [authorId], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
const authorName = author ? author.name : 'Система';
const authorLogin = author ? author.login : 'system';
// Логируем в PostgreSQL
const postgresLogIds = await logNotificationToPostgres({
type,
taskId,
taskTitle,
taskDescription,
authorId,
authorName,
authorLogin,
participants,
comment,
status,
userName
});
let subject, content;
switch (type) {
case 'created':
subject = `Новая задача: ${taskTitle}`;
content = `Создана новая задача:\n\n` +
`📋 ${taskTitle}\n` +
`📝 ${taskDescription || 'Без описания'}\n` +
`👤 Автор: ${authorName}\n\n` +
`Для просмотра перейдите в систему управления задачами.`;
break;
case 'updated':
subject = `Обновлена задача: ${taskTitle}`;
content = `Задача была обновлена:\n\n` +
`📋 ${taskTitle}\n` +
`📝 ${taskDescription || 'Без описания'}\n` +
`👤 Изменено: ${authorName}\n\n` +
`Для просмотра изменений перейдите в систему управления задачами.`;
break;
case 'rework':
subject = `Задача возвращена на доработку: ${taskTitle}`;
content = `Задача возвращена на доработку:\n\n` +
`📋 ${taskTitle}\n` +
`📝 Комментарий: ${comment}\n` +
`👤 Автор замечания: ${authorName}\n\n` +
`Пожалуйста, исправьте замечания и обновите статус задачи.`;
break;
case 'closed':
subject = `Задача закрыта: ${taskTitle}`;
content = `Задача была закрыта:\n\n` +
`📋 ${taskTitle}\n` +
`👤 Закрыта: ${authorName}\n\n` +
`Задача завершена и перемещена в архив.`;
break;
case 'status_changed':
const statusText = getStatusText(status);
subject = `Изменен статус задачи: ${taskTitle}`;
content = `Статус задачи изменен:\n\n` +
`📋 ${taskTitle}\n` +
`🔄 Новый статус: ${statusText}\n` +
`👤 Изменил: ${userName || authorName}\n\n` +
`Для просмотра перейдите в систему управления задачами.`;
break;
default:
console.log(`⚠️ Неизвестный тип уведомления: ${type}`);
return;
}
// Фильтруем получателей: исключаем автора действия
const recipientIds = participants
.filter(p => {
const shouldExclude = p.user_id === authorId;
if (shouldExclude) {
console.log(` ✋ Исключаем автора действия: ${p.user_name} (ID: ${p.user_id})`);
}
return !shouldExclude;
})
.map(p => p.user_id);
if (recipientIds.length === 0) {
console.log('❌ Нет получателей для уведомления (все участники - автор изменения)');
// Обновляем статус в PostgreSQL
await updatePostgresLogStatus(postgresLogIds, 'no_recipients', 'Нет получателей после фильтрации');
return;
}
const authHeader = encodeBasicAuth(
process.env.NOTIFICATION_SERVICE_LOGIN,
process.env.NOTIFICATION_SERVICE_PASSWORD
);
const FormData = require('form-data');
const formData = new FormData();
formData.append('subject', subject);
formData.append('content', content);
formData.append('recipients', JSON.stringify(recipientIds));
formData.append('deliveryMethods', JSON.stringify(['email', 'telegram', 'vk']));
console.log(`🚀 Отправляем запрос на сервис уведомлений...`);
try {
const response = await fetch(process.env.NOTIFICATION_SERVICE_URL, {
method: 'POST',
headers: {
'Authorization': `Basic ${authHeader}`
},
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log(`✅ Уведомления успешно отправлены для задачи ${taskId}`);
// Обновляем статус в PostgreSQL
await updatePostgresLogStatus(postgresLogIds, 'sent', null, new Date().toISOString());
console.log(` Результат от сервиса:`, result);
} catch (error) {
console.error('❌ Ошибка отправки уведомлений:', error);
// Обновляем статус в PostgreSQL
await updatePostgresLogStatus(postgresLogIds, 'failed', error.message);
console.error(' Детали ошибки:', {
taskId,
type,
authorId,
errorMessage: error.message,
stack: error.stack
});
}
} catch (error) {
console.error('❌ Общая ошибка при обработке уведомлений:', error);
}
}
// Вспомогательные функции для работы с PostgreSQL
async function logNotificationToPostgres(data) {
try {
const {
type,
taskId,
taskTitle,
taskDescription,
authorId,
authorName,
authorLogin,
participants = [],
comment = '',
status = '',
userName = '',
error = ''
} = data;
// Создаем сообщение
let messageContent = '';
switch (type) {
case 'created':
messageContent = `Создана новая задача: ${taskTitle}`;
break;
case 'updated':
messageContent = `Обновлена задача: ${taskTitle}`;
break;
case 'rework':
messageContent = `Задача возвращена на доработку: ${taskTitle}. Комментарий: ${comment}`;
break;
case 'closed':
messageContent = `Задача закрыта: ${taskTitle}`;
break;
case 'status_changed':
messageContent = `Изменен статус задачи: ${taskTitle}. Новый статус: ${getStatusText(status)}`;
break;
}
// Логируем для каждого получателя отдельно
const recipientsToNotify = participants.filter(p => p.user_id !== authorId);
const logIds = [];
for (const recipient of recipientsToNotify) {
const logId = await postgresLogger.logNotification({
taskId,
taskTitle,
taskDescription,
notificationType: type,
authorId,
authorName,
authorLogin,
recipientId: recipient.user_id,
recipientName: recipient.user_name,
recipientLogin: recipient.user_login,
messageContent: `${messageContent}\n\nЗадача: ${taskTitle}\nОписание: ${taskDescription || 'Без описания'}\nАвтор: ${authorName}`,
messageSubject: getNotificationSubject(type, taskTitle),
deliveryMethods: ['email', 'telegram', 'vk'],
comments: error ? `Ошибка: ${error}` : comment
});
if (logId) {
logIds.push(logId);
}
}
return logIds;
} catch (error) {
console.error('❌ Ошибка логирования в PostgreSQL:', error);
return [];
}
}
async function updatePostgresLogStatus(logIds, status, errorMessage = null, sentAt = null) {
if (!logIds || logIds.length === 0) return;
for (const logId of logIds) {
await postgresLogger.updateNotificationStatus(logId, status, errorMessage, sentAt);
}
}
function getNotificationSubject(type, taskTitle) {
switch (type) {
case 'created':
return `Новая задача: ${taskTitle}`;
case 'updated':
return `Обновлена задача: ${taskTitle}`;
case 'rework':
return `Задача возвращена на доработку: ${taskTitle}`;
case 'closed':
return `Задача закрыта: ${taskTitle}`;
case 'status_changed':
return `Изменен статус задачи: ${taskTitle}`;
default:
return `Уведомление по задаче: ${taskTitle}`;
}
}
function getStatusText(status) {
const statusMap = {
'assigned': 'Назначена',
'in_progress': 'В работе',
'completed': 'Завершена',
'overdue': 'Просрочена',
'rework': 'На доработке'
};
return statusMap[status] || status;
}
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);
res.status(500).json({ error: 'Ошибка сервера при авторизации' });
}
});
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') {
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: 'Не аутентифицирован' });
}
});
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);
});
});
app.get('/api/tasks', requireAuth, (req, res) => {
const userId = req.session.user.id;
const showDeleted = req.session.user.role === 'admin' && req.query.showDeleted === 'true';
const search = req.query.search || '';
const statusFilter = req.query.status || 'active,in_progress,assigned,overdue,rework';
const creatorFilter = req.query.creator || '';
const assigneeFilter = req.query.assignee || '';
const deadlineFilter = req.query.deadline || '';
let query = `
SELECT DISTINCT
t.*,
u.name as creator_name,
u.login as creator_login,
ot.title as original_task_title,
ou.name as original_creator_name,
GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids,
GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names
FROM tasks t
LEFT JOIN users u ON t.created_by = u.id
LEFT JOIN tasks ot ON t.original_task_id = ot.id
LEFT JOIN users ou ON ot.created_by = ou.id
LEFT JOIN task_assignments ta ON t.id = ta.task_id
LEFT JOIN users u2 ON ta.user_id = u2.id
WHERE 1=1
`;
const params = [];
if (req.session.user.role !== 'admin') {
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
params.push(userId, userId);
}
if (!showDeleted) {
query += " AND t.status = 'active'";
}
if (statusFilter && statusFilter !== 'all') {
const statuses = statusFilter.split(',');
if (statuses.includes('closed')) {
if (req.session.user.role !== 'admin') {
query += ` AND (t.closed_at IS NOT NULL AND t.created_by = ?)`;
params.push(userId);
} else {
query += ` AND t.closed_at IS NOT NULL`;
}
} else {
query += ` AND t.closed_at IS NULL`;
if (statuses.length > 0 && !statuses.includes('all')) {
query += ` AND EXISTS (
SELECT 1 FROM task_assignments ta2
WHERE ta2.task_id = t.id AND ta2.status IN (${statuses.map(() => '?').join(',')})
)`;
statuses.forEach(status => params.push(status));
}
}
} else {
if (req.session.user.role !== 'admin') {
query += ` AND (t.closed_at IS NULL OR t.created_by = ?)`;
params.push(userId);
}
}
if (creatorFilter) {
query += ` AND t.created_by = ?`;
params.push(creatorFilter);
}
if (assigneeFilter) {
query += ` AND ta.user_id = ?`;
params.push(assigneeFilter);
}
if (deadlineFilter) {
const now = new Date();
let hours = 48;
if (deadlineFilter === '24h') hours = 24;
const deadlineTime = new Date(now.getTime() + hours * 60 * 60 * 1000);
const deadlineISO = deadlineTime.toISOString();
const nowISO = now.toISOString();
query += ` AND ta.due_date IS NOT NULL
AND ta.due_date > ?
AND ta.due_date <= ?
AND ta.status NOT IN ('completed', 'overdue')`;
params.push(nowISO, deadlineISO);
}
if (search) {
query += ` AND (t.title LIKE ? OR t.description LIKE ?)`;
const searchPattern = `%${search}%`;
params.push(searchPattern, searchPattern);
}
query += " GROUP BY t.id ORDER BY t.created_at DESC";
db.all(query, params, (err, tasks) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const taskPromises = tasks.map(task => {
return new Promise((resolve) => {
db.all(`
SELECT ta.*, u.name as user_name, u.login as user_login
FROM task_assignments ta
LEFT JOIN users u ON ta.user_id = u.id
WHERE ta.task_id = ?
`, [task.id], (err, assignments) => {
if (err) {
task.assignments = [];
resolve(task);
return;
}
assignments.forEach(assignment => {
if (checkIfOverdue(assignment.due_date, assignment.status) && assignment.status !== 'completed') {
assignment.status = 'overdue';
}
});
task.assignments = assignments || [];
resolve(task);
});
});
});
Promise.all(taskPromises).then(completedTasks => {
res.json(completedTasks);
});
});
});
app.get('/api/tasks/no-date', requireAuth, (req, res) => {
const userId = req.session.user.id;
const query = `
SELECT DISTINCT
t.*,
u.name as creator_name,
u.login as creator_login,
ot.title as original_task_title,
ou.name as original_creator_name,
GROUP_CONCAT(DISTINCT ta.user_id) as assigned_user_ids,
GROUP_CONCAT(DISTINCT u2.name) as assigned_user_names
FROM tasks t
LEFT JOIN users u ON t.created_by = u.id
LEFT JOIN tasks ot ON t.original_task_id = ot.id
LEFT JOIN users ou ON ot.created_by = ou.id
LEFT JOIN task_assignments ta ON t.id = ta.task_id
LEFT JOIN users u2 ON ta.user_id = u2.id
WHERE t.status = 'active'
AND t.closed_at IS NULL
AND (t.due_date IS NULL OR t.due_date = '')
AND (ta.due_date IS NULL OR ta.due_date = '')
`;
const params = [];
if (req.session.user.role !== 'admin') {
query += ` AND (t.created_by = ? OR ta.user_id = ?)`;
params.push(userId, userId);
}
query += " GROUP BY t.id ORDER BY t.created_at DESC";
db.all(query, params, (err, tasks) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const taskPromises = tasks.map(task => {
return new Promise((resolve) => {
db.all(`
SELECT ta.*, u.name as user_name, u.login as user_login
FROM task_assignments ta
LEFT JOIN users u ON ta.user_id = u.id
WHERE ta.task_id = ?
`, [task.id], (err, assignments) => {
if (err) {
task.assignments = [];
resolve(task);
return;
}
task.assignments = assignments || [];
resolve(task);
});
});
});
Promise.all(taskPromises).then(completedTasks => {
res.json(completedTasks);
});
});
});
app.post('/api/tasks', requireAuth, upload.array('files', 15), (req, res) => {
const { title, description, assignedUsers, originalTaskId, dueDate } = req.body;
const createdBy = req.session.user.id;
if (!title) {
return res.status(400).json({ error: 'Название задачи обязательно' });
}
if (!dueDate) {
return res.status(400).json({ error: 'Дата и время выполнения обязательны' });
}
db.serialize(() => {
const startDate = new Date().toISOString();
db.run(
"INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date) VALUES (?, ?, ?, ?, ?, ?)",
[title, description, createdBy, originalTaskId || null, startDate, dueDate || null],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const taskId = this.lastID;
saveTaskMetadata(taskId, title, description, createdBy, originalTaskId, startDate, dueDate);
const action = originalTaskId ? 'TASK_COPIED' : 'TASK_CREATED';
const details = originalTaskId ?
`Создана копия задачи: ${title}` :
`Создана задача: ${title}`;
logActivity(taskId, createdBy, action, details);
if (req.files && req.files.length > 0) {
const userFolder = createUserTaskFolder(taskId, req.session.user.login);
req.files.forEach(file => {
const newPath = path.join(userFolder, path.basename(file.filename));
fs.renameSync(file.path, newPath);
const originalName = file.originalname;
db.run(
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
[taskId, createdBy, path.basename(file.filename), originalName, newPath, file.size]
);
logActivity(taskId, createdBy, 'FILE_UPLOADED', `Загружен файл: ${originalName}`);
});
const tempDir = path.join(__dirname, 'data', 'uploads', 'temp');
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
if (assignedUsers) {
const userIds = Array.isArray(assignedUsers) ? assignedUsers : [assignedUsers];
userIds.forEach(userId => {
db.run(
"INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)",
[taskId, userId, startDate, dueDate || null]
);
logActivity(taskId, createdBy, 'TASK_ASSIGNED', `Задача назначена пользователю ${userId}`);
});
sendTaskNotifications('created', taskId, title, description, createdBy);
}
res.json({
success: true,
taskId: taskId,
message: originalTaskId ? 'Копия задачи создана' : 'Задача успешно создана'
});
}
);
});
});
app.post('/api/tasks/:taskId/copy', requireAuth, (req, res) => {
const { taskId } = req.params;
const { assignedUsers, dueDate } = req.body;
const createdBy = req.session.user.id;
if (!dueDate) {
return res.status(400).json({ error: 'Дата и время выполнения обязательны для копии задачи' });
}
checkTaskAccess(createdBy, taskId, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' });
}
db.serialize(() => {
db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, originalTask) => {
if (err || !originalTask) {
return res.status(404).json({ error: 'Оригинальная задача не найдена' });
}
const newTitle = `Копия: ${originalTask.title}`;
const startDate = new Date().toISOString();
db.run(
"INSERT INTO tasks (title, description, created_by, original_task_id, start_date, due_date) VALUES (?, ?, ?, ?, ?, ?)",
[newTitle, originalTask.description, createdBy, taskId, startDate, dueDate || null],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const newTaskId = this.lastID;
saveTaskMetadata(newTaskId, newTitle, originalTask.description, createdBy, taskId, startDate, dueDate);
logActivity(newTaskId, createdBy, 'TASK_COPIED', `Создана копия задачи: ${newTitle}`);
db.all("SELECT * FROM task_files WHERE task_id = ?", [taskId], (err, originalFiles) => {
if (!err && originalFiles && originalFiles.length > 0) {
originalFiles.forEach(originalFile => {
const originalFilePath = originalFile.file_path;
const newFilename = Date.now() + '-' + Math.round(Math.random() * 1E9) + path.extname(originalFile.original_name);
const userFolder = createUserTaskFolder(newTaskId, req.session.user.login);
const newFilePath = path.join(userFolder, newFilename);
if (fs.existsSync(originalFilePath)) {
fs.copyFileSync(originalFilePath, newFilePath);
db.run(
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
[newTaskId, createdBy, newFilename, originalFile.original_name, newFilePath, originalFile.file_size]
);
logActivity(newTaskId, createdBy, 'FILE_COPIED', `Скопирован файл: ${originalFile.original_name}`);
}
});
}
});
if (assignedUsers && assignedUsers.length > 0) {
assignedUsers.forEach(userId => {
db.run(
"INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)",
[newTaskId, userId, startDate, dueDate || null]
);
});
logActivity(newTaskId, createdBy, 'TASK_ASSIGNED', `Задача назначена пользователям: ${assignedUsers.join(', ')}`);
sendTaskNotifications('created', newTaskId, newTitle, originalTask.description, createdBy);
}
res.json({
success: true,
taskId: newTaskId,
message: 'Копия задачи успешно создана'
});
}
);
});
});
});
});
app.put('/api/tasks/:taskId', requireAuth, upload.array('files', 15), (req, res) => {
const { taskId } = req.params;
const { title, description, assignedUsers, dueDate } = req.body;
const userId = req.session.user.id;
if (!title) {
return res.status(400).json({ error: 'Название задачи обязательно' });
}
if (!dueDate) {
return res.status(400).json({ error: 'Дата и время выполнения обязательны' });
}
db.get("SELECT created_by FROM tasks WHERE id = ? AND status = 'active'", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
if (req.session.user.role !== 'admin' && task.created_by !== userId) {
return res.status(403).json({ error: 'У вас нет прав для редактирования этой задачи' });
}
db.serialize(() => {
db.run(
"UPDATE tasks SET title = ?, description = ?, due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[title, description, dueDate || null, taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
updateTaskMetadata(taskId, { title, description, due_date: dueDate });
logActivity(taskId, userId, 'TASK_UPDATED', `Задача обновлена: ${title}`);
if (req.files && req.files.length > 0) {
const userFolder = createUserTaskFolder(taskId, req.session.user.login);
req.files.forEach(file => {
const newPath = path.join(userFolder, path.basename(file.filename));
fs.renameSync(file.path, newPath);
const originalName = file.originalname;
db.run(
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
[taskId, createdBy, path.basename(file.filename), originalName, newPath, file.size]
);
logActivity(taskId, createdBy, 'FILE_UPLOADED', `Загружен файл: ${originalName}`);
});
const tempDir = path.join(__dirname, 'data', 'uploads', 'temp');
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
if (assignedUsers) {
db.run("DELETE FROM task_assignments WHERE task_id = ?", [taskId], (err) => {
if (err) {
console.error('Ошибка удаления старых назначений:', err);
}
const userIds = Array.isArray(assignedUsers) ? assignedUsers : [assignedUsers];
userIds.forEach(userId => {
const startDate = new Date().toISOString();
db.run(
"INSERT INTO task_assignments (task_id, user_id, start_date, due_date) VALUES (?, ?, ?, ?)",
[taskId, userId, startDate, dueDate || null]
);
});
logActivity(taskId, userId, 'TASK_ASSIGNMENTS_UPDATED', `Назначения обновлены`);
sendTaskNotifications('updated', taskId, title, description, userId);
});
} else {
sendTaskNotifications('updated', taskId, title, description, userId);
}
res.json({ success: true, message: 'Задача обновлена' });
}
);
});
});
});
app.post('/api/tasks/:taskId/rework', requireAuth, (req, res) => {
const { taskId } = req.params;
const { comment } = req.body;
const userId = req.session.user.id;
db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
if (req.session.user.role !== 'admin' && task.created_by !== userId) {
return res.status(403).json({ error: 'У вас нет прав для возврата задачи на доработку' });
}
db.serialize(() => {
db.run(
"UPDATE tasks SET rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[comment || 'Требуется доработка', taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
db.run(
"UPDATE task_assignments SET status = 'rework', rework_comment = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ?",
[comment || 'Требуется доработка', taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
logActivity(taskId, userId, 'TASK_SENT_FOR_REWORK', `Задача возвращена на доработку: ${comment}`);
db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => {
if (!err && taskData) {
sendTaskNotifications('rework', taskId, taskData.title, taskData.description, userId, comment);
}
});
res.json({ success: true, message: 'Задача возвращена на доработку' });
}
);
}
);
});
});
});
app.post('/api/tasks/:taskId/close', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
if (req.session.user.role !== 'admin' && task.created_by !== userId) {
return res.status(403).json({ error: 'У вас нет прав для закрытия этой задачи' });
}
db.run(
"UPDATE tasks SET closed_at = CURRENT_TIMESTAMP, closed_by = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[userId, taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
logActivity(taskId, userId, 'TASK_CLOSED', `Задача закрыта`);
db.get("SELECT title FROM tasks WHERE id = ?", [taskId], (err, taskData) => {
if (!err && taskData) {
sendTaskNotifications('closed', taskId, taskData.title, '', userId);
}
});
res.json({ success: true, message: 'Задача закрыта' });
}
);
});
});
app.post('/api/tasks/:taskId/reopen', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
db.get("SELECT created_by FROM tasks WHERE id = ?", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
if (req.session.user.role !== 'admin' && task.created_by !== userId) {
return res.status(403).json({ error: 'У вас нет прав для открытия этой задачи' });
}
db.run(
"UPDATE tasks SET closed_at = NULL, closed_by = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
[taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
logActivity(taskId, userId, 'TASK_REOPENED', `Задача открыта`);
res.json({ success: true, message: 'Задача открыта' });
}
);
});
});
app.put('/api/tasks/:taskId/assignment/:userId', requireAuth, (req, res) => {
const { taskId, userId } = req.params;
const { dueDate } = req.body;
const currentUserId = req.session.user.id;
if (!dueDate) {
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: 'Задача не найдена' });
}
if (req.session.user.role !== 'admin' && task.created_by !== currentUserId) {
return res.status(403).json({ error: 'У вас нет прав для редактирования сроки' });
}
db.run(
"UPDATE task_assignments SET due_date = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?",
[dueDate || null, taskId, userId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (this.changes === 0) {
return res.status(404).json({ error: 'Назначение не найдено' });
}
logActivity(taskId, currentUserId, 'ASSIGNMENT_UPDATED', `Обновлены сроки для пользователя ${userId}`);
db.get("SELECT title, description FROM tasks WHERE id = ?", [taskId], (err, taskData) => {
if (!err && taskData) {
sendTaskNotifications('updated', taskId, taskData.title, taskData.description, currentUserId);
}
});
res.json({ success: true, message: 'Сроки обновлены' });
}
);
});
});
app.delete('/api/tasks/:taskId', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
db.get("SELECT created_by FROM tasks WHERE id = ? AND status = 'active'", [taskId], (err, task) => {
if (err || !task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
if (req.session.user.role !== 'admin' && task.created_by !== userId) {
return res.status(403).json({ error: 'У вас нет прав для удаления этой задачи' });
}
db.run(
"UPDATE tasks SET status = 'deleted', deleted_at = CURRENT_TIMESTAMP, deleted_by = ? WHERE id = ?",
[userId, taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
updateTaskMetadata(taskId, {
status: 'deleted',
deleted_at: new Date().toISOString(),
deleted_by: userId
});
logActivity(taskId, userId, 'TASK_DELETED', `Задача помечена как удаленная`);
res.json({ success: true, message: 'Задача удалена' });
}
);
});
});
app.post('/api/tasks/:taskId/restore', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
if (req.session.user.role !== 'admin') {
return res.status(403).json({ error: 'Недостаточно прав' });
}
db.run(
"UPDATE tasks SET status = 'active', deleted_at = NULL, deleted_by = NULL WHERE id = ?",
[taskId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (this.changes === 0) {
return res.status(404).json({ error: 'Задача не найдена' });
}
updateTaskMetadata(taskId, {
status: 'active',
deleted_at: null,
deleted_by: null
});
logActivity(taskId, userId, 'TASK_RESTORED', `Задача восстановлена`);
res.json({ success: true, message: 'Задача восстановлена' });
}
);
});
app.put('/api/tasks/:taskId/status', requireAuth, (req, res) => {
const { taskId } = req.params;
const { userId: targetUserId, status } = req.body;
const currentUserId = req.session.user.id;
if (parseInt(targetUserId) !== currentUserId) {
return res.status(403).json({ error: 'Недостаточно прав' });
}
if (!targetUserId || !status) {
return res.status(400).json({ error: 'userId и status обязательны' });
}
db.get("SELECT 1 FROM task_assignments WHERE task_id = ? AND user_id = ?", [taskId, currentUserId], (err, assignment) => {
if (err || !assignment) {
return res.status(403).json({ error: 'Вы не назначены на эту задачу' });
}
db.get(`
SELECT t.title, t.description, u.name as user_name
FROM tasks t
LEFT JOIN users u ON u.id = ?
WHERE t.id = ?
`, [currentUserId, taskId], (err, taskData) => {
if (err) {
console.error('Ошибка получения данных задачи:', err);
}
const finalStatus = status === 'completed' ? 'completed' : status;
db.run(
"UPDATE task_assignments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?",
[finalStatus, taskId, targetUserId],
function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (this.changes === 0) {
res.status(404).json({ error: 'Назначение не найдено' });
return;
}
logActivity(taskId, targetUserId, 'STATUS_CHANGED', `Статус изменен на: ${finalStatus}`);
if (taskData) {
sendTaskNotifications(
'status_changed',
taskId,
taskData.title,
taskData.description,
currentUserId,
'',
finalStatus,
taskData.user_name || req.session.user.name
);
}
res.json({ success: true, message: 'Статус обновлен' });
}
);
});
});
});
app.post('/api/tasks/:taskId/files', requireAuth, upload.array('files', 15), (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'Нет файлов для загрузки' });
}
checkTaskAccess(userId, taskId, (err, hasAccess) => {
if (err || !hasAccess) {
return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' });
}
req.files.forEach(file => {
const newPath = path.join(userFolder, path.basename(file.filename));
fs.renameSync(file.path, newPath);
const originalName = file.originalname;
db.run(
"INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)",
[taskId, createdBy, path.basename(file.filename), originalName, newPath, file.size]
);
logActivity(taskId, createdBy, 'FILE_UPLOADED', `Загружен файл: ${originalName}`);
});
res.json({ success: true, message: 'Файлы успешно загружены' });
});
});
app.get('/api/tasks/:taskId/files', requireAuth, (req, res) => {
const { taskId } = req.params;
const userId = req.session.user.id;
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: 'Файл не найдена' });
}
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);
});
});
});
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);
});
});
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'));
});
// API для получения логов уведомлений
app.get('/api/notification-logs', requireAuth, async (req, res) => {
try {
const {
taskId,
status,
startDate,
endDate,
limit = 50,
offset = 0
} = req.query;
const logs = await postgresLogger.getNotifications({
taskId: taskId ? parseInt(taskId) : null,
userId: req.session.user.id,
status,
startDate,
endDate,
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json(logs);
} 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: 'Недостаточно прав' });
}
const stats = await postgresLogger.getStatistics(period);
res.json(stats);
} 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.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);
});