import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import http from 'http'; import passport from './modules/auth/passport.config'; import authRoutes from './modules/auth/auth.routes'; import oauthRoutes from './modules/auth/oauth.routes'; import adminRoutes from './modules/admin/admin.routes'; import ticketRoutes from './modules/ticket/ticket.routes'; import checkRoutes from './modules/check/check.routes'; import blogRoutes from './modules/blog/blog.routes'; import notificationRoutes from './modules/notification/notification.routes'; import userRoutes from './modules/user/user.routes'; import sessionRoutes from './modules/session/session.routes'; import qrAuthRoutes from './modules/qr-auth/qr-auth.routes'; import storageRoutes from './modules/storage/storage.routes'; import { logger } from './utils/logger'; dotenv.config(); const app = express(); app.set('trust proxy', 1); const allowedOrigins = Array.from(new Set([ process.env.PUBLIC_APP_ORIGIN, process.env.PUBLIC_API_ORIGIN, 'http://localhost:3000', 'http://localhost:5173', 'https://ospab.host', 'https://api.ospab.host' ].filter((origin): origin is string => Boolean(origin)))); const stripTrailingSlash = (value: string) => (value.endsWith('/') ? value.slice(0, -1) : value); const deriveWebsocketUrl = (origin: string) => { try { const url = new URL(origin); url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; url.pathname = '/ws'; url.search = ''; url.hash = ''; return url.toString(); } catch (error) { logger.warn('[Server] Не удалось сконструировать WS URL, возвращаем origin как есть', error); return origin; } }; const buildUrl = (origin: string, pathname: string) => { try { const url = new URL(origin); url.pathname = pathname; url.search = ''; url.hash = ''; return url.toString(); } catch { return `${stripTrailingSlash(origin)}${pathname}`; } }; app.use(cors({ origin: allowedOrigins, credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] })); app.use(express.json({ limit: '100mb' })); app.use(express.urlencoded({ limit: '100mb', extended: true })); app.use(passport.initialize()); // Глобальная обработка необработанных ошибок и Promise rejection — логируем и не даём молча закрывать соединение process.on('uncaughtException', (err) => { try { logger.error('[Process] uncaughtException', err); } catch (e) { console.error('[Process] uncaughtException', err); } }); process.on('unhandledRejection', (reason) => { try { logger.error('[Process] unhandledRejection', reason); } catch (e) { console.error('[Process] unhandledRejection', reason); } }); app.get('/health', (_req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); app.get('/', async (req, res) => { // Статистика WebSocket const wsConnectedUsers = getConnectedUsersCount(); const wsRoomsStats = getRoomsStats(); res.json({ message: 'Сервер ospab.host запущен!', timestamp: new Date().toISOString(), port: PORT, database: process.env.DATABASE_URL ? 'подключена' : 'НЕ НАСТРОЕНА', websocket: { connected_users: wsConnectedUsers, rooms: wsRoomsStats } }); }); // ==================== SITEMAP ==================== app.get('/sitemap.xml', (req, res) => { const baseUrl = 'https://ospab.host'; const staticPages = [ { loc: '/', priority: '1.0', changefreq: 'weekly', description: 'Ospab Host - Облачное S3 хранилище и хостинг сайтов' }, { loc: '/about', priority: '0.9', changefreq: 'monthly', description: 'О компании - Современная платформа хранения данных' }, { loc: '/login', priority: '0.7', changefreq: 'monthly', description: 'Вход в панель управления с QR-аутентификацией' }, { loc: '/register', priority: '0.8', changefreq: 'monthly', description: 'Регистрация аккаунта - Начните за 2 минуты' }, { loc: '/blog', priority: '0.85', changefreq: 'daily', description: 'Блог о S3 хранилище и хостинге' }, { loc: '/terms', priority: '0.5', changefreq: 'yearly', description: 'Условия использования сервиса' }, { loc: '/privacy', priority: '0.5', changefreq: 'yearly', description: 'Политика конфиденциальности и защита данных' }, ]; let xml = '\n'; xml += '\n'; const lastmod = new Date().toISOString().split('T')[0]; for (const page of staticPages) { xml += ' \n'; xml += ` ${baseUrl}${page.loc}\n`; xml += ` ${lastmod}\n`; xml += ` ${page.priority}\n`; xml += ` ${page.changefreq}\n`; xml += ' \n'; } xml += ''; res.header('Content-Type', 'application/xml'); res.send(xml); }); // ==================== ROBOTS.TXT ==================== app.get('/robots.txt', (req, res) => { const robots = `# ospab Host - Облачное S3 хранилище и хостинг # Хранение данных, техподдержка 24/7 User-agent: * Allow: / Allow: /about Allow: /login Allow: /register Allow: /blog Allow: /blog/* Allow: /terms Allow: /privacy Allow: /uploads/blog # Запрет индексации приватных разделов Disallow: /dashboard Disallow: /dashboard/* Disallow: /api/ Disallow: /qr-login Disallow: /admin Disallow: /admin/* Disallow: /uploads/avatars Disallow: /uploads/tickets Disallow: /uploads/checks Sitemap: https://ospab.host/sitemap.xml # Поисковые роботы User-agent: Googlebot Allow: / Crawl-delay: 0 User-agent: Yandexbot Allow: / Crawl-delay: 0 User-agent: Bingbot Allow: / Crawl-delay: 0 User-agent: Mail.RU_Bot Allow: / Crawl-delay: 1`; res.header('Content-Type', 'text/plain; charset=utf-8'); res.send(robots); }); import path from 'path'; // Публичный доступ к блогу, аватарам и файлам тикетов app.use('/uploads/blog', express.static(path.join(__dirname, '../uploads/blog'))); app.use('/uploads/avatars', express.static(path.join(__dirname, '../uploads/avatars'))); app.use('/uploads/tickets', express.static(path.join(__dirname, '../uploads/tickets'))); // Логирование всех запросов в /api/auth (не модифицируем логику, только логируем) app.use('/api/auth', (req, res, next) => { const start = Date.now(); try { logger.info('[Audit] Auth request received', { method: req.method, path: req.originalUrl, ip: req.ip || req.connection.remoteAddress, }); } catch (err) { console.error('[Audit] Failed to log auth request received', err); } // Log when response finished res.on('finish', () => { try { logger.info('[Audit] Auth request finished', { method: req.method, path: req.originalUrl, ip: req.ip || req.connection.remoteAddress, statusCode: res.statusCode, durationMs: Date.now() - start, }); } catch (err) { console.error('[Audit] Failed to log auth request finished', err); } }); next(); }); app.use('/api/auth', authRoutes); app.use('/api/auth', oauthRoutes); app.use('/api/admin', adminRoutes); app.use('/api/ticket', ticketRoutes); app.use('/api/check', checkRoutes); app.use('/api/blog', blogRoutes); app.use('/api/notifications', notificationRoutes); app.use('/api/user', userRoutes); app.use('/api/sessions', sessionRoutes); app.use('/api/qr-auth', qrAuthRoutes); app.use('/api/storage', storageRoutes); const PORT = process.env.PORT || 5000; import { initWebSocketServer, getConnectedUsersCount, getRoomsStats } from './websocket/server'; import https from 'https'; import fs from 'fs'; const keyPath = process.env.SSL_KEY_PATH ?? '/etc/apache2/ssl/ospab.host.key'; const certPath = process.env.SSL_CERT_PATH ?? '/etc/apache2/ssl/ospab.host.fullchain.crt'; const shouldUseHttps = process.env.NODE_ENV === 'production'; const PUBLIC_API_ORIGIN = process.env.PUBLIC_API_ORIGIN || (shouldUseHttps ? 'https://api.ospab.host' : `http://localhost:${PORT}`); const normalizedApiOrigin = stripTrailingSlash(PUBLIC_API_ORIGIN); const PUBLIC_WS_URL = process.env.PUBLIC_WS_URL || deriveWebsocketUrl(PUBLIC_API_ORIGIN); let server: http.Server | https.Server; let protocolLabel = 'HTTP'; if (shouldUseHttps) { const missingPaths: string[] = []; if (!fs.existsSync(keyPath)) { missingPaths.push(keyPath); } if (!fs.existsSync(certPath)) { missingPaths.push(certPath); } if (missingPaths.length > 0) { console.error('[Server] SSL режим включён, но сертификаты не найдены:', missingPaths.join(', ')); console.error('[Server] Укажите корректные пути в переменных SSL_KEY_PATH и SSL_CERT_PATH. Сервер остановлен.'); process.exit(1); } const sslOptions = { key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) }; server = https.createServer(sslOptions, app); protocolLabel = 'HTTPS'; } else { server = http.createServer(app); } // Инициализация основного WebSocket сервера для real-time обновлений const wss = initWebSocketServer(server); // Установка timeout для всех запросов (120 сек = 120000 мс) server.setTimeout(120000); // Глобальный express error handler — логируем и возвращаем 500, не ломая сокет app.use((err: any, _req: any, res: any, _next: any) => { try { logger.error('[Express] Unhandled error:', err); } catch (e) { console.error('[Express] Unhandled error:', err); } if (!res.headersSent) { res.status(500).json({ message: 'Internal server error' }); } }); server.listen(PORT, () => { logger.info(`${protocolLabel} сервер запущен на порту ${PORT}`); logger.info(`База данных: ${process.env.DATABASE_URL ? 'подключена' : 'НЕ НАСТРОЕНА'}`); logger.info(`API доступен: ${normalizedApiOrigin}`); logger.info(`WebSocket доступен: ${PUBLIC_WS_URL}`); logger.info(`Sitemap доступен: ${buildUrl(normalizedApiOrigin, '/sitemap.xml')}`); logger.info(`Robots.txt доступен: ${buildUrl(normalizedApiOrigin, '/robots.txt')}`); });