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