Files
ldap-auth-service/server.js
2026-08-24 17:46:07 +05:00

231 lines
7.5 KiB
JavaScript

'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();