// api-keys.js - API для управления ключами доступа к задачам с логированием const express = require('express'); const crypto = require('crypto'); const path = require('path'); const fs = require('fs'); const router = express.Router(); module.exports = function(app, db, upload) { // Middleware для проверки аутентификации const requireAuth = (req, res, next) => { if (!req.session || !req.session.user) { return res.status(401).json({ error: 'Требуется аутентификация' }); } next(); }; // Middleware для проверки прав администратора const requireAdmin = (req, res, next) => { if (!req.session || !req.session.user || req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Недостаточно прав' }); } next(); }; // Функция для логирования действий API const logApiAction = (apiKeyId, userId, action, details, req, statusCode = 200, responseTime = 0) => { const timestamp = new Date().toISOString(); const ip = req.ip || req.connection.remoteAddress; const userAgent = req.get('User-Agent') || 'Unknown'; const logEntry = { timestamp, api_key_id: apiKeyId, user_id: userId, action, details, ip, user_agent: userAgent, method: req.method, path: req.originalUrl, status_code: statusCode, response_time: responseTime }; // Цветное логирование в консоль const colors = { GET: '\x1b[32m', // зеленый POST: '\x1b[33m', // желтый PUT: '\x1b[34m', // синий DELETE: '\x1b[31m', // красный reset: '\x1b[0m' }; const methodColor = colors[req.method] || '\x1b[36m'; console.log('\n' + '='.repeat(50)); console.log(`${methodColor}🔑 API ${req.method} ${req.originalUrl}${colors.reset}`); console.log(`📅 ${timestamp}`); console.log(`👤 Пользователь: ID ${userId} (${req.apiUser?.name || 'Unknown'})`); console.log(`🔑 API Key ID: ${apiKeyId}`); console.log(`📋 Действие: ${action}`); console.log(`📝 Детали: ${details}`); console.log(`🌐 IP: ${ip}`); console.log(`📱 User-Agent: ${userAgent.substring(0, 50)}...`); console.log(`📊 Статус: ${statusCode} | Время: ${responseTime}ms`); console.log('='.repeat(50)); // Сохраняем в базу данных db.run(` INSERT INTO api_logs ( api_key_id, user_id, action, details, ip, user_agent, method, path, response_status, response_time, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ apiKeyId, userId, action, details, ip, userAgent, req.method, req.originalUrl, statusCode, responseTime, timestamp ], (err) => { if (err) { console.error('❌ Ошибка сохранения лога API:', err.message); } }); // Логируем в общую активность для важных действий const { logActivity } = require('./database'); if (logActivity && ( action.includes('STATUS') || action.includes('FILE') || action.includes('CREATE') || action.includes('UPDATE') || action.includes('DELETE') )) { const taskId = extractTaskIdFromDetails(details) || extractTaskIdFromUrl(req.originalUrl); if (taskId) { logActivity( taskId, userId, `API_${action}`, `[API Key ${apiKeyId}] ${details}` ); } } }; // Вспомогательная функция для извлечения ID задачи из деталей function extractTaskIdFromDetails(details) { const match = details.match(/задачи?\s*[#№]?\s*(\d+)/i); return match ? parseInt(match[1]) : null; } // Вспомогательная функция для извлечения ID задачи из URL function extractTaskIdFromUrl(url) { const match = url.match(/\/tasks?\/(\d+)/i); return match ? parseInt(match[1]) : null; } // Функция для логирования попыток аутентификации const logApiAttempt = (apiKeyId, userId, action, details, req, statusCode, responseTime) => { const timestamp = new Date().toISOString(); const ip = req.ip || req.connection.remoteAddress; const userAgent = req.get('User-Agent') || 'Unknown'; const color = statusCode >= 400 ? '\x1b[31m' : '\x1b[33m'; console.log('\n' + '='.repeat(50)); console.log(`${color}🔑 API ATTEMPT ${req.method} ${req.originalUrl}${'\x1b[0m'}`); console.log(`📅 ${timestamp}`); console.log(`👤 Пользователь ID: ${userId || 'N/A'}`); console.log(`🔑 API Key ID: ${apiKeyId || 'N/A'}`); console.log(`📋 Действие: ${action}`); console.log(`📝 Детали: ${details}`); console.log(`🌐 IP: ${ip}`); console.log(`📊 Статус: ${statusCode} | Время: ${responseTime}ms`); console.log('='.repeat(50)); if (apiKeyId && userId) { db.run(` INSERT INTO api_logs ( api_key_id, user_id, action, details, ip, user_agent, method, path, response_status, response_time, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ apiKeyId, userId, action, details, ip, userAgent, req.method, req.originalUrl, statusCode, responseTime, timestamp ], (err) => { if (err) { console.error('❌ Ошибка сохранения лога попытки:', err.message); } }); } }; // Создание таблиц для API ключей и логов function createApiKeysTables() { return new Promise((resolve, reject) => { // Таблица API ключей db.run(` CREATE TABLE IF NOT EXISTS api_keys ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, key TEXT NOT NULL UNIQUE, user_id INTEGER NOT NULL, description TEXT, allowed_ips TEXT, is_active BOOLEAN DEFAULT true, last_used_at DATETIME, last_used_ip TEXT, created_by INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES users(id) ) `, (err) => { if (err) { console.error('❌ Ошибка создания таблицы api_keys:', err.message); reject(err); return; } // Проверяем и добавляем отсутствующие колонки db.all("PRAGMA table_info(api_keys)", (err, columns) => { if (err) { console.error('❌ Ошибка получения информации о таблице:', err); } else { const columnNames = columns.map(c => c.name); // Добавляем last_used_ip если отсутствует if (!columnNames.includes('last_used_ip')) { db.run("ALTER TABLE api_keys ADD COLUMN last_used_ip TEXT", (err) => { if (err) { console.error('❌ Ошибка добавления колонки last_used_ip:', err.message); } else { console.log('✅ Добавлена колонка last_used_ip в таблицу api_keys'); } }); } } }); // Таблица для логов API действий db.run(` CREATE TABLE IF NOT EXISTS api_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, api_key_id INTEGER NOT NULL, user_id INTEGER NOT NULL, action TEXT NOT NULL, details TEXT, ip TEXT, user_agent TEXT, method TEXT, path TEXT, response_status INTEGER, response_time INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ) `, (err) => { if (err) { console.error('❌ Ошибка создания таблицы api_logs:', err.message); reject(err); return; } // Таблица для связи API ключей с задачами db.run(` CREATE TABLE IF NOT EXISTS api_task_mappings ( id INTEGER PRIMARY KEY AUTOINCREMENT, api_key_id INTEGER NOT NULL, task_id INTEGER NOT NULL, external_task_id TEXT, synced_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_status TEXT, FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE, FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE, UNIQUE(api_key_id, task_id) ) `, (err) => { if (err) { console.error('❌ Ошибка создания таблицы api_task_mappings:', err.message); reject(err); return; } // Индексы db.run('CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id)'); db.run('CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key)'); db.run('CREATE INDEX IF NOT EXISTS idx_api_logs_api_key_id ON api_logs(api_key_id)'); db.run('CREATE INDEX IF NOT EXISTS idx_api_logs_created_at ON api_logs(created_at)'); db.run('CREATE INDEX IF NOT EXISTS idx_api_task_mappings_task_id ON api_task_mappings(task_id)'); console.log('✅ Таблицы для API ключей и логов созданы'); resolve(); }); }); }); }); } // Генерация уникального API ключа function generateApiKey() { return crypto.randomBytes(32).toString('hex'); } // Middleware для проверки API ключа с логированием const requireApiKey = (req, res, next) => { const startTime = Date.now(); const apiKey = req.headers['x-api-key'] || req.query.api_key; const ip = req.ip || req.connection.remoteAddress; console.log('\n\x1b[33m%s\x1b[0m', '🔑 Входящий API запрос'); console.log('🌐 IP:', ip); console.log('🔑 Ключ:', apiKey ? apiKey.substring(0, 8) + '...' : 'не предоставлен'); console.log('🔄 Метод:', req.method); console.log('📍 Путь:', req.originalUrl); if (!apiKey) { logApiAttempt(null, null, 'AUTH_FAILED', 'Ключ не предоставлен', req, 401, Date.now() - startTime); return res.status(401).json({ error: 'API ключ обязателен' }); } // Проверяем ключ в базе db.get(` SELECT ak.*, u.login as user_login, u.name as user_name, u.id as user_id FROM api_keys ak JOIN users u ON ak.user_id = u.id WHERE ak.key = ? AND ak.is_active = 1 `, [apiKey], (err, keyData) => { const responseTime = Date.now() - startTime; if (err) { console.error('❌ Ошибка проверки ключа:', err); logApiAttempt(null, null, 'AUTH_ERROR', `Ошибка БД: ${err.message}`, req, 500, responseTime); return res.status(500).json({ error: 'Ошибка проверки ключа' }); } if (!keyData) { console.log('❌ Недействительный ключ:', apiKey.substring(0, 8) + '...'); // Проверяем, есть ли такой ключ в базе (неактивный) db.get('SELECT is_active FROM api_keys WHERE key = ?', [apiKey], (err, inactiveKey) => { const reason = inactiveKey ? 'Ключ деактивирован' : 'Ключ не найден'; logApiAttempt(null, null, 'AUTH_FAILED', reason, req, 401, responseTime); }); return res.status(401).json({ error: 'Недействительный API ключ' }); } // Проверка IP, если настроено if (keyData.allowed_ips) { try { const allowedIPs = JSON.parse(keyData.allowed_ips); if (allowedIPs.length > 0 && !allowedIPs.includes(ip)) { console.log('❌ IP не разрешен:', ip); logApiAttempt(keyData.id, keyData.user_id, 'AUTH_FAILED', `IP ${ip} не разрешен`, req, 403, responseTime); return res.status(403).json({ error: 'IP адрес не разрешен' }); } } catch (e) { console.error('Ошибка парсинга allowed_ips:', e); } } // Обновляем информацию о последнем использовании db.run( 'UPDATE api_keys SET last_used_at = CURRENT_TIMESTAMP, last_used_ip = ? WHERE id = ?', [ip, keyData.id] ); console.log('✅ Ключ валидный. Пользователь:', keyData.user_name); console.log('⏱️ Время проверки:', responseTime + 'ms'); req.apiKey = keyData; req.apiUser = { id: keyData.user_id, login: keyData.user_login, name: keyData.user_name }; // Сохраняем startTime для логирования времени ответа req.apiRequestStartTime = startTime; next(); }); }; // Middleware для логирования ответов const logResponse = (req, res, next) => { const originalJson = res.json; const originalSend = res.send; const startTime = req.apiRequestStartTime || Date.now(); res.json = function(data) { const responseTime = Date.now() - startTime; if (req.apiKey) { logApiAction( req.apiKey.id, req.apiUser.id, `${req.method}_${req.path.replace(/[^a-zA-Z0-9]/g, '_')}`, `Статус: ${res.statusCode}`, req, res.statusCode, responseTime ); } console.log(`📤 Ответ отправлен за ${responseTime}ms`); originalJson.call(this, data); }; res.send = function(data) { const responseTime = Date.now() - startTime; if (req.apiKey) { logApiAction( req.apiKey.id, req.apiUser.id, `${req.method}_${req.path.replace(/[^a-zA-Z0-9]/g, '_')}`, `Статус: ${res.statusCode}`, req, res.statusCode, responseTime ); } console.log(`📤 Ответ отправлен за ${responseTime}ms`); originalSend.call(this, data); }; next(); }; // GET /api/api-keys/logs - Получить логи API (только админ) router.get('/api/api-keys/logs', requireAuth, requireAdmin, (req, res) => { const { limit = 100, offset = 0, api_key_id, user_id, action } = req.query; let query = ` SELECT l.*, ak.name as api_key_name, u.name as user_name, u.login as user_login FROM api_logs l LEFT JOIN api_keys ak ON l.api_key_id = ak.id LEFT JOIN users u ON l.user_id = u.id WHERE 1=1 `; const params = []; if (api_key_id) { query += ' AND l.api_key_id = ?'; params.push(api_key_id); } if (user_id) { query += ' AND l.user_id = ?'; params.push(user_id); } if (action) { query += ' AND l.action LIKE ?'; params.push(`%${action}%`); } query += ' ORDER BY l.created_at DESC LIMIT ? OFFSET ?'; params.push(parseInt(limit), parseInt(offset)); db.all(query, params, (err, logs) => { if (err) { console.error('❌ Ошибка получения логов:', err); return res.status(500).json({ error: 'Ошибка получения логов' }); } // Получаем общее количество db.get('SELECT COUNT(*) as total FROM api_logs', [], (err, count) => { if (err) { return res.json({ logs: logs || [], total: 0 }); } res.json({ success: true, logs: logs || [], total: count.total, limit: parseInt(limit), offset: parseInt(offset) }); }); }); }); // GET /api/api-keys/:id/logs - Получить логи конкретного ключа router.get('/api/api-keys/:id/logs', requireAuth, requireAdmin, (req, res) => { const { id } = req.params; const { limit = 50 } = req.query; db.all(` SELECT l.*, u.name as user_name FROM api_logs l LEFT JOIN users u ON l.user_id = u.id WHERE l.api_key_id = ? ORDER BY l.created_at DESC LIMIT ? `, [id, parseInt(limit)], (err, logs) => { if (err) { console.error('❌ Ошибка получения логов ключа:', err); return res.status(500).json({ error: 'Ошибка получения логов' }); } res.json({ success: true, logs: logs || [] }); }); }); // GET /api/api-keys/stats - Статистика использования API router.get('/api/api-keys/stats', requireAuth, requireAdmin, (req, res) => { const stats = {}; // Общая статистика db.get(` SELECT COUNT(DISTINCT api_key_id) as active_keys, COUNT(*) as total_requests, COUNT(DISTINCT user_id) as unique_users, AVG(response_time) as avg_response_time, COUNT(CASE WHEN response_status >= 400 THEN 1 END) as error_count, COUNT(CASE WHEN DATE(created_at) = DATE('now') THEN 1 END) as today_requests FROM api_logs WHERE created_at >= DATE('now', '-30 days') `, [], (err, total) => { if (err) { console.error('❌ Ошибка получения статистики:', err); return res.status(500).json({ error: 'Ошибка получения статистики' }); } stats.total = total || {}; // Статистика по методам db.all(` SELECT method, COUNT(*) as count FROM api_logs WHERE created_at >= DATE('now', '-30 days') GROUP BY method ORDER BY count DESC `, [], (err, methods) => { if (err) { return res.json(stats); } stats.by_method = methods || []; // Статистика по дням db.all(` SELECT DATE(created_at) as date, COUNT(*) as count FROM api_logs WHERE created_at >= DATE('now', '-7 days') GROUP BY DATE(created_at) ORDER BY date DESC `, [], (err, daily) => { if (err) { return res.json(stats); } stats.daily = daily || []; // Топ ключей db.all(` SELECT l.api_key_id, ak.name as api_key_name, COUNT(*) as request_count, COUNT(DISTINCT l.user_id) as users_count, AVG(l.response_time) as avg_time FROM api_logs l LEFT JOIN api_keys ak ON l.api_key_id = ak.id WHERE l.created_at >= DATE('now', '-30 days') GROUP BY l.api_key_id ORDER BY request_count DESC LIMIT 10 `, [], (err, topKeys) => { if (err) { return res.json(stats); } stats.top_keys = topKeys || []; res.json({ success: true, stats, timestamp: new Date().toISOString() }); }); }); }); }); }); // GET /api/api-keys - Получить все ключи (только админ) router.get('/api/api-keys', requireAuth, requireAdmin, (req, res) => { db.all(` SELECT ak.*, u.name as user_name, u.login as user_login, creator.name as creator_name, (SELECT COUNT(*) FROM api_logs WHERE api_key_id = ak.id) as request_count, (SELECT created_at FROM api_logs WHERE api_key_id = ak.id ORDER BY created_at DESC LIMIT 1) as last_request FROM api_keys ak LEFT JOIN users u ON ak.user_id = u.id LEFT JOIN users creator ON ak.created_by = creator.id ORDER BY ak.created_at DESC `, [], (err, keys) => { if (err) { console.error('❌ Ошибка получения API ключей:', err); return res.status(500).json({ error: 'Ошибка получения ключей' }); } // Маскируем ключи для безопасности const maskedKeys = (keys || []).map(key => ({ ...key, key: key.key.substring(0, 8) + '...' + key.key.substring(key.key.length - 8), request_count: key.request_count || 0 })); res.json(maskedKeys); }); }); // POST /api/api-keys - Создать новый API ключ (только админ) router.post('/api/api-keys', requireAuth, requireAdmin, (req, res) => { const { name, user_id, description, allowed_ips } = req.body; const startTime = Date.now(); if (!name || !user_id) { return res.status(400).json({ error: 'Название и пользователь обязательны' }); } // Проверяем существование пользователя db.get('SELECT id, name FROM users WHERE id = ?', [user_id], (err, user) => { if (err || !user) { return res.status(404).json({ error: 'Пользователь не найден' }); } const apiKey = generateApiKey(); const allowedIPsJson = allowed_ips ? JSON.stringify(allowed_ips.split(',').map(ip => ip.trim())) : null; db.run(` INSERT INTO api_keys (name, key, user_id, description, allowed_ips, created_by) VALUES (?, ?, ?, ?, ?, ?) `, [name, apiKey, user_id, description || '', allowedIPsJson, req.session.user.id], function(err) { const responseTime = Date.now() - startTime; if (err) { console.error('❌ Ошибка создания API ключа:', err); return res.status(500).json({ error: 'Ошибка создания ключа' }); } // Логируем создание ключа logApiAction( this.lastID, req.session.user.id, 'API_KEY_CREATED', `Создан ключ "${name}" для пользователя ${user.name} (ID: ${user_id})`, req, 201, responseTime ); res.status(201).json({ success: true, id: this.lastID, key: apiKey, message: 'API ключ успешно создан' }); }); }); }); // PUT /api/api-keys/:id/toggle - Активировать/деактивировать ключ router.put('/api/api-keys/:id/toggle', requireAuth, requireAdmin, (req, res) => { const { id } = req.params; const { is_active } = req.body; const startTime = Date.now(); db.get('SELECT name FROM api_keys WHERE id = ?', [id], (err, key) => { if (err || !key) { return res.status(404).json({ error: 'Ключ не найден' }); } db.run( 'UPDATE api_keys SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [is_active ? 1 : 0, id], function(err) { const responseTime = Date.now() - startTime; if (err) { console.error('❌ Ошибка обновления ключа:', err); return res.status(500).json({ error: 'Ошибка обновления ключа' }); } logApiAction( id, req.session.user.id, is_active ? 'API_KEY_ACTIVATED' : 'API_KEY_DEACTIVATED', `Ключ "${key.name}" ${is_active ? 'активирован' : 'деактивирован'}`, req, 200, responseTime ); res.json({ success: true, message: `Ключ ${is_active ? 'активирован' : 'деактивирован'}` }); } ); }); }); // DELETE /api/api-keys/:id - Удалить ключ (только админ) router.delete('/api/api-keys/:id', requireAuth, requireAdmin, (req, res) => { const { id } = req.params; const startTime = Date.now(); db.get('SELECT name FROM api_keys WHERE id = ?', [id], (err, key) => { if (err || !key) { return res.status(404).json({ error: 'Ключ не найден' }); } db.run('DELETE FROM api_keys WHERE id = ?', [id], function(err) { const responseTime = Date.now() - startTime; if (err) { console.error('❌ Ошибка удаления ключа:', err); return res.status(500).json({ error: 'Ошибка удаления ключа' }); } logApiAction( id, req.session.user.id, 'API_KEY_DELETED', `Удален ключ "${key.name}"`, req, 200, responseTime ); res.json({ success: true, message: 'Ключ удален' }); }); }); }); // ==================== Публичное API для внешних сервисов ==================== // GET /api/external/tasks - Получить задачи для внешнего сервиса router.get('/api/external/tasks', requireApiKey, logResponse, (req, res) => { const userId = req.apiUser.id; const { status, limit = 50, offset = 0 } = req.query; const startTime = Date.now(); let query = ` SELECT DISTINCT t.id, t.title, t.description, t.created_at, t.due_date, t.task_type, u.name as creator_name, ( SELECT json_group_array(json_object( 'id', tf.id, 'filename', tf.original_name, 'file_size', tf.file_size, 'uploaded_at', tf.uploaded_at )) FROM task_files tf WHERE tf.task_id = t.id ) as files, ta.status as assignment_status, ta.start_date, ta.due_date as assignment_due_date FROM tasks t LEFT JOIN users u ON t.created_by = u.id LEFT JOIN task_assignments ta ON t.id = ta.task_id AND ta.user_id = ? WHERE ta.user_id = ? AND t.status = 'active' AND t.closed_at IS NULL `; const params = [userId, userId]; if (status) { if (status === 'active') { query += ` AND ta.status IN ('assigned', 'in_progress')`; } else { query += ` AND ta.status = ?`; params.push(status); } } query += ` ORDER BY t.due_date ASC, t.created_at DESC LIMIT ? OFFSET ?`; params.push(limit, offset); db.all(query, params, (err, tasks) => { const responseTime = Date.now() - startTime; if (err) { console.error('❌ Ошибка получения задач:', err); logApiAction( req.apiKey.id, userId, 'GET_TASKS_ERROR', `Ошибка: ${err.message}`, req, 500, responseTime ); return res.status(500).json({ error: 'Ошибка получения задач' }); } // Парсим JSON с файлами const result = (tasks || []).map(task => { try { task.files = JSON.parse(task.files || '[]'); } catch (e) { task.files = []; } return task; }); logApiAction( req.apiKey.id, userId, 'GET_TASKS', `Получено ${result.length} задач${status ? ` (статус: ${status})` : ''}`, req, 200, responseTime ); res.json({ success: true, tasks: result, meta: { total: result.length, limit, offset, user: req.apiUser.name } }); }); }); // GET /api/external/tasks/:taskId - Получить конкретную задачу router.get('/api/external/tasks/:taskId', requireApiKey, logResponse, (req, res) => { const { taskId } = req.params; const userId = req.apiUser.id; const startTime = Date.now(); // Проверяем, что задача назначена этому пользователю db.get(` SELECT t.*, u.name as creator_name, ( SELECT json_group_array(json_object( 'id', tf.id, 'filename', tf.original_name, 'file_path', tf.file_path, 'file_size', tf.file_size, 'uploaded_at', tf.uploaded_at, 'uploader_name', up.name )) FROM task_files tf LEFT JOIN users up ON tf.user_id = up.id WHERE tf.task_id = t.id ) as files, ta.status as assignment_status, ta.start_date, ta.due_date as assignment_due_date FROM tasks t LEFT JOIN users u ON t.created_by = u.id LEFT JOIN task_assignments ta ON t.id = ta.task_id AND ta.user_id = ? WHERE t.id = ? AND ta.user_id IS NOT NULL `, [userId, taskId], (err, task) => { const responseTime = Date.now() - startTime; if (err) { console.error('❌ Ошибка получения задачи:', err); logApiAction( req.apiKey.id, userId, 'GET_TASK_ERROR', `Ошибка получения задачи ${taskId}: ${err.message}`, req, 500, responseTime ); return res.status(500).json({ error: 'Ошибка получения задачи' }); } if (!task) { logApiAction( req.apiKey.id, userId, 'GET_TASK_NOT_FOUND', `Задача ${taskId} не найдена или не назначена`, req, 404, responseTime ); return res.status(404).json({ error: 'Задача не найдена или не назначена вам' }); } try { task.files = JSON.parse(task.files || '[]'); } catch (e) { task.files = []; } logApiAction( req.apiKey.id, userId, 'GET_TASK', `Получена задача ${taskId}: "${task.title}"`, req, 200, responseTime ); res.json({ success: true, task }); }); }); // PUT /api/external/tasks/:taskId/status - Изменить статус задачи router.put('/api/external/tasks/:taskId/status', requireApiKey, logResponse, (req, res) => { const { taskId } = req.params; const { status, comment } = req.body; const userId = req.apiUser.id; const startTime = Date.now(); if (!status || !['in_progress', 'completed'].includes(status)) { logApiAction( req.apiKey.id, userId, 'UPDATE_STATUS_ERROR', `Попытка установить недопустимый статус "${status}" для задачи ${taskId}`, req, 400, Date.now() - startTime ); return res.status(400).json({ error: 'Статус должен быть "in_progress" или "completed"' }); } // Проверяем, что задача назначена этому пользователю db.get(` SELECT ta.id, ta.status as current_status, t.title, t.created_by FROM task_assignments ta JOIN tasks t ON ta.task_id = t.id WHERE ta.task_id = ? AND ta.user_id = ? `, [taskId, userId], (err, assignment) => { if (err || !assignment) { const responseTime = Date.now() - startTime; logApiAction( req.apiKey.id, userId, 'UPDATE_STATUS_ERROR', `Задача ${taskId} не назначена пользователю`, req, 404, responseTime ); return res.status(404).json({ error: 'Задача не назначена вам' }); } // Обновляем статус db.run( `UPDATE task_assignments SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE task_id = ? AND user_id = ?`, [status, taskId, userId], function(err) { const responseTime = Date.now() - startTime; if (err) { console.error('❌ Ошибка обновления статуса:', err); logApiAction( req.apiKey.id, userId, 'UPDATE_STATUS_ERROR', `Ошибка обновления статуса задачи ${taskId}: ${err.message}`, req, 500, responseTime ); return res.status(500).json({ error: 'Ошибка обновления статуса' }); } const details = `Статус задачи ${taskId} "${assignment.title}" изменен с ${assignment.current_status} на ${status}. Комментарий: ${comment || 'нет'}`; logApiAction( req.apiKey.id, userId, `TASK_${status.toUpperCase()}`, details, req, 200, responseTime ); // Логируем в общую активность const { logActivity } = require('./database'); if (logActivity) { logActivity( parseInt(taskId), userId, `API_TASK_${status.toUpperCase()}`, `[API Key ${req.apiKey.id}] ${comment || 'Без комментария'}` ); } res.json({ success: true, message: `Статус изменен на "${status}"`, old_status: assignment.current_status, new_status: status, task_title: assignment.title }); } ); }); }); // POST /api/external/tasks/:taskId/files - Загрузить файл к задаче router.post('/api/external/tasks/:taskId/files', requireApiKey, logResponse, upload.single('file'), (req, res) => { const { taskId } = req.params; const userId = req.apiUser.id; const startTime = Date.now(); if (!req.file) { logApiAction( req.apiKey.id, userId, 'UPLOAD_FILE_ERROR', `Попытка загрузить файл без файла к задаче ${taskId}`, req, 400, Date.now() - startTime ); return res.status(400).json({ error: 'Файл обязателен' }); } // Проверяем, что задача назначена этому пользователю db.get(` SELECT ta.id, t.title FROM task_assignments ta JOIN tasks t ON ta.task_id = t.id WHERE ta.task_id = ? AND ta.user_id = ? `, [taskId, userId], (err, assignment) => { if (err || !assignment) { // Удаляем загруженный файл, если он не нужен if (req.file && req.file.path && fs.existsSync(req.file.path)) { fs.unlinkSync(req.file.path); } const responseTime = Date.now() - startTime; logApiAction( req.apiKey.id, userId, 'UPLOAD_FILE_ERROR', `Задача ${taskId} не назначена пользователю`, req, 404, responseTime ); return res.status(404).json({ error: 'Задача не назначена вам' }); } const { createUserTaskFolder } = require('./database'); const userFolder = createUserTaskFolder(taskId, req.apiUser.login); const newPath = path.join(userFolder, path.basename(req.file.filename)); fs.renameSync(req.file.path, newPath); db.run( `INSERT INTO task_files (task_id, user_id, filename, original_name, file_path, file_size) VALUES (?, ?, ?, ?, ?, ?)`, [taskId, userId, path.basename(req.file.filename), req.file.originalname, newPath, req.file.size], function(err) { const responseTime = Date.now() - startTime; if (err) { console.error('❌ Ошибка сохранения файла:', err); logApiAction( req.apiKey.id, userId, 'UPLOAD_FILE_ERROR', `Ошибка сохранения файла для задачи ${taskId}: ${err.message}`, req, 500, responseTime ); return res.status(500).json({ error: 'Ошибка сохранения файла' }); } const details = `Загружен файл "${req.file.originalname}" (${(req.file.size / 1024).toFixed(2)} KB) к задаче ${taskId} "${assignment.title}"`; logApiAction( req.apiKey.id, userId, 'FILE_UPLOADED', details, req, 200, responseTime ); // Логируем в общую активность const { logActivity } = require('./database'); if (logActivity) { logActivity( parseInt(taskId), userId, 'API_FILE_UPLOADED', `[API Key ${req.apiKey.id}] Загружен файл: ${req.file.originalname}` ); } res.json({ success: true, file_id: this.lastID, filename: req.file.originalname, size: req.file.size, message: 'Файл успешно загружен' }); } ); }); }); // GET /api/external/tasks/:taskId/files/:fileId/download - Скачать файл router.get('/api/external/tasks/:taskId/files/:fileId/download', requireApiKey, logResponse, (req, res) => { const { taskId, fileId } = req.params; const userId = req.apiUser.id; const startTime = Date.now(); db.get(` SELECT tf.*, t.title as task_title FROM task_files tf JOIN task_assignments ta ON tf.task_id = ta.task_id JOIN tasks t ON tf.task_id = t.id WHERE tf.id = ? AND tf.task_id = ? AND ta.user_id = ? `, [fileId, taskId, userId], (err, file) => { const responseTime = Date.now() - startTime; if (err || !file) { logApiAction( req.apiKey.id, userId, 'DOWNLOAD_FILE_ERROR', `Файл ${fileId} не найден для задачи ${taskId}`, req, 404, responseTime ); return res.status(404).json({ error: 'Файл не найден' }); } if (!fs.existsSync(file.file_path)) { logApiAction( req.apiKey.id, userId, 'DOWNLOAD_FILE_ERROR', `Файл ${fileId} не найден на сервере для задачи ${taskId}`, req, 404, responseTime ); return res.status(404).json({ error: 'Файл не найден на сервере' }); } logApiAction( req.apiKey.id, userId, 'FILE_DOWNLOADED', `Скачан файл "${file.original_name}" из задачи ${taskId} "${file.task_title}"`, req, 200, responseTime ); res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(file.original_name)}`); res.setHeader('Content-Type', 'application/octet-stream'); res.sendFile(file.file_path); }); }); // ==================== Инициализация ==================== const init = async () => { try { await createApiKeysTables(); console.log('✅ Модуль API ключей с логированием инициализирован'); } catch (error) { console.error('❌ Ошибка инициализации API ключей:', error.message); } }; init(); app.use(router); return { requireApiKey, generateApiKey }; };