import fs from 'fs' import path from 'path' import { Router } from 'express' import mongoose from 'mongoose' import pkg from '../package.json' import './utils/mongoose' import { ErrorLog } from './models/ErrorLog' const folderPath = path.resolve(__dirname, './routers') const folders = fs.readdirSync(folderPath) // Определение типов interface FileInfo { path: string; type: 'file'; endpoints: number; } interface DirectoryInfo { path: string; type: 'directory'; endpoints: number; children: (FileInfo | DirectoryInfo)[]; } interface DirScanResult { items: (FileInfo | DirectoryInfo)[]; totalEndpoints: number; } // Функция для поиска эндпоинтов в файлах function countEndpoints(filePath) { if (!fs.existsSync(filePath) || !filePath.endsWith('.js') && !filePath.endsWith('.ts')) { return 0; } try { const content = fs.readFileSync(filePath, 'utf8'); const httpMethods = ['get', 'post', 'put', 'delete', 'patch']; return httpMethods.reduce((count, method) => { const regex = new RegExp(`router\\.${method}\\(`, 'gi'); const matches = content.match(regex) || []; return count + matches.length; }, 0); } catch (err) { return 0; } } // Функция для рекурсивного обхода директорий function getAllDirs(dir, basePath = ''): DirScanResult { const items: (FileInfo | DirectoryInfo)[] = []; let totalEndpoints = 0; try { const dirItems = fs.readdirSync(dir); for (const item of dirItems) { const itemPath = path.join(dir, item); const relativePath = path.join(basePath, item); const stat = fs.statSync(itemPath); if (stat.isDirectory()) { const dirResult = getAllDirs(itemPath, relativePath); totalEndpoints += dirResult.totalEndpoints; items.push({ path: relativePath, type: 'directory', endpoints: dirResult.totalEndpoints, children: dirResult.items }); } else { const fileEndpoints = countEndpoints(itemPath); totalEndpoints += fileEndpoints; items.push({ path: relativePath, type: 'file', endpoints: fileEndpoints }); } } } catch (err) { console.error(`Ошибка при чтении директории ${dir}:`, err); } return { items, totalEndpoints }; } // Функция для генерации HTML-списка директорий function generateDirList(dirs: (FileInfo | DirectoryInfo)[], level = 0) { if (dirs.length === 0) return ''; const indent = level * 20; return ``; } const router = Router() // Эндпоинт для получения содержимого файла router.get('/file-content', async (req, res) => { try { const filePath = req.query.path as string; if (!filePath) { return res.status(400).json({ error: 'Путь к файлу не указан' }); } // Используем корень проекта для получения абсолютного пути к файлу const projectRoot = path.resolve(__dirname, 'routers'); const absolutePath = path.join(projectRoot, filePath); // Проверка, что путь не выходит за пределы проекта if (!absolutePath.startsWith(projectRoot)) { return res.status(403).json({ error: 'Доступ запрещен' }); } if (!fs.existsSync(absolutePath)) { return res.status(404).json({ error: 'Файл не найден' }); } // Проверяем, что это файл, а не директория const stat = fs.statSync(absolutePath); if (!stat.isFile()) { return res.status(400).json({ error: 'Указанный путь не является файлом' }); } // Проверяем размер файла, чтобы не загружать слишком большие файлы const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB if (stat.size > MAX_FILE_SIZE) { return res.status(413).json({ error: 'Файл слишком большой', size: stat.size, maxSize: MAX_FILE_SIZE }); } const content = fs.readFileSync(absolutePath, 'utf8'); const fileExtension = path.extname(absolutePath).slice(1); res.json({ content, extension: fileExtension, fileName: path.basename(absolutePath) }); } catch (err) { console.error('Ошибка при чтении файла:', err); res.status(500).json({ error: 'Ошибка при чтении файла' }); } }); router.get('/', async (req, res) => { // throw new Error('check error message') // Используем корень проекта вместо только директории routers const projectRoot = path.resolve(__dirname, 'routers'); const routersPath = path.resolve(__dirname, 'routers'); const routerFolders = fs.readdirSync(routersPath); const directoryResult = getAllDirs(projectRoot); const totalEndpoints = directoryResult.totalEndpoints; // Получаем последние 10 ошибок const latestErrors = await ErrorLog.find().sort({ createdAt: -1 }).limit(10); // Сформируем HTML для секции с ошибками let errorsHtml = ''; if (latestErrors.length > 0) { errorsHtml = latestErrors.map(error => `

${error.message}

${new Date(error.createdAt).toLocaleString()}
${error.path ? `
${error.method || 'GET'} ${error.path}
` : ''} ${error.stack ? `
${error.stack}
` : ''}
`).join(''); } else { errorsHtml = '

Нет зарегистрированных ошибок

'; } // Создаем JavaScript для клиентской части const clientScript = ` document.addEventListener('DOMContentLoaded', function() { // Директории document.querySelectorAll('.dir-item[data-expandable="true"]').forEach(item => { item.addEventListener('click', function(e) { const subdirectory = this.nextElementSibling; const isExpanded = this.classList.toggle('expanded'); if (isExpanded) { subdirectory.style.display = 'block'; this.querySelector('.expand-icon').textContent = '▼'; } else { subdirectory.style.display = 'none'; this.querySelector('.expand-icon').textContent = '▶'; } e.stopPropagation(); }); }); // Модальное окно const modal = document.getElementById('fileModal'); const closeBtn = document.querySelector('.close-modal'); const fileContent = document.getElementById('fileContent'); const fileLoader = document.getElementById('fileLoader'); const modalFileName = document.getElementById('modalFileName'); // Закрытие модального окна closeBtn.addEventListener('click', function() { modal.style.display = 'none'; }); window.addEventListener('click', function(event) { if (event.target == modal) { modal.style.display = 'none'; } }); // Обработчик для файлов document.querySelectorAll('.file-item').forEach(item => { item.addEventListener('click', async function() { const filePath = this.getAttribute('data-path'); if (!filePath) return; // Показываем модальное окно и лоадер modal.style.display = 'block'; fileContent.style.display = 'none'; fileLoader.style.display = 'block'; modalFileName.textContent = 'Загрузка...'; try { const response = await fetch('/file-content?path=' + encodeURIComponent(filePath)); if (!response.ok) { throw new Error('Ошибка при загрузке файла'); } const data = await response.json(); // Отображаем содержимое файла fileLoader.style.display = 'none'; fileContent.style.display = 'block'; fileContent.textContent = data.content; modalFileName.textContent = data.fileName; // Подсветка синтаксиса const extensionMap = { 'js': 'javascript', 'ts': 'typescript', 'json': 'json', 'css': 'css', 'html': 'xml', 'xml': 'xml', 'md': 'markdown', 'yaml': 'yaml', 'yml': 'yaml', 'sh': 'bash', 'bash': 'bash' }; const language = extensionMap[data.extension] || ''; if (language) { fileContent.className = 'language-' + language; hljs.highlightElement(fileContent); } } catch (error) { fileLoader.style.display = 'none'; fileContent.style.display = 'block'; fileContent.textContent = 'Ошибка при загрузке файла: ' + error.message; modalFileName.textContent = 'Ошибка'; } }); }); // Обработчик кнопки очистки ошибок const clearErrorsBtn = document.getElementById('clearErrorsBtn'); const successAction = document.getElementById('successAction'); clearErrorsBtn.addEventListener('click', async function() { try { const response = await fetch('/clear-old-errors', { method: 'POST', headers: { 'Content-Type': 'application/json', } }); if (!response.ok) { throw new Error('Ошибка при очистке старых ошибок'); } const data = await response.json(); // Показываем сообщение об успехе successAction.textContent = 'Удалено ' + data.deletedCount + ' записей'; successAction.style.display = 'block'; // Перезагружаем страницу через 2 секунды setTimeout(() => { window.location.reload(); }, 2000); } catch (error) { console.error('Ошибка:', error); alert('Произошла ошибка: ' + error.message); } }); }); `; res.send(` Multy Stub v${pkg.version}

Multy Stub v${pkg.version}

${totalEndpoints}
Всего эндпоинтов

Routers:

${routerFolders.map((f) => `
${f}
`).join('')}

Структура директорий проекта:

${generateDirList(directoryResult.items)}

Models:

${ (await Promise.all( (await mongoose.modelNames()).map(async (name) => { const model = mongoose.model(name); const count = await model.countDocuments(); // Получаем информацию о полях модели const schema = model.schema; const fields = Object.keys(schema.paths).filter(field => !['__v', '_id'].includes(field)); return `

${name}

${count} документов

Поля:

    ${fields.map(field => { const fieldType = schema.paths[field].instance; return `
  • ${field}: ${fieldType}
  • `; }).join('')}
`; } ) )).join('') }

Последние ошибки:

${errorsHtml}
Операция выполнена успешно
`) }) // Эндпоинт для очистки ошибок старше 10 дней router.post('/clear-old-errors', async (req, res) => { try { const tenDaysAgo = new Date(); tenDaysAgo.setDate(tenDaysAgo.getDate() - 10); const result = await ErrorLog.deleteMany({ createdAt: { $lt: tenDaysAgo } }); res.json({ success: true, deletedCount: result.deletedCount || 0 }); } catch (error) { console.error('Ошибка при очистке старых ошибок:', error); res.status(500).json({ success: false, error: 'Ошибка при очистке старых ошибок' }); } }); export default router