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

1176 lines
47 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.
// 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
};
};