/
DATKAI
/
label_system
Обзор
Документация
Войти
/
DATKAI
/
label_system
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
server.js
236 строк
11 KB
DATKAI
Оповещения: события, очередь и журнал — v3.11.0
20 июл 2026, 17:17
20 июл 2026, 17:17
38fdfc2
Код
Авторство
О чём код?
'use strict'; const http = require('http'); const fs = require('fs'); const path = require('path'); const os = require('os'); const { initDB, readDB } = require('./lib/db'); const { OK, ERR, CORS } = require('./lib/helpers'); const sessions = require('./lib/sessions'); const events = require('./lib/events'); // ── Политика доступа к API ──────────────────────────────────── // Возвращает { auth } или { auth, perm } — что требуется для запроса. // GET и /api/auth/* открыты. Мутации требуют валидный токен; админ-зоны — // конкретное право. Операторские действия (задания, печать) — только вход. function apiPolicy(method, p){ if(p.startsWith('/api/auth/')) return { auth: false }; if(p.startsWith('/api/update')) return { auth: true, perm: 'all' }; // и GET только полному админу if(p.startsWith('/api/notify')) return { auth: true, perm: 'notify' }; // и GET: настройки SMTP не публичны if(method === 'GET' || method === 'OPTIONS') return { auth: false }; if(p.startsWith('/api/users')) return { auth: true, perm: 'users' }; if(p.startsWith('/api/backup')) return { auth: true, perm: 'backup' }; if(p.startsWith('/api/templates')) return { auth: true, perm: 'templates' }; if(p.startsWith('/api/decors')) return { auth: true, perm: 'decors' }; if(p.startsWith('/api/settings') || p === '/api/company') return { auth: true, perm: 'panel' }; if(p === '/api/log' && method === 'DELETE') return { auth: true, perm: 'log' }; // Остальное (задания, продукты/раскрой, печать в журнал, обратная связь) — любой вошедший return { auth: true }; } // ── Route modules ──────────────────────────────────────────── const routes = [ require('./routes/auth'), require('./routes/users'), require('./routes/decors'), require('./routes/templates'), require('./routes/tasks'), require('./routes/shifts'), require('./routes/products'), require('./routes/settings'), require('./routes/notify'), require('./routes/backup'), require('./routes/update'), ]; const PORT = process.env.PORT || 3000; const PUBLIC = path.join(__dirname, 'public'); const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.png': 'image/png', '.ico': 'image/x-icon', '.svg': 'image/svg+xml', }; // ── Auto-backup ─────────────────────────────────────────────── let _backupTimer = null; function setupAutoBackup(cfg){ if(_backupTimer) clearInterval(_backupTimer); if(!cfg?.enabled) return; const BACKUPS = require('./lib/db').BACKUPS; _backupTimer = setInterval(()=>{ try{ const db = readDB(); const ts = new Date().toISOString().replace(/[:.]/g,'-'); const name = `backup_${ts}.json`; fs.writeFileSync(path.join(BACKUPS, name), JSON.stringify(db,null,2), 'utf8'); const files = fs.readdirSync(BACKUPS).filter(f=>f.endsWith('.json')).sort(); const keep = cfg.keep||5; while(files.length > keep) fs.unlinkSync(path.join(BACKUPS, files.shift())); console.log('[AutoBackup] Сохранён бэкап:', ts); } catch(e){ console.error('[AutoBackup] Ошибка:', e.message); } }, cfg.interval||86400000); } // ── Local IP ────────────────────────────────────────────────── function getLocalIP(){ try{ const ifaces = os.networkInterfaces(); for(const name of Object.keys(ifaces)){ for(const iface of ifaces[name]){ if(iface.family==='IPv4' && !iface.internal) return iface.address; } } } catch(e){} return null; } // ── Диагностика (для поиска причины «зависаний») ────────────── const _diag = { reqs: 0, slow: 0, errors: 0, lastSlow: '' }; // ── HTTP server ─────────────────────────────────────────────── const server = http.createServer(async (req, res) => { const m = req.method; const p = req.url.split('?')[0]; const _t0 = Date.now(); _diag.reqs++; // Логируем медленные запросы (>1с) — блокировка event loop = «подвисание» для всех res.on('finish', () => { const dt = Date.now() - _t0; if(dt > 1000){ _diag.slow++; _diag.lastSlow = `${m} ${p} — ${dt}мс`; console.warn(`[SLOW] ${new Date().toISOString()} ${m} ${p} — ${dt}мс`); } }); // CORS preflight if(m === 'OPTIONS'){ res.writeHead(204, CORS); return res.end(); } // Health-check / диагностика — память, сессии, нагрузка if(p === '/api/health'){ let version = '?'; try { version = require('./package.json').version; } catch(e){} const mem = process.memoryUsage(); return OK(res, { ok: true, version, uptime: Math.round(process.uptime()), time: new Date().toISOString(), memRssMB: Math.round(mem.rss/1048576), memHeapMB: Math.round(mem.heapUsed/1048576), sessions: sessions.count(), reqs: _diag.reqs, slow: _diag.slow, errors: _diag.errors, lastSlow: _diag.lastSlow, }); } // SSE-поток оповещений (мгновенные тревоги операторам). // EventSource не умеет слать заголовки — токен принимаем в query (?token=). if(p === '/api/events'){ let token = ''; try { token = new URL(req.url, 'http://x').searchParams.get('token') || ''; } catch(e){} const session = sessions.resolve(token || sessions.tokenFromReq(req)); if(!session){ res.writeHead(401, CORS); return res.end(); } res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', ...CORS, }); res.write(': connected\n\n'); const client = { res, login: session.login, role: session.role }; events.addClient(client); req.on('close', () => events.removeClient(client)); return; // держим соединение открытым } // Static files if(!p.startsWith('/api/')){ const rel = decodeURIComponent(p === '/' ? '/index.html' : p); // Защита от path traversal: нормализуем и проверяем, что путь внутри PUBLIC. const full = path.normalize(path.join(PUBLIC, rel)); if((full === PUBLIC || full.startsWith(PUBLIC + path.sep)) && fs.existsSync(full) && fs.statSync(full).isFile()){ const ext = path.extname(full); res.writeHead(200, { 'Content-Type': MIME[ext]||'application/octet-stream' }); return res.end(fs.readFileSync(full)); } res.writeHead(404); return res.end('Not found'); } // Авторизация: резолвим токен и применяем политику доступа const session = sessions.resolve(sessions.tokenFromReq(req)); req.auth = session; // доступно роутам (для аудита и т.п.) const pol = apiPolicy(m, p); if(pol.auth){ if(!session) return ERR(res, 401, 'Требуется вход'); if(pol.perm && !sessions.hasPerm(session, pol.perm)) return ERR(res, 403, 'Недостаточно прав'); } // API — try each route module try { for(const route of routes){ await route.handle(p, m, req, res); if(res.writableEnded) return; // response already sent } if(!res.writableEnded) ERR(res, 404, 'api not found'); } catch(e){ _diag.errors++; console.error('[Server error]', e.message); if(!res.writableEnded) ERR(res, 500, e.message); } }); // ── Start ───────────────────────────────────────────────────── initDB(); const _db = readDB(); if(_db.autoBackup?.enabled) setupAutoBackup(_db.autoBackup); // Очередь оповещений: фоновая отправка писем с повторами require('./lib/outbox').start(15000); // Пульс раз в минуту: память, сессии, нагрузка + чистка просроченных сессий. // Помогает поймать утечку памяти / зависание (смотрите, растёт ли memRss). setInterval(() => { const removed = sessions.cleanup(); const mem = process.memoryUsage(); console.log(`[HEARTBEAT] ${new Date().toISOString()} rss=${Math.round(mem.rss/1048576)}MB heap=${Math.round(mem.heapUsed/1048576)}MB sessions=${sessions.count()} reqs=${_diag.reqs} slow=${_diag.slow} err=${_diag.errors}${removed?` (сессий очищено: ${removed})`:''}`); }, 60*1000).unref(); // Понятное сообщение, если порт занят (уже запущен другой экземпляр) — // вместо падения со стек-трейсом. server.on('error', (e) => { if(e.code === 'EADDRINUSE'){ console.error('\n┌─────────────────────────────────────────────────────────────┐'); console.error(`│ ⚠ ПОРТ ${PORT} УЖЕ ЗАНЯТ — сервер уже запущен. │`); console.error('│ Закройте старое окно/процесс перед запуском нового: │'); console.error('│ 1) netstat -ano | findstr :' + PORT + ' — узнать PID │'); console.error('│ 2) taskkill /PID <PID> /F — закрыть его │'); console.error('│ Либо закрыть ВСЕ node: taskkill /IM node.exe /F │'); console.error('└─────────────────────────────────────────────────────────────┘\n'); process.exit(1); } console.error('[Server listen error]', e.message); process.exit(1); }); server.listen(PORT, '0.0.0.0', ()=>{ const ip = getLocalIP(); const line = '+' + '-'.repeat(46) + '+'; console.log('\n'+line); console.log('| LABEL SYSTEM v4.0 |'); console.log('| Разработал: Ковыляк Александр |'); console.log(line); console.log(`| Local: http://localhost:${PORT} |`); if(ip){ const netLine = `| Network: http://${ip}:${PORT}`; console.log(netLine.padEnd(48)+'|'); } console.log(line+'\n'); });