'use strict'; const path = require('path'); const crypto = require('crypto'); const express = require('express'); const config = require('./lib/config'); const ldapClient = require('./lib/ldapClient'); const adminApi = require('./lib/adminApi'); const registerApi = require('./lib/registerApi'); const directoryApi = require('./lib/directoryApi'); const gosuslugiApi = require('./lib/gosuslugiApi'); const zupApi = require('./lib/zupApi'); const favicon = require('./lib/favicon'); const adminSettings = require('./lib/adminSettings'); const sessions = require('./lib/sessions'); const audit = require('./lib/auditLog'); const rateLimit = require('./lib/rateLimit'); const app = express(); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'), { setHeaders(res, filePath) { if (/\.(css|js|woff2?|png|jpe?g|svg|ico)$/i.test(filePath)) { res.setHeader('Cache-Control', 'public, max-age=604800'); } else { res.setHeader('Cache-Control', 'no-cache'); } } })); app.use((req, res, next) => { res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('Referrer-Policy', 'no-referrer'); next(); }); function getToken(req) { const header = req.headers.authorization || ''; if (header.startsWith('Bearer ')) return header.slice(7); return null; } function timingSafeEqualStr(a, b) { const ba = Buffer.from(String(a)); const bb = Buffer.from(String(b)); if (ba.length !== bb.length) return false; return crypto.timingSafeEqual(ba, bb); } function adminAliasesSet() { const aliases = new Set(ldapClient.buildBindNames(config.adminLogin).map((n) => n.toLowerCase())); aliases.add(config.adminLogin.split('@')[0].split('\\').pop().toLowerCase()); return aliases; } function basicAuthOk(req) { const header = req.headers.authorization || ''; if (!header.startsWith('Basic ')) return false; if (!config.adminLogin || !config.adminPassword) return false; let decoded; try { decoded = Buffer.from(header.slice(6), 'base64').toString('utf8'); } catch (err) { return false; } const idx = decoded.indexOf(':'); if (idx === -1) return false; const username = decoded.slice(0, idx); const password = decoded.slice(idx + 1); if (!adminAliasesSet().has(String(username).toLowerCase())) return false; return timingSafeEqualStr(password, config.adminPassword); } function authEnabled() { return adminSettings.getSettings().authEnabled !== false; } app.get('/', (req, res) => res.redirect(302, authEnabled() ? '/auth' : '/register')); app.get('/auth', (req, res) => { if (!authEnabled()) return res.redirect(302, '/register'); res.sendFile(path.join(__dirname, 'public', 'auth.html')); }); app.get('/admin', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'admin.html')); }); app.get('/gosuslugi', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'gosuslugi.html')); }); app.get('/zup', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'zup.html')); }); app.get('/register', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'register.html')); }); app.get('/change-password', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'change-password.html')); }); app.get('/help', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'help.html')); }); app.get('/favicon.ico', (req, res) => { const ico = favicon.getFavicon(); if (!ico) return res.status(404).end(); res.setHeader('Cache-Control', 'public, max-age=604800'); res.type('image/x-icon'); res.send(ico); }); app.get('/health', async (req, res) => { try { await ldapClient.testConnection(); res.json({ status: 'ok', ldap: 'connected' }); } catch (err) { res.status(503).json({ status: 'error', error: err.message }); } }); app.get('/status', (req, res) => { res.setHeader('Cache-Control', 'no-store'); if (!sessions.getSession(getToken(req)) && !basicAuthOk(req)) { res.setHeader('WWW-Authenticate', 'Basic realm="LDAP Auth Admin"'); return res.status(401).json({ success: false, error: 'Требуется авторизация' }); } res.json({ status: 'running', service: 'LDAP Auth', ldap_url: config.ldapUrl, base_dn: config.baseDN, timestamp: new Date().toISOString() }); }); app.post('/api/auth', async (req, res) => { const { username, password } = req.body || {}; const ip = rateLimit.getClientIp(req); if (rateLimit.isBlocked(ip)) { return res.status(429).json({ success: false, error: 'Слишком много попыток, попробуйте через час' }); } const all = req.query.all === '1'; const description = req.query.description === '1'; const groups = req.query.groups === '1'; const modeCount = [all, description, groups].filter(Boolean).length; if (modeCount > 1) { return res.status(400).json({ success: false, error: 'Conflicting parameters: use only one of description=1, groups=1, or all=1' }); } if (!username) { return res.status(400).json({ success: false, error: 'Username is required' }); } let mode = 'basic'; if (description) mode = 'description'; else if (groups) mode = 'groups'; else if (all) mode = 'all'; try { const result = await ldapClient.authenticate(username, password, mode); rateLimit.reset(ip); audit.logAuthEntry({ ip, username, success: true }); res.json(result); } catch (err) { if (err.status === 401) { rateLimit.recordFailure(ip); audit.logAuthEntry({ ip, username, success: false }); } res.status(err.status || 401).json({ success: false, error: err.message }); } }); app.use('/api/admin', adminApi); app.use('/api/register', registerApi); app.use('/api/directory', directoryApi); app.use('/api/gosuslugi', gosuslugiApi); app.use('/api/zup', zupApi); app.get('/vendor/xlsx.full.min.js', (req, res) => { res.setHeader('Cache-Control', 'public, max-age=604800'); res.sendFile(path.join(__dirname, 'node_modules', 'xlsx', 'dist', 'xlsx.full.min.js')); }); app.use((err, req, res, next) => { const status = err.status || err.statusCode || 500; if (status >= 500) console.error(err); res.status(status).json({ success: false, error: status >= 500 ? 'Внутренняя ошибка сервера' : (err.message || 'Некорректный запрос') }); }); async function main() { console.log('=== LDAP Auth Service Starting ==='); console.log(`LDAP URL: ${config.ldapUrl}`); console.log(`Base DN: ${config.baseDN}`); console.log(`Admin: ${config.adminLogin || '(not set)'}`); console.log(`Port: ${config.port}`); console.log('=================================='); await favicon.loadFavicon(); if (favicon.getFavicon()) { console.log('✓ Favicon prepared from logo.png'); } else { console.warn('⚠ logo.png not found or invalid — favicon disabled'); } if (!config.adminLogin || !config.adminPassword) { console.warn('⚠ ADMIN_LOGIN / ADMIN_PASSWORD not set in .env - admin panel disabled'); } else { try { await ldapClient.testConnection(); console.log('✓ LDAP connection test passed'); } catch (err) { console.warn(`⚠ LDAP connection test failed: ${err.message}`); } } app.listen(config.port, () => { console.log(`🚀 Server running on http://localhost:${config.port}`); console.log(`🌐 Web auth: http://localhost:${config.port}/auth`); console.log(`🛠 Admin panel: http://localhost:${config.port}/admin`); console.log(`📊 Health: http://localhost:${config.port}/health`); }); } main();